1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-22 16:56:48 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
f6ad6197de ListViews et templates 2025-07-05 19:47:35 +02:00
6c7d86185a Models 2025-07-03 14:34:04 +02:00
56 changed files with 2710 additions and 1719 deletions

View File

@ -21,6 +21,3 @@ EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration
WIKI_USER=NoteKfet2020
WIKI_PASSWORD=
# OIDC
OIDC_RSA_PRIVATE_KEY=CHANGE_ME

View File

@ -8,7 +8,7 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04
py310-django52:
py310-django42:
stage: test
image: ubuntu:22.04
before_script:
@ -22,10 +22,10 @@ py310-django52:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py310-django52
script: tox -e py310-django42
# Debian Bookworm
py311-django52:
py311-django42:
stage: test
image: debian:bookworm
before_script:
@ -37,7 +37,7 @@ py311-django52:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py311-django52
script: tox -e py311-django42
linters:
stage: quality-assurance

View File

@ -61,8 +61,8 @@ Bien que cela permette de créer une instance sur toutes les distributions,
6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et copier la clé dans .env dans le champ
`OIDC_RSA_PRIVATE_KEY`.
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy :
@ -237,8 +237,8 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner le champ
`OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`).
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/*

0
apps/family/__init__.py Normal file
View File

11
apps/family/apps.py Normal file
View File

@ -0,0 +1,11 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FamilyConfig(AppConfig):
name = 'family'
verbose_name = _('family')

View File

@ -0,0 +1,85 @@
# Generated by Django 4.2.21 on 2025-07-04 19:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChallengeCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
],
options={
'verbose_name': 'challenge category',
'verbose_name_plural': 'challenge categories',
},
),
migrations.CreateModel(
name='Family',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('score', models.PositiveIntegerField(verbose_name='score')),
('rank', models.PositiveIntegerField(verbose_name='rank')),
],
options={
'verbose_name': 'Family',
'verbose_name_plural': 'Families',
},
),
migrations.CreateModel(
name='Challenge',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('points', models.PositiveIntegerField(verbose_name='points')),
('obtained', models.PositiveIntegerField(verbose_name='obtained')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challengecategory', verbose_name='category')),
],
options={
'verbose_name': 'challenge',
'verbose_name_plural': 'challenges',
},
),
migrations.CreateModel(
name='Achievement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('obtained_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='obtained at')),
('challenge', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challenge')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.family', verbose_name='family')),
],
options={
'verbose_name': 'achievement',
'verbose_name_plural': 'achievements',
},
),
migrations.CreateModel(
name='FamilyMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField(default=2025, verbose_name='year')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='members', to='family.family', verbose_name='family')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='family_memberships', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'family membership',
'verbose_name_plural': 'family memberships',
'unique_together': {('user', 'year')},
},
),
]

View File

202
apps/family/models.py Normal file
View File

@ -0,0 +1,202 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models, transaction
from django.utils import timezone
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
class Family(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
unique=True
)
description = models.CharField(
max_length=255,
verbose_name=_('description')
)
score = models.PositiveIntegerField(
verbose_name=_('score')
)
rank = models.PositiveIntegerField(
verbose_name=_('rank'),
)
class Meta:
verbose_name = _('Family')
verbose_name_plural = _('Families')
def __str__(self):
return self.name
class FamilyMembership(models.Model):
user = models.OneToOneField(
User,
on_delete=models.PROTECT,
related_name=_('family_memberships'),
verbose_name=_('user'),
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
related_name=_('members'),
verbose_name=_('family'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
default=timezone.now().year,
)
class Meta:
unique_together = ('user', 'year',)
verbose_name = _('family membership')
verbose_name_plural = _('family memberships')
def __str__(self):
return _('Family membership of {user} to {family}').format(user=self.user.username, family=self.family.name, )
class ChallengeCategory(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
unique=True,
)
class Meta:
verbose_name = _('challenge category')
verbose_name_plural = _('challenge categories')
def __str__(self):
return self.name
class Challenge(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
)
description = models.CharField(
max_length=255,
verbose_name=_('description'),
)
points = models.PositiveIntegerField(
verbose_name=_('points'),
)
category = models.ForeignKey(
ChallengeCategory,
verbose_name=_('category'),
on_delete=models.PROTECT
)
obtained = models.PositiveIntegerField(
verbose_name=_('obtained')
)
class Meta:
verbose_name = _('challenge')
verbose_name_plural = _('challenges')
def __str__(self):
return self.name
class Achievement(models.Model):
challenge = models.ForeignKey(
Challenge,
on_delete=models.PROTECT,
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
verbose_name=_('family'),
)
obtained_at = models.DateTimeField(
verbose_name=_('obtained at'),
default=timezone.now,
)
class Meta:
verbose_name = _('achievement')
verbose_name_plural = _('achievements')
def __str__(self):
return _('Challenge {challenge} carried out by Family {family}').format(challenge=self.challenge.name, family=self.family.name, )
@classmethod
def update_ranking(cls, *args, **kwargs):
"""
Update ranking when adding or removing points
"""
family_set = cls.objects.select_for_update().all().order_by("-score")
for i in range(family_set.count()):
if i == 0 or family_set[i].score != family_set[i - 1].score:
new_rank = i + 1
family = family_set[i]
family.rank = new_rank
family._force_save = True
family.save()
@transaction.atomic
def save(self, *args, **kwargs):
"""
When saving, also grants points to the family
"""
self.family = Family.objects.select_for_update().get(pk=self.family_id)
self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id)
challenge_points = self.challenge.points
is_new = self.pk is None
super.save(*args, **kwargs)
# Only grant points when getting a new achievement
if is_new:
self.family.refresh_from_db()
self.family.score += challenge_points
self.family._force_save = True
self.family.save()
self.challenge.refresh_from_db()
self.challenge.obtained += 1
self.challenge._force_save = True
self.challenge.save()
self.__class__.update_ranking()
@transaction.atomic
def delete(self, *args, **kwargs):
"""
When deleting, also removes points from the family
"""
# Get the family and challenge before deletion
self.family = Family.objects.select_for_update().get(pk=self.family_id)
challenge_points = self.challenge.points
# Delete the achievement
super().delete(*args, **kwargs)
# Remove points from the family
self.family.refresh_from_db()
self.family.score -= challenge_points
self.family._force_save = True
self.family.save()
self.challenge.refresh_from_db()
self.challenge.obtained -= 1
self.challenge._force_save = True
self.challenge.save()
self.__class__.update_ranking()

40
apps/family/tables.py Normal file
View File

@ -0,0 +1,40 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django_tables2 import A
from .models import Family, Challenge
class FamilyTable(tables.Table):
"""
List all families
"""
name = tables.LinkColumn(
"family:family_detail",
args=[A("pk")],
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Family
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'score', 'rank',)
order_by = ('rank',)
class ChallengeTable(tables.Table):
"""
List all challenges
"""
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('id',)
model = Challenge
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'points', 'category',)

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="{% url "family:family_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Families" %}
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Challenges" %}
</a>
</div>
</div>
</div>
<<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Families" %}
</a>
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Challenges" %}
</a>
</div>
</div>
</div>
<<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}

12
apps/family/urls.py Normal file
View File

@ -0,0 +1,12 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import FamilyListView, ChallengeListView
app_name = 'family'
urlpatterns = [
path('list/', FamilyListView.as_view(), name="family_list"),
path('challenge/list/', ChallengeListView.as_view(), name="challenge_list"),
]

63
apps/family/views.py Normal file
View File

@ -0,0 +1,63 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView, UpdateView
from django.utils.translation import gettext_lazy as _
from django_tables2 import SingleTableView
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .models import Family, Challenge
from .tables import FamilyTable, ChallengeTable
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create family
"""
model = Family
extra_context = {"title": _('Create family')}
def get_sample_object(self):
return Family(
name="",
description="Sample family",
score=0,
rank=0,
)
class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing Families
"""
model = Family
table_class = FamilyTable
extra_context = {"title": _('Families list')}
class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Display details of a family
"""
model = Family
context_object_name = "family"
extra_context = {"title": _('Family detail')}
class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the information of a family.
"""
model = Family
context_object_name = "family"
extra_context = {"title": _('Update family')}
class ChallengeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all challenges
"""
model = Challenge
table_class = ChallengeTable
extra_context = {"title": _('Challenges list')}

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(), Food, "change")).exclude(pk=pk)
).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
class Meta:
model = TransformedFood

View File

@ -12,21 +12,18 @@ 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 %}
@ -34,23 +31,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,52 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %}
{% block content %}
<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>
{{ block.super }}
<br>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
@ -113,20 +68,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endfor %}
{% endif %}
</div>
<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 %}
{% endblock %}

View File

@ -18,5 +18,4 @@ 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,7 +10,6 @@ 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 _
@ -64,8 +63,7 @@ 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})
| Q(**{f'owner__name{suffix}': prefix + pattern}))
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern}))
else:
qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
@ -455,8 +453,6 @@ 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"))
@ -510,14 +506,3 @@ 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

@ -6,7 +6,7 @@ from django.conf import settings
from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _
from .signals import save_user_profile, update_wei_registration_fee_on_membership_creation, update_wei_registration_fee_on_club_change
from .signals import save_user_profile
class MemberConfig(AppConfig):
@ -17,16 +17,7 @@ class MemberConfig(AppConfig):
"""
Define app internal signals to interact with other apps
"""
from .models import Membership, Club
post_save.connect(
save_user_profile,
sender=settings.AUTH_USER_MODEL,
)
post_save.connect(
update_wei_registration_fee_on_membership_creation,
sender=Membership
)
post_save.connect(
update_wei_registration_fee_on_club_change,
sender=Club
)

View File

@ -438,6 +438,8 @@ 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()
@ -456,6 +458,8 @@ 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()
@ -467,6 +471,8 @@ 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

@ -13,25 +13,3 @@ def save_user_profile(instance, created, raw, **_kwargs):
instance.profile.email_confirmed = True
instance.profile.registration_valid = True
instance.profile.save()
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if created:
from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter(
user=instance.user,
wei__year=instance.date_start.year,
)
for r in registrations:
r.save()
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
from wei.models import WEIRegistration
if instance.id == 1 or instance.id == 2:
registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year,
)
for r in registrations:
r.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.post(reverse("logout"))
response = self.client.get(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, basename='alias2')
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet)

View File

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

View File

@ -1,10 +1,8 @@
# 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
@ -18,58 +16,26 @@ class PermissionScopes(BaseScopes):
and can be useful to make queries through the API with limited privileges.
"""
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_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_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
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
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])]
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
scopes = [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
class PermissionOAuth2Validator(OAuth2Validator):
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"]
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
@ -88,8 +54,6 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes:
valid_scopes.add(scope)
if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes

View File

@ -13,14 +13,12 @@ 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,24 +164,14 @@ class ScopesView(LoginRequiredMixin, TemplateView):
from oauth2_provider.models import Application
from .scopes import PermissionScopes
oidc = False
scopes = PermissionScopes()
context["scopes"] = {}
all_scopes = scopes.get_all_scopes()
for app in Application.objects.filter(user=self.request.user).all():
available_scopes = PermissionScopes().get_available_scopes(app)
available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict()
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 = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
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(max(transaction.total - 2000, 0) for transaction in self.transactions.all())
amount = sum(transaction.total 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(max(transaction.total - 2000, 0) for transaction in self.transactions.all() if not transaction.valid)
total_fee = sum(transaction.total 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', 'deposit_check', 'birth_date', 'gender',
'wei__email', 'wei__year', 'soge_credit', 'caution_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

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

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, RadioSelect
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -24,8 +24,7 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
"deposit_amount": AmountInput(),
"fee_soge_credit": AmountInput(),
"caution_amount": AmountInput(),
}
@ -44,7 +43,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', 'deposit_check', 'deposit_type'
'first_year', 'information_json', 'caution_check'
]
widgets = {
"user": Autocomplete(
@ -59,13 +58,26 @@ class WEIRegistrationForm(forms.ModelForm):
'minDate': '1900-01-01',
'maxDate': '2100-01-01'
}),
"deposit_check": forms.BooleanField(
"caution_check": forms.BooleanField(
required=False,
),
"deposit_type": forms.RadioSelect(),
}
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,
@ -87,7 +99,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(Q(name="Adhérent⋅e WEI") | Q(name="\u00c9lectron libre")).all(),
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
widget=CheckboxSelectMultiple(),
)
@ -128,19 +140,6 @@ 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 \
@ -152,8 +151,21 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
"bus": RadioSelect(),
"team": RadioSelect(),
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
}
@ -161,7 +173,7 @@ class WEIMembership1AForm(WEIMembershipForm):
"""
Used to confirm registrations of first year members without choosing a bus now.
"""
deposit_check = None
caution_check = None
roles = None
def clean(self):

View File

@ -1,18 +0,0 @@
# 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

@ -1,40 +0,0 @@
# 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

@ -1,23 +0,0 @@
# Generated by Django 5.2.4 on 2025-07-19 12:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0015_remove_weiclub_caution_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='weiregistration',
name='fee',
field=models.PositiveIntegerField(blank=True, default=0, verbose_name='fee'),
),
migrations.AlterField(
model_name='weiclub',
name='fee_soge_credit',
field=models.PositiveIntegerField(default=2000, verbose_name='membership fee (soge credit)'),
),
]

View File

@ -33,16 +33,11 @@ class WEIClub(Club):
verbose_name=_("date end"),
)
deposit_amount = models.PositiveIntegerField(
verbose_name=_("deposit amount"),
caution_amount = models.PositiveIntegerField(
verbose_name=_("caution 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")
@ -202,19 +197,19 @@ class WEIRegistration(models.Model):
verbose_name=_("Credit from Société générale"),
)
deposit_check = models.BooleanField(
caution_check = models.BooleanField(
default=False,
verbose_name=_("Deposit check given")
verbose_name=_("Caution check given")
)
deposit_type = models.CharField(
caution_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("deposit type"),
verbose_name=_("caution type"),
)
birth_date = models.DateField(
@ -285,12 +280,6 @@ class WEIRegistration(models.Model):
"encoded in JSON"),
)
fee = models.PositiveIntegerField(
default=0,
verbose_name=_('fee'),
blank=True,
)
class Meta:
unique_together = ('user', 'wei',)
verbose_name = _("WEI User")
@ -315,25 +304,7 @@ class WEIRegistration(models.Model):
self.information_json = json.dumps(information, indent=2)
@property
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
@property
def validation_status(self):
"""
Define an order to have easier access to validatable registrations
"""
if self.fee + (self.wei.deposit_amount if self.deposit_type == 'note' else 0) > self.user.note.balance:
return 2
elif self.first_year:
return 1
else:
return 0
def calculate_fee(self):
def fee(self):
bde = Club.objects.get(pk=1)
kfet = Club.objects.get(pk=2)
@ -348,8 +319,7 @@ class WEIRegistration(models.Model):
date_start__gte=bde.membership_start,
).exists()
fee = self.wei.fee_soge_credit if self.soge_credit \
else self.wei.membership_fee_paid if self.user.profile.paid \
fee = 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 \
@ -360,9 +330,12 @@ class WEIRegistration(models.Model):
return fee
def save(self, *args, **kwargs):
self.fee = self.calculate_fee()
super().save(*args, **kwargs)
@property
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
class WEIMembership(Membership):

View File

@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table):
validate = tables.Column(
verbose_name=_("Validate"),
orderable=True,
accessor='validate_status',
orderable=False,
accessor=A('pk'),
attrs={
'th': {
'id': 'validate-membership-header'
@ -98,13 +98,12 @@ class WEIRegistrationTable(tables.Table):
if not hasperm:
return format_html("<span class='no-perm'></span>")
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true'
text = _('Validate')
status = record.validation_status
if status == 2:
if record.fee > record.user.note.balance and not record.soge_credit:
btn_class = 'btn-secondary'
tooltip = _("The user does not have enough money.")
elif status == 1:
elif record.first_year:
btn_class = 'btn-info'
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
else:
@ -122,10 +121,9 @@ class WEIRegistrationTable(tables.Table):
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('validate', 'user',)
model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check',
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'caution_check',
'edit', 'validate', 'delete',)
row_attrs = {
'class': 'table-row',
@ -165,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__deposit_check', )
'year', 'bus', 'team', 'registration__caution_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.deposit_amount > 0 %}
<dt class="col-xl-6">{% trans 'Deposit amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd>
{% 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 %}

View File

@ -67,6 +67,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
{% endif %}
{% if history_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% if pre_registrations.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
@ -85,19 +99,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %}
{% if history_list.data %}
<div class="card bg-white mt-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% endblock %}

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 'Deposit check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.deposit_check|yesno }}</dd>
<dt class="col-xl-6">{% trans 'caution check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.caution_check|yesno }}</dd>
{% with information=registration.information %}
<dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt>
@ -137,37 +137,41 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if registration.soge_credit %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
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 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 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>
{% 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 }}
{% 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>
{% 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>
{% 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 %}
{% if not registration.deposit_check and not registration.first_year and registration.caution_type == 'check' %}
{% if not registration.caution_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>
@ -206,27 +210,4 @@ 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,
deposit_check=True,
caution_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",
@ -121,13 +121,12 @@ 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",
deposit_amount=12000,
caution_amount=12000,
))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists())
@ -158,12 +157,11 @@ 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",
deposit_amount=12000,
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)
@ -322,7 +320,7 @@ class TestWEIRegistration(TestCase):
bus=[],
team=[],
roles=[],
deposit_type='check'
caution_type='check'
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid())
@ -340,7 +338,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()],
deposit_type='check'
caution_type='check'
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
@ -360,7 +358,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()],
deposit_type='check'
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))
@ -513,7 +511,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,
deposit_type='check'
caution_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
@ -568,7 +566,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,
deposit_type='check'
caution_type='check'
)
)
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
@ -592,7 +590,7 @@ class TestWEIRegistration(TestCase):
team=[],
roles=[],
information_json=self.registration.information_json,
deposit_type='check'
caution_type='check'
)
)
self.assertFalse(response.context["membership_form"].is_valid())
@ -642,7 +640,7 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
deposit_check=True,
caution_check=True,
))
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid())
@ -657,7 +655,7 @@ class TestWEIRegistration(TestCase):
last_name="admin",
first_name="admin",
bank="Société générale",
deposit_check=True,
caution_check=True,
))
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
@ -680,7 +678,11 @@ 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)
@ -813,7 +815,7 @@ class TestWeiAPI(TestAPI):
user_id=self.user.id,
wei_id=self.wei.id,
soge_credit=True,
deposit_check=True,
caution_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",

View File

@ -13,7 +13,7 @@ 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, Case, When, Value, IntegerField, F
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
@ -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
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
from note.tables import HistoryTable
from note_kfet.settings import BASE_DIR
from permission.backends import PermissionBackend
@ -35,7 +35,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, BusForm, BusTeamForm, WEIMembership1AForm, \
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable
@ -133,23 +133,6 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
membership=None,
wei=club
)
# Annotate the query to be able to sort registrations on validate status
pre_registrations = pre_registrations.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
return [club_transactions, club_member, pre_registrations, buses, ]
@ -277,23 +260,6 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
# Annotate the query to be able to sort registrations on validate status
qs = qs.annotate(
deposit=Case(
When(deposit_type='note', then=F('wei__deposit_amount')),
default=Value(0),
output_field=IntegerField()
)
).annotate(
total_fee=F('fee') + F('deposit')
).annotate(
validate_status=Case(
When(total_fee__gt=F('user__note__balance'), then=Value(2)),
When(first_year=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
pattern = self.request.GET.get("search", "")
@ -544,7 +510,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistrationForm
form_class = WEIRegistration1AForm
extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self):
@ -594,15 +560,13 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
# Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields:
del form.fields["first_year"]
if "deposit_check" in form.fields:
del form.fields["deposit_check"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
if "deposit_type" in form.fields:
del form.fields["deposit_type"]
if "caution_type" in form.fields:
del form.fields["caution_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
@ -640,7 +604,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistrationForm
form_class = WEIRegistration2AForm
extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self):
@ -694,9 +658,6 @@ 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.")
@ -704,16 +665,16 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
# Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields:
del form.fields["first_year"]
if "deposit_check" in form.fields:
del form.fields["deposit_check"]
if "caution_check" in form.fields:
del form.fields["caution_check"]
if "information_json" in form.fields:
del form.fields["information_json"]
# 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)
# 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
@ -742,7 +703,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
form.instance.information = information
# Sauvegarder le type de caution
form.instance.deposit_type = form.cleaned_data["deposit_type"]
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save()
if 'treasury' in settings.INSTALLED_APPS:
@ -773,11 +734,14 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
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,
@ -809,22 +773,22 @@ 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 "information_json" in form.fields:
if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object):
del form.fields["information_json"]
# 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"]
# 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 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")
# 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):
registration = self.get_object()
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
membership_form = WEIMembershipForm(data if data else None, instance=instance)
del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"]
@ -880,8 +844,8 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "deposit_type" in form.cleaned_data:
form.instance.deposit_type = form.cleaned_data["deposit_type"]
if "caution_type" in form.cleaned_data:
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save()
return super().form_valid(form)
@ -892,6 +856,9 @@ 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"):
return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -984,15 +951,15 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.deposit_type == 'note':
total_needed += registration.wei.deposit_amount
if registration.caution_type == 'note':
total_needed += registration.wei.caution_amount
context["total_needed"] = total_needed
form = context["form"]
if registration.soge_credit:
form.fields["credit_amount"].initial = fee
form.fields["credit_amount"].initial = registration.fee
else:
form.fields["credit_amount"].initial = max(0, fee - registration.user.note.balance)
form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance)
return context
@ -1002,38 +969,40 @@ 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 deposit_check uniquement pour les non-première année et le rendre obligatoire
# Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire
if not registration.first_year:
if registration.deposit_type == 'check':
form.fields["deposit_check"] = forms.BooleanField(
if registration.caution_type == 'check':
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=registration.deposit_check,
label=_("Deposit check given"),
initial=registration.caution_check,
label=_("Caution check given"),
help_text=_("Please make sure the check is given before validating the registration")
)
else:
form.fields["deposit_check"] = forms.BooleanField(
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.deposit_amount / 100
'amount': registration.wei.caution_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)
@ -1066,8 +1035,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
club = registration.wei
user = registration.user
if "deposit_check" in form.data:
registration.deposit_check = form.data["deposit_check"] == "on"
if "caution_check" in form.data:
registration.caution_check = form.data["caution_check"] == "on"
registration.save()
membership = form.instance
membership.user = user
@ -1078,8 +1047,6 @@ 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 = registration.wei.fee_soge_credit
kfet = club.parent_club
bde = kfet.parent_club
@ -1106,16 +1073,16 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
if credit_type is None:
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.deposit_type == 'note':
total_needed += club.deposit_amount
if registration.caution_type == 'note':
total_needed += club.caution_amount
# Vérifier que l'utilisateur a assez d'argent pour tout payer
if user.note.balance + credit_amount < total_needed:
if not registration.soge_credit and 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") % {
@ -1163,14 +1130,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.deposit_type == 'note':
if registration.caution_type == 'note':
from note.models import Transaction
Transaction.objects.create(
source=user.note,
destination=club.note,
quantity=1,
amount=club.deposit_amount,
reason=_("Deposit %(name)s") % {'name': club.name},
amount=club.caution_amount,
reason=_("Caution %(name)s") % {'name': club.name},
valid=True,
)

View File

@ -136,7 +136,7 @@ de diffusion utiles.
Faîtes attention, donc où la sortie est stockée.
Il prend 4 options :
Il prend 2 options :
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es
@ -149,10 +149,7 @@ Il prend 4 options :
pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe
laquelle des ``n+1`` dernières années.
* ``--email``, qui prend en argument une chaine de caractère contenant une adresse email.
Si aucun email n'est renseigné, le script sort sur la sortie standard la liste des adresses mails à inscrire.
Dans le cas contraire, la liste est envoyée à l'adresse passée en argument.
Le script sort sur la sortie standard la liste des adresses mails à inscrire.
Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est
malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives.

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-15 18:18+0200\n"
"POT-Creation-Date: 2025-06-20 14:02+0200\n"
"PO-Revision-Date: 2022-04-11 23:12+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: \n"
@ -65,7 +65,7 @@ msgstr "Usted no puede invitar más de 3 persona a esta actividad."
#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:299
#: apps/permission/models.py:329
#: apps/registration/templates/registration/future_profile_detail.html:16
#: apps/wei/models.py:77 apps/wei/models.py:150 apps/wei/tables.py:282
#: apps/wei/models.py:72 apps/wei/models.py:145 apps/wei/tables.py:282
#: apps/wei/templates/wei/base.html:26
#: apps/wei/templates/wei/weimembership_form.html:14 apps/wrapped/models.py:16
msgid "name"
@ -100,7 +100,7 @@ msgstr "tipos de actividad"
#: apps/activity/models.py:68
#: apps/activity/templates/activity/includes/activity_info.html:19
#: apps/note/models/transactions.py:82 apps/permission/models.py:109
#: apps/permission/models.py:188 apps/wei/models.py:97 apps/wei/models.py:161
#: apps/permission/models.py:188 apps/wei/models.py:92 apps/wei/models.py:156
msgid "description"
msgstr "descripción"
@ -121,7 +121,7 @@ msgstr "tipo"
#: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325
#: apps/note/models/notes.py:148 apps/treasury/models.py:294
#: apps/wei/models.py:190 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/models.py:185 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
msgid "user"
msgstr "usuario"
@ -1297,7 +1297,7 @@ msgid "add to registration form"
msgstr "Validar la afiliación"
#: apps/member/models.py:268 apps/member/models.py:331
#: apps/note/models/notes.py:176 apps/wei/models.py:91
#: apps/note/models/notes.py:176 apps/wei/models.py:86
msgid "club"
msgstr "club"
@ -2017,8 +2017,8 @@ msgstr ""
"pago y un usuario o un club"
#: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360
#: apps/note/models/transactions.py:363 apps/wei/views.py:1103
#: apps/wei/views.py:1107
#: apps/note/models/transactions.py:363 apps/wei/views.py:1097
#: apps/wei/views.py:1101
msgid "This field is required."
msgstr "Este campo es obligatorio."
@ -2515,7 +2515,7 @@ msgstr "El usuario declara que ya abrió una cuenta a la Société Générale."
#: apps/registration/templates/registration/future_profile_detail.html:73
#: apps/wei/templates/wei/weimembership_form.html:127
#: apps/wei/templates/wei/weimembership_form.html:192
#: apps/wei/templates/wei/weimembership_form.html:196
msgid "Validate registration"
msgstr "Validar la afiliación"
@ -3043,8 +3043,8 @@ msgstr "Lista de los créditos de la Société Générale"
msgid "Manage credits from the Société générale"
msgstr "Gestionar los créditos de la Société Générale"
#: apps/wei/apps.py:10 apps/wei/models.py:47 apps/wei/models.py:48
#: apps/wei/models.py:72 apps/wei/models.py:197
#: apps/wei/apps.py:10 apps/wei/models.py:42 apps/wei/models.py:43
#: apps/wei/models.py:67 apps/wei/models.py:192
#: note_kfet/templates/base.html:108
msgid "WEI"
msgstr "WEI"
@ -3054,8 +3054,8 @@ msgid "The selected user is not validated. Please validate its account first"
msgstr ""
"El usuario seleccionado no ha sido validado. Validar esta cuenta primero"
#: apps/wei/forms/registration.py:84 apps/wei/models.py:145
#: apps/wei/models.py:354
#: apps/wei/forms/registration.py:84 apps/wei/models.py:140
#: apps/wei/models.py:348
msgid "bus"
msgstr "bus"
@ -3081,7 +3081,7 @@ msgstr ""
"electrón libre)"
#: apps/wei/forms/registration.py:100 apps/wei/forms/registration.py:110
#: apps/wei/models.py:179
#: apps/wei/models.py:174
msgid "WEI Roles"
msgstr "Papeles en el WEI"
@ -3089,19 +3089,14 @@ msgstr "Papeles en el WEI"
msgid "Select the roles that you are interested in."
msgstr "Elegir los papeles que le interesa."
#: apps/wei/forms/registration.py:160
#: apps/wei/forms/registration.py:147
msgid "This team doesn't belong to the given bus."
msgstr "Este equipo no pertenece al bus dado."
#: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:38
#: apps/wei/forms/surveys/wei2025.py:36
msgid "Choose a word:"
msgstr "Elegir una palabra :"
#: apps/wei/forms/surveys/wei2025.py:123
msgid "Rate between 0 and 5."
msgstr ""
#: apps/wei/models.py:25 apps/wei/templates/wei/base.html:36
msgid "year"
msgstr "año"
@ -3118,147 +3113,138 @@ msgstr "fecha de fin"
#: apps/wei/models.py:37
#, fuzzy
#| msgid "Credit amount"
msgid "deposit amount"
msgstr "Valor del crédito"
#| msgid "total amount"
msgid "caution amount"
msgstr "monto total"
#: apps/wei/models.py:42
#, fuzzy
#| msgid "No credit"
msgid "membership fee (soge credit)"
msgstr "No crédito"
#: apps/wei/models.py:81 apps/wei/tables.py:305
#: apps/wei/models.py:76 apps/wei/tables.py:305
msgid "seat count in the bus"
msgstr "cantidad de asientos en el bus"
#: apps/wei/models.py:102
#: apps/wei/models.py:97
msgid "survey information"
msgstr "informaciones sobre el cuestionario"
#: apps/wei/models.py:103
#: apps/wei/models.py:98
msgid "Information about the survey for new members, encoded in JSON"
msgstr ""
"Informaciones sobre el cuestionario para los nuevos miembros, registrado en "
"JSON"
#: apps/wei/models.py:107
#: apps/wei/models.py:102
msgid "Bus"
msgstr "Bus"
#: apps/wei/models.py:108 apps/wei/templates/wei/weiclub_detail.html:51
#: apps/wei/models.py:103 apps/wei/templates/wei/weiclub_detail.html:51
msgid "Buses"
msgstr "Bus"
#: apps/wei/models.py:154
#: apps/wei/models.py:149
msgid "color"
msgstr "color"
#: apps/wei/models.py:155
#: apps/wei/models.py:150
msgid "The color of the T-Shirt, stored with its number equivalent"
msgstr "El color de la camiseta, registrado con su número equivalente"
#: apps/wei/models.py:166
#: apps/wei/models.py:161
msgid "Bus team"
msgstr "Equipo de bus"
#: apps/wei/models.py:167
#: apps/wei/models.py:162
msgid "Bus teams"
msgstr "Equipos de bus"
#: apps/wei/models.py:178
#: apps/wei/models.py:173
msgid "WEI Role"
msgstr "Papeles en el WEI"
#: apps/wei/models.py:202
#: apps/wei/models.py:197
msgid "Credit from Société générale"
msgstr "Crédito de la Société Générale"
#: apps/wei/models.py:207 apps/wei/templates/wei/weimembership_form.html:98
#: apps/wei/views.py:997
#, fuzzy
#| msgid "Caution check given"
msgid "Deposit check given"
#: apps/wei/models.py:202 apps/wei/views.py:984
msgid "Caution check given"
msgstr "Cheque de garantía dado"
#: apps/wei/models.py:213
#: apps/wei/models.py:208
msgid "Check"
msgstr ""
#: apps/wei/models.py:214
#: apps/wei/models.py:209
#, fuzzy
#| msgid "transactions"
msgid "Note transaction"
msgstr "Transacción"
#: apps/wei/models.py:217
#: apps/wei/models.py:212
#, fuzzy
#| msgid "Credit type"
msgid "deposit type"
msgstr "Tipo de crédito"
#| msgid "created at"
msgid "caution type"
msgstr "tipo de fianza"
#: apps/wei/models.py:221 apps/wei/templates/wei/weimembership_form.html:64
#: apps/wei/models.py:216 apps/wei/templates/wei/weimembership_form.html:64
msgid "birth date"
msgstr "fecha de nacimiento"
#: apps/wei/models.py:227 apps/wei/models.py:237
#: apps/wei/models.py:222 apps/wei/models.py:232
msgid "Male"
msgstr "Hombre"
#: apps/wei/models.py:228 apps/wei/models.py:238
#: apps/wei/models.py:223 apps/wei/models.py:233
msgid "Female"
msgstr "Mujer"
#: apps/wei/models.py:229
#: apps/wei/models.py:224
msgid "Non binary"
msgstr "No binari@"
#: apps/wei/models.py:231 apps/wei/templates/wei/attribute_bus_1A.html:22
#: apps/wei/models.py:226 apps/wei/templates/wei/attribute_bus_1A.html:22
#: apps/wei/templates/wei/weimembership_form.html:55
msgid "gender"
msgstr "género"
#: apps/wei/models.py:239
#: apps/wei/models.py:234
msgid "Unisex"
msgstr "Unisex"
#: apps/wei/models.py:242 apps/wei/templates/wei/weimembership_form.html:58
#: apps/wei/models.py:237 apps/wei/templates/wei/weimembership_form.html:58
msgid "clothing cut"
msgstr "forma de ropa"
#: apps/wei/models.py:255 apps/wei/templates/wei/weimembership_form.html:61
#: apps/wei/models.py:250 apps/wei/templates/wei/weimembership_form.html:61
msgid "clothing size"
msgstr "medida de ropa"
#: apps/wei/models.py:261
#: apps/wei/models.py:256
msgid "health issues"
msgstr "problemas de salud"
#: apps/wei/models.py:266 apps/wei/templates/wei/weimembership_form.html:70
#: apps/wei/models.py:261 apps/wei/templates/wei/weimembership_form.html:70
msgid "emergency contact name"
msgstr "nombre del contacto de emergencia"
#: apps/wei/models.py:267
#: apps/wei/models.py:262
msgid "The emergency contact must not be a WEI participant"
msgstr "El contacto de emergencia no debe ser un participante de WEI"
#: apps/wei/models.py:272 apps/wei/templates/wei/weimembership_form.html:73
#: apps/wei/models.py:267 apps/wei/templates/wei/weimembership_form.html:73
msgid "emergency contact phone"
msgstr "teléfono del contacto de emergencia"
#: apps/wei/models.py:277 apps/wei/templates/wei/weimembership_form.html:52
#: apps/wei/models.py:272 apps/wei/templates/wei/weimembership_form.html:52
msgid "first year"
msgstr "primer año"
#: apps/wei/models.py:278
#: apps/wei/models.py:273
msgid "Tells if the user is new in the school."
msgstr "Indica si el usuario es nuevo en la escuela."
#: apps/wei/models.py:283
#: apps/wei/models.py:278
msgid "registration information"
msgstr "informaciones sobre la afiliación"
#: apps/wei/models.py:284
#: apps/wei/models.py:279
msgid ""
"Information about the registration (buses for old members, survey for the "
"new members), encoded in JSON"
@ -3266,27 +3252,27 @@ msgstr ""
"Informaciones sobre la afiliacion (bus para miembros ancianos, cuestionario "
"para los nuevos miembros), registrado en JSON"
#: apps/wei/models.py:290
#: apps/wei/models.py:285
msgid "WEI User"
msgstr "Participante WEI"
#: apps/wei/models.py:291
#: apps/wei/models.py:286
msgid "WEI Users"
msgstr "Participantes WEI"
#: apps/wei/models.py:364
#: apps/wei/models.py:358
msgid "team"
msgstr "equipo"
#: apps/wei/models.py:374
#: apps/wei/models.py:368
msgid "WEI registration"
msgstr "Apuntación al WEI"
#: apps/wei/models.py:378
#: apps/wei/models.py:372
msgid "WEI membership"
msgstr "Afiliación al WEI"
#: apps/wei/models.py:379
#: apps/wei/models.py:373
msgid "WEI memberships"
msgstr "Afiliaciones al WEI"
@ -3314,7 +3300,7 @@ msgstr "Año"
msgid "preferred bus"
msgstr "bus preferido"
#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:38
#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:36
#: apps/wei/templates/wei/busteam_detail.html:52
msgid "Teams"
msgstr "Equipos"
@ -3386,9 +3372,9 @@ msgstr "Pago de entrada del WEI (estudiantes no pagados)"
#: apps/wei/templates/wei/base.html:53
#, fuzzy
#| msgid "Credit amount"
msgid "Deposit amount"
msgstr "Valor del crédito"
#| msgid "total amount"
msgid "Caution amount"
msgstr "monto total"
#: apps/wei/templates/wei/base.html:74
msgid "WEI list"
@ -3398,7 +3384,7 @@ msgstr "Lista de los WEI"
msgid "Register 1A"
msgstr "Apuntar un 1A"
#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:646
#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:644
msgid "Register 2A+"
msgstr "Apuntar un 2A+"
@ -3415,21 +3401,15 @@ msgid "View club"
msgstr "Ver club"
#: apps/wei/templates/wei/bus_detail.html:26
#, fuzzy
#| msgid "survey information"
msgid "Edit information"
msgstr "informaciones sobre el cuestionario"
#: apps/wei/templates/wei/bus_detail.html:28
#: apps/wei/templates/wei/busteam_detail.html:24
msgid "Add team"
msgstr "Añadir un equipo"
#: apps/wei/templates/wei/bus_detail.html:51
#: apps/wei/templates/wei/bus_detail.html:49
msgid "Members"
msgstr "Miembros"
#: apps/wei/templates/wei/bus_detail.html:60
#: apps/wei/templates/wei/bus_detail.html:58
#: apps/wei/templates/wei/busteam_detail.html:62
#: apps/wei/templates/wei/weimembership_list.html:31
msgid "View as PDF"
@ -3437,8 +3417,8 @@ msgstr "Descargar un PDF"
#: apps/wei/templates/wei/survey.html:11
#: apps/wei/templates/wei/survey_closed.html:11
#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1165
#: apps/wei/views.py:1220 apps/wei/views.py:1267
#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1159
#: apps/wei/views.py:1214 apps/wei/views.py:1261
msgid "Survey WEI"
msgstr "Cuestionario WEI"
@ -3514,6 +3494,10 @@ msgstr "Informaciones crudas del cuestionario"
msgid "The algorithm didn't run."
msgstr "El algoritmo no funcionó."
#: apps/wei/templates/wei/weimembership_form.html:98
msgid "caution check given"
msgstr "cheque de garantía dado"
#: apps/wei/templates/wei/weimembership_form.html:105
msgid "preferred team"
msgstr "equipo preferido"
@ -3548,18 +3532,11 @@ msgid "with the following roles:"
msgstr "con los papeles :"
#: apps/wei/templates/wei/weimembership_form.html:139
#, fuzzy
#| msgid ""
#| "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 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."
msgid ""
"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."
"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 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."
msgstr ""
"El WEI será pagado por la Société Générale. La afiliación será creada aunque "
"el banco no pago el BDE ya. La transacción de afiliación será creada pero "
@ -3581,26 +3558,27 @@ msgstr "Pagos de afiliación (estudiantes pagados)"
msgid "Deposit (by Note transaction): %(amount)s"
msgstr "Fianza (transacción) : %(amount)s"
#: apps/wei/templates/wei/weimembership_form.html:157
#, python-format
msgid "Deposit (by check): %(amount)s"
msgstr "Fianza (cheque) : %(amount)s"
#: apps/wei/templates/wei/weimembership_form.html:161
#: apps/wei/templates/wei/weimembership_form.html:156
#: apps/wei/templates/wei/weimembership_form.html:163
#, python-format
msgid "Total needed: %(total)s"
msgstr "Total necesario : %(total)s"
#: apps/wei/templates/wei/weimembership_form.html:165
#: apps/wei/templates/wei/weimembership_form.html:160
#, python-format
msgid "Deposit (by check): %(amount)s"
msgstr "Fianza (cheque) : %(amount)s"
#: apps/wei/templates/wei/weimembership_form.html:168
#, python-format
msgid "Current balance: %(balance)s"
msgstr "Saldo actual : %(balance)s"
#: apps/wei/templates/wei/weimembership_form.html:172
#: apps/wei/templates/wei/weimembership_form.html:176
msgid "The user didn't give her/his caution check."
msgstr "El usuario no dio su cheque de garantía."
#: apps/wei/templates/wei/weimembership_form.html:180
#: apps/wei/templates/wei/weimembership_form.html:184
msgid ""
"This user is not a member of the Kfet club for the coming year. The "
"membership will be processed automatically, the WEI registration includes "
@ -3690,109 +3668,110 @@ msgstr "Gestionar el equipo"
msgid "Register first year student to the WEI"
msgstr "Registrar un 1A al WEI"
#: apps/wei/views.py:571 apps/wei/views.py:664
#, fuzzy
#| msgid "Check this case if the Société Générale paid the inscription."
msgid "Check if you will open a Société Générale account"
msgstr "Marcar esta casilla si Société Générale pagó la registración."
#: apps/wei/views.py:582 apps/wei/views.py:694
#: apps/wei/views.py:580 apps/wei/views.py:689
msgid "This user is already registered to this WEI."
msgstr "Este usuario ya afilió a este WEI."
#: apps/wei/views.py:587
#: apps/wei/views.py:585
msgid ""
"This user can't be in her/his first year since he/she has already "
"participated to a WEI."
msgstr "Este usuario no puede ser un 1A porque ya participó en un WEI."
#: apps/wei/views.py:610
#: apps/wei/views.py:608
msgid "Register old student to the WEI"
msgstr "Registrar un 2A+ al WEI"
#: apps/wei/views.py:668 apps/wei/views.py:773
#: apps/wei/views.py:663 apps/wei/views.py:768
msgid "You already opened an account in the Société générale."
msgstr "Usted ya abrió una cuenta a la Société Générale."
#: apps/wei/views.py:681 apps/wei/views.py:790
#: apps/wei/views.py:676 apps/wei/views.py:785
msgid "Choose how you want to pay the deposit"
msgstr ""
#: apps/wei/views.py:733
#: apps/wei/views.py:728
msgid "Update WEI Registration"
msgstr "Modificar la inscripción WEI"
#: apps/wei/views.py:816
#: apps/wei/views.py:810
#, fuzzy
#| msgid "The BDE membership is included in the WEI registration."
msgid "No membership found for this registration"
msgstr "La afiliación al BDE esta incluida en la afiliación WEI."
#: apps/wei/views.py:825
#: apps/wei/views.py:819
#| msgid ""
#| "You don't have the permission to add an instance of model {app_label}."
#| "{model_name}."
msgid "You don't have the permission to update memberships"
msgstr ""
"Usted no tiene permiso a añadir una instancia al modelo {app_label}."
"{model_name}."
#: apps/wei/views.py:831
#: apps/wei/views.py:825
#, python-format
#| msgid ""
#| "You don't have the permission to delete this instance of model "
#| "{app_label}.{model_name}."
msgid "You don't have the permission to update the field %(field)s"
msgstr "Usted no tiene permiso a modificar el campo %(field)s"
#: apps/wei/views.py:876
#: apps/wei/views.py:870
msgid "Delete WEI registration"
msgstr "Suprimir la inscripción WEI"
#: apps/wei/views.py:887
#: apps/wei/views.py:881
msgid "You don't have the right to delete this WEI registration."
msgstr "Usted no tiene derecho a suprimir esta inscripción WEI."
#: apps/wei/views.py:905
#: apps/wei/views.py:899
msgid "Validate WEI registration"
msgstr "Validar la inscripción WEI"
#: apps/wei/views.py:998
#: apps/wei/views.py:985
msgid "Please make sure the check is given before validating the registration"
msgstr ""
"Por favor asegúrese de que el cheque se entrega antes de validar el registro"
#: apps/wei/views.py:1004
#: apps/wei/views.py:991
#| msgid "credit transaction"
msgid "Create deposit transaction"
msgstr "Crear transacción de crédito"
#: apps/wei/views.py:1005
#: apps/wei/views.py:992
#, python-format
msgid ""
"A transaction of %(amount).2f€ will be created from the user's Note account"
msgstr ""
#: apps/wei/views.py:1093
#, fuzzy, python-format
#: apps/wei/views.py:1087
#, python-format
#| msgid ""
#| "This user doesn't have enough money. Current balance: %(balance)d€, "
#| "credit: %(credit)d€, needed: %(needed)d€"
#| "This user don't have enough money to join this club, and can't have a "
#| "negative balance."
msgid ""
"This user doesn't have enough money to join this club and pay the deposit. "
"This user doesn't have enough money. "
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d€"
msgstr ""
"Este usuario no tiene suficiente dinero. Saldo actual : %(balance)d€, "
"crédito: %(credit)d€, requerido: %(needed)d€"
"Este usuario no tiene suficiente dinero. "
"Saldo actual : %(balance)d€, crédito: %(credit)d€, requerido: %(needed)d€"
#: apps/wei/views.py:1146
#, fuzzy, python-format
#| msgid "Caution %(name)s"
msgid "Deposit %(name)s"
#: apps/wei/views.py:1140
#, python-format
#| msgid "created at"
msgid "Caution %(name)s"
msgstr "Fianza %(name)s"
#: apps/wei/views.py:1360
#: apps/wei/views.py:1354
msgid "Attribute buses to first year members"
msgstr "Repartir los primer años en los buses"
#: apps/wei/views.py:1386
#: apps/wei/views.py:1379
msgid "Attribute bus"
msgstr "Repartir en un bus"
#: apps/wei/views.py:1426
#: apps/wei/views.py:1419
msgid ""
"No first year student without a bus found. Either all of them have a bus, or "
"none has filled the survey yet."
@ -4358,24 +4337,6 @@ msgstr ""
"pagar su afiliación. Tambien tiene que validar su correo electronico con el "
"enlace que recibió."
#, fuzzy
#~| msgid "total amount"
#~ msgid "caution amount"
#~ msgstr "monto total"
#, fuzzy
#~| msgid "created at"
#~ msgid "caution type"
#~ msgstr "tipo de fianza"
#, fuzzy
#~| msgid "total amount"
#~ msgid "Caution amount"
#~ msgstr "monto total"
#~ msgid "caution check given"
#~ msgstr "cheque de garantía dado"
#, fuzzy
#~| msgid "Invitation"
#~ msgid "Syndication"

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,5 @@ MAILTO=notekfet2020@lists.crans.org
# Vider les tokens Oauth2
00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0
# Envoyer la liste des abonnés à la NL BDA
00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art -e "bda.ensparissaclay@gmail.com"
# Envoyer la liste de la bouffe au club et aux GCKs
00 8 * * 1 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_for_food --report --club
00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art -e "bda.ensparissaclay@gmail.com"

View File

@ -56,8 +56,3 @@ if "cas_server" in settings.INSTALLED_APPS:
from cas_server.models import *
admin_site.register(ServicePattern, ServicePatternAdmin)
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
if "constance" in settings.INSTALLED_APPS:
from constance.admin import *
from constance.models import *
admin_site.register([Config], ConstanceAdmin)

View File

@ -39,9 +39,7 @@ SECURE_HSTS_PRELOAD = True
INSTALLED_APPS = [
# External apps
'bootstrap_datepicker_plus',
'cas_server',
'colorfield',
'constance',
'crispy_bootstrap4',
'crispy_forms',
# 'django_htcpcp_tea',
@ -72,6 +70,7 @@ INSTALLED_APPS = [
# Note apps
'api',
'activity',
'family',
'food',
'logs',
'member',
@ -113,7 +112,6 @@ TEMPLATES = [
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'constance.context_processors.config',
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
@ -310,30 +308,6 @@ PHONENUMBER_DEFAULT_REGION = 'FR'
# We add custom information to CAS, in order to give a normalized name to other services
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
CAS_LOGIN_TEMPLATE = 'cas/login.html'
CAS_LOGOUT_TEMPLATE = 'cas/logout.html'
CAS_WARN_TEMPLATE = 'cas/warn.html'
CAS_LOGGED_TEMPLATE = 'cas/logged.html'
# Default field for primary key
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Constance settings
CONSTANCE_ADDITIONAL_FIELDS = {
'banner_type': ['django.forms.fields.ChoiceField', {
'widget': 'django.forms.Select',
'choices': (('info', 'Info'), ('success', 'Success'), ('warning', 'Warning'), ('danger', 'Danger'))
}],
}
CONSTANCE_CONFIG = {
'BANNER_MESSAGE': ('', 'Some message', str),
'BANNER_TYPE': ('info', 'Banner type', 'banner_type'),
'MAINTENANCE': (False, 'check for mainteance mode', bool),
'MAINTENANCE_MESSAGE': ('', 'Some maintenance message', str),
}
CONSTANCE_CONFIG_FIELDSETS = {
'Maintenance': ('MAINTENANCE_MESSAGE', 'MAINTENANCE'),
'Banner': ('BANNER_MESSAGE', 'BANNER_TYPE'),
}
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_SUPERUSER_ONLY = True

View File

@ -5,7 +5,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
<html lang="{{ LANGUAGE_CODE|default:"en" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %} class="position-relative h-100">
{% if not config.MAINTENANCE %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -79,6 +78,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %}</a>
</li>
{% endif %}
{% if user.is_authenticated %}
<li class="nav-item">
{% url 'family:family_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Families' %}</a>
</li>
{% endif %}
{% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item">
{% url 'member:user_list' as url %}
@ -139,12 +145,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
<i class="fa fa-user"></i> {% trans "My account" %}
</a>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button class="dropdown-item" type=submit">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</button>
</form>
<a class="dropdown-item" href="{% url 'logout' %}">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
</div>
</li>
{% else %}
@ -192,11 +195,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}
</div>
{% endif %}
{% if config.BANNER_MESSAGE and user.is_authenticated %}
<div class="alert alert-{{ config.BANNER_TYPE }}">
{{ config.BANNER_MESSAGE }}
</div>
{% endif %}
{# TODO Add banners #}
</div>
{% block content %}
<p>Default content...</p>
@ -218,10 +217,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
class="text-muted">{% trans "Charte Info (FR)" %}</a> &mdash;
<a href="https://note.crans.org/doc/faq/"
class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash;
<a href="https://bde.ens-cachan.fr"
class="text-muted">{% trans "Managed by BDE" %}</a> &mdash;
<a href="https://crans.org"
class="text-muted">{% trans "Hosted by Cr@ns" %}</a> &mdash;
</span>
{% csrf_token %}
<select title="language" name="language"
@ -258,15 +253,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extrajavascript %}{% endblock %}
</body>
{% endif %}
{% if config.MAINTENANCE %}
<body>
<div style="text-align:center">
<br />
{% trans "The note is not available for now" %}<br /><br />
{{ config.MAINTENANCE_MESSAGE }}<br /><br />
{% trans "Thank you for your understanding -- The Respos Info of BDE" %}
</div>
</body>
{% endif %}
</html>

View File

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
<div class="card bg-light mx-auto" style="max-width:30rem;">
<div class="card-body">
<form class="form-signin" method="get" action="logout">
<div class="checkbox">
<label>
<input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %}
</label>
</div>
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
<div class="checkbox">
<label>
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
</label>
</div>
{% endif %}
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,42 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block ante_messages %}
{% if auto_submit %}<noscript>{% endif %}
<div class="card-header text-center">
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
</div>
{% if auto_submit %}</noscript>{% endif %}
{% endblock %}
{% block content %}
<div class="card bg-light mx-auto" style="max-width: 30rem;">
<div class="card-body">
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
{% csrf_token %}
{% include "cas_server/bs4/form.html" %}
{% if auto_submit %}<noscript>{% endif %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
{% if auto_submit %}</noscript>{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}
{% block javascript_inline %}
jQuery(function( $ ){
$("#id_warn").click(function(e){
if($("#id_warn").is(':checked')){
createCookie("warn", "on", 10 * 365);
} else {
eraseCookie("warn");
}
});
});
{% if auto_submit %}document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static %}
{% block content %}
<div class="alert alert-success" role="alert">{{ logout_msg }}</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS-Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n static %}
{% block content %}
<div class="card bg-light mx-auto" style="max-width: 30rem;">
<div class="card-body">
<form class="form-signin" method="post">
{% csrf_token %}
{% include "cas_server/bs4/form.html" %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -21,8 +21,9 @@ urlpatterns = [
path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
path('food/',include('food.urls')),
path('wrapped/',include('wrapped.urls')),
path('food/', include('food.urls')),
path('wrapped/', include('wrapped.urls')),
path('family/', include('family.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),

View File

@ -1,21 +1,20 @@
beautifulsoup4~=4.13.4
crispy-bootstrap4~=2025.6
Django~=5.2.4
beautifulsoup4~=4.12.3
crispy-bootstrap4~=2023.1
Django~=4.2.9
django-bootstrap-datepicker-plus~=5.0.5
django-cas-server~=3.1.0
django-colorfield~=0.14.0
django-constance~=4.3.2
django-crispy-forms~=2.4.0
django-extensions>=4.1.0
django-filter~=25.1
#django-cas-server~=2.0.0
django-colorfield~=0.11.0
django-crispy-forms~=2.1.0
django-extensions>=3.2.3
django-filter~=23.5
#django-htcpcp-tea~=0.8.1
django-mailer~=2.3.2
django-oauth-toolkit~=3.0.1
django-phonenumber-field~=8.1.0
django-mailer~=2.3.1
django-oauth-toolkit~=2.3.0
django-phonenumber-field~=7.3.0
django-polymorphic~=3.1.0
djangorestframework~=3.16.0
djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10
django-tables2~=2.7.5
django-tables2~=2.7.0
python-memcached~=1.62
phonenumbers~=9.0.8
Pillow>=11.3.0
phonenumbers~=8.13.28
Pillow>=10.2.0

View File

@ -1,13 +1,13 @@
[tox]
envlist =
# Ubuntu 22.04 Python
py310-django52
py310-django42
# Debian Bookworm Python
py311-django52
py311-django42
# Ubuntu 24.04 Python
py312-django52
py312-django42
linters
skipsdist = True
@ -32,7 +32,8 @@ deps =
pep8-naming
pyflakes
commands =
flake8 apps --extend-exclude apps/scripts
flake8 apps --extend-exclude apps/scripts,apps/wrapped/management/commands
flake8 apps/wrapped/management/commands --extend-ignore=C901
[flake8]
ignore = W503, I100, I101, B019