Merge pull request 'Application des changement statuts et RI' (#846) from status-change-2024-nov-13 into master

Reviewed-on: #846
This commit is contained in:
catvayor 2025-04-25 22:19:27 +02:00
commit 5c4b752363
31 changed files with 748 additions and 165 deletions

View file

@ -130,15 +130,33 @@ class UserProfileAdmin(UserAdmin):
is_buro.short_description = "Membre du Buro"
is_buro.boolean = True
def is_chef(self, obj):
try:
return obj.profile.is_chef
except CofProfile.DoesNotExist:
return False
is_chef.short_description = "Chef K-Fêt"
is_chef.boolean = True
def is_cof(self, obj):
try:
return obj.profile.is_cof
except CofProfile.DoesNotExist:
return False
is_cof.short_description = "Membre du COF"
is_cof.short_description = "Membre COF"
is_cof.boolean = True
def is_kfet(self, obj):
try:
return obj.profile.is_kfet
except CofProfile.DoesNotExist:
return False
is_kfet.short_description = "Membre K-Fêt"
is_kfet.boolean = True
list_display = UserAdmin.list_display + (
"profile_phone",
"profile_occupation",
@ -146,7 +164,9 @@ class UserProfileAdmin(UserAdmin):
"profile_mailing_bda",
"profile_mailing_bda_revente",
"is_cof",
"is_kfet",
"is_buro",
"is_chef",
)
list_display_links = ("username", "email", "first_name", "last_name")
list_filter = UserAdmin.list_filter + (

View file

@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
def cof_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
"""Décorateur qui vérifie que l'utilisateur est connecté et membre COF.
- Si l'utilisteur n'est pas connecté, il est redirigé vers la page de
connexion
@ -33,6 +33,31 @@ def cof_required(view_func):
return login_required(_wrapped_view)
def kfet_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre K-Fêt.
- Si l'utilisteur n'est pas connecté, il est redirigé vers la page de
connexion
- Si l'utilisateur est connecté mais pas membre K-Fêt, il obtient une
page d'erreur lui demandant de s'inscrire à la K-Fêt
"""
def is_kfet(user):
try:
return user.profile.is_cof or user.profile.is_kfet
except AttributeError:
return False
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if is_kfet(request.user):
return view_func(request, *args, **kwargs)
return render(request, "kfet-denied.html", status=403)
return login_required(_wrapped_view)
def buro_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du burô.
@ -58,6 +83,33 @@ def buro_required(view_func):
return login_required(_wrapped_view)
def chef_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du burô ou chef K-fêt.
- Si l'utilisateur n'est pas connecté, il est redirigé vers la page de
connexion
- Si l'utilisateur est connecté mais pas membre du burô ou chef, il obtient une
page d'erreur 403 Forbidden
"""
def is_chef(user):
try:
return user.profile.is_chef or user.profile.is_buro
except AttributeError:
return False
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if is_chef(request.user):
return view_func(request, *args, **kwargs)
return render(
request, "buro-denied.html", status=403
) # TODO: reservé au burô ou au chef
return login_required(_wrapped_view)
class CofRequiredMixin(PermissionRequiredMixin):
def has_permission(self):
if not self.request.user.is_authenticated:
@ -79,3 +131,18 @@ class BuroRequiredMixin(PermissionRequiredMixin):
"L'utilisateur %s n'a pas de profil !", self.request.user.username
)
return False
class ChefRequiredMixin(PermissionRequiredMixin):
def has_permission(self):
if not self.request.user.is_authenticated:
return False
try:
return (
self.request.user.profile.is_chef or self.request.user.profile.is_buro
)
except AttributeError:
logger.error(
"L'utilisateur %s n'a pas de profil !", self.request.user.username
)
return False

View file

@ -284,6 +284,7 @@ class RegistrationProfileForm(forms.ModelForm):
"occupation",
"departement",
"is_cof",
"is_kfet",
"type_cotiz",
"mailing_cof",
"mailing_bda",
@ -293,6 +294,19 @@ class RegistrationProfileForm(forms.ModelForm):
]
class RegistrationKFProfileForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = [
"login_clipper",
"phone",
"occupation",
"departement",
"is_kfet",
"comments",
]
STATUS_CHOICES = (
("no", "Non"),
("wait", "Oui mais attente paiement"),
@ -444,3 +458,20 @@ class GestioncofConfigForm(ConfigForm):
max_length=2048,
required=False,
)
# ----
# Formulaire pour les adhésions self-service
# ----
class SubscribForm(forms.Form):
accept_ri = forms.BooleanField(
label="Lu et accepte le réglement intérieur de l'AEENS (COF).", required=True
)
accept_status = forms.BooleanField(
label="Lu et accepte les status de l'AEENS (COF).", required=True
)
accept_charte_kf = forms.BooleanField(
label="Lu et accepte la charte de la K-Fêt.", required=True
)

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-12-24 10:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gestioncof", "0020_merge_20241218_2240"),
]
operations = [
migrations.AddField(
model_name="cofprofile",
name="is_kfet",
field=models.BooleanField(default=False, verbose_name="Membre K-Fêt"),
),
migrations.AlterField(
model_name="cofprofile",
name="is_cof",
field=models.BooleanField(default=False, verbose_name="Membre COF"),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 4.2.16 on 2024-12-30 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gestioncof", "0021_cofprofile_is_kfet_alter_cofprofile_is_cof"),
]
operations = [
migrations.AlterField(
model_name="cofprofile",
name="date_adhesion",
field=models.DateField(
blank=True, null=True, verbose_name="Date d'adhésion COF"
),
),
migrations.RenameField(
model_name="cofprofile",
old_name="date_adhesion",
new_name="date_adhesion_cof",
),
migrations.AddField(
model_name="cofprofile",
name="date_adhesion_kfet",
field=models.DateField(
blank=True, null=True, verbose_name="Date d'adhésion K-Fêt"
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-01-21 10:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gestioncof", "0022_rename_cofprofile_date_adhesion_and_more"),
]
operations = [
migrations.AddField(
model_name="cofprofile",
name="is_chef",
field=models.BooleanField(default=False, verbose_name="Chef K-Fêt"),
),
]

View file

@ -1,7 +1,13 @@
from datetime import date
from smtplib import SMTPRecipientsRefused
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.mail import send_mail
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.template import loader
from django.utils.translation import gettext_lazy as _
from bda.models import Spectacle
@ -49,8 +55,12 @@ class CofProfile(models.Model):
login_clipper = models.CharField(
"Login clipper", max_length=32, blank=True, unique=True, null=True
)
is_cof = models.BooleanField("Membre du COF", default=False)
date_adhesion = models.DateField("Date d'adhésion", blank=True, null=True)
is_cof = models.BooleanField("Membre COF", default=False)
is_kfet = models.BooleanField("Membre K-Fêt", default=False)
date_adhesion_cof = models.DateField("Date d'adhésion COF", blank=True, null=True)
date_adhesion_kfet = models.DateField(
"Date d'adhésion K-Fêt", blank=True, null=True
)
phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(
_("Occupation"),
@ -75,6 +85,7 @@ class CofProfile(models.Model):
)
comments = models.TextField("Commentaires visibles par l'utilisateur", blank=True)
is_buro = models.BooleanField("Membre du Burô", default=False)
is_chef = models.BooleanField("Chef K-Fêt", default=False)
petits_cours_accept = models.BooleanField(
"Recevoir des petits cours", default=False
)
@ -89,6 +100,46 @@ class CofProfile(models.Model):
def __str__(self):
return self.user.username
def make_adh_cof(self, request, was_cof):
if self.is_cof and not was_cof:
notify_new_member(request, self.user)
self.date_adhesion_cof = date.today()
self.save()
def make_adh_kfet(self, request, was_kfet):
if self.is_kfet and not was_kfet:
notify_new_member(request, self.user)
self.date_adhesion_kfet = date.today()
self.save()
def notify_new_member(request, member: User):
if not member.email:
messages.warning(
request,
"GestioCOF n'a pas d'adresse mail pour {}, ".format(member)
+ "aucun email de bienvenue n'a été envoyé",
)
return
# Try to send a welcome email and report SMTP errors
try:
send_mail(
"Bienvenue au COF",
loader.render_to_string(
"gestioncof/mails/welcome.txt", context={"member": member}
),
"cof@ens.fr",
[member.email],
)
except SMTPRecipientsRefused:
messages.error(
request,
"Error lors de l'envoi de l'email de bienvenue à {} ({})".format(
member, member.email
),
)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):

View file

@ -701,6 +701,9 @@ header a:active {
.user-is-cof {
color : #ADE297;
}
.user-is-kfet {
color : #FF8C00;
}
.user-is-not-cof {
color: #EE8585;
}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Section réservée aux membres du COF -- merci de vous inscrire au COF ou de passer au COF/nous envoyer un mail si vous êtes déjà membre :)</h2>
<h2>Section réservée aux membres COF -- merci de vous inscrire au COF ou de passer au COF/nous envoyer un mail si vous êtes déjà membre :)</h2>
{% endblock %}

View file

@ -10,10 +10,12 @@
{% endblock %}
</a>
<div class="secondary">
{% if user.is_authenticated %}
<span class="hidden-xxs">&nbsp;&nbsp;|&nbsp; </span>
<span><a href="{% url "cof-logout" %}">Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span></a></span>
{% endif %}
</div>
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
<h2 class="member-status">{%if user.is_authenticated %}{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% endif %}{% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% elif user.profile.is_kfet %}<tt class="user-is-kfet">membre K-Fêt{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
</div><!-- /.container -->
</header>

View file

@ -0,0 +1,19 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>Pass K-Fêt</h2>
<center style="font-size: 20pt;">
<p>Profil de {{ user.first_name }} {{ user.last_name }}</p>
{% if user.profile.is_cof %}
<p>Membre COF depuis le {{ user.profile.date_adhesion_cof }}</p>
{% else %}
<p>Membre K-Fêt depuis le {{ user.profile.date_adhesion_kfet }}</p>
{% endif %}
</center>
{% endblock %}

View file

@ -8,7 +8,7 @@
<div class="container hidden-xs espace"></div>
<div class="container">
<div class="home-menu row">
<div class="{% if user.profile.is_buro %}col-sm-6 {% else %}col-sm-8 col-sm-offset-2 col-xs-12 {%endif%}normal-user-hm">
<div class="{% if user.profile.is_buro or user.profile.is_chef %}col-sm-6 {% else %}col-sm-8 col-sm-offset-2 col-xs-12 {%endif%}normal-user-hm">
{% if open_surveys %}
<h3 class="block-title">Sondages en cours<span class="pull-right glyphicon glyphicon-stats"></span></h3>
<div class="hm-block">
@ -50,6 +50,9 @@
<ul>
{# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
<li><a href="{% slugurl "k-fet" %}">Page d'accueil</a></li>
{% if user.profile.is_cof or user.profile.is_kfet %}
<li><a href="{% url "profile.carte" %}">Carte K-Fêt</a></li>
{% endif %}
<li><a href="https://cof.ens.fr/k-fet/le-calendrier/">Calendrier</a></li>
{% if perms.kfet.is_team %}
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
@ -68,9 +71,23 @@
{% if not user.profile.login_clipper %}
<li><a href="{% url "password_change" %}">Changer mon mot de passe</a></li>
{% endif %}
{% if not user.profile.is_cof and not user.profile.is_kfet %}
<li><a href="{% url "self.kf_registration" %}">Adhérer à la K-Fêt</a></li>
{% endif %}
</ul>
</div>
</div>
{% if user.profile.is_chef and not user.profile.is_buro %}
<div class="col-sm-6 buro-user-hm">
<h3 class="block-title">Administration<span class="pull-right glyphicon glyphicon-cog"></span></h3>
<div class="hm-block">
<ul>
<h4>Général</h4>
<li><a href="{% url "registration" %}">Inscription d'un nouveau membre</a></li>
</ul>
</div>
</div>
{% endif %}
{% if user.profile.is_buro %}
<div class="col-sm-6 buro-user-hm">
<h3 class="block-title">Administration<span class="pull-right glyphicon glyphicon-cog"></span></h3>

View file

@ -0,0 +1,21 @@
{% load bootstrap %}
{% if login_clipper %}
<h3>Inscription associée au compte clipper <tt>{{ login_clipper }}</tt></h3>
{% elif member %}
<h3>Inscription du compte GestioCOF existant <tt>{{ member.username }}</tt></h3>
{% else %}
<h3>Inscription d'un nouveau compte (extérieur ?)</h3>
{% endif %}
<form role="form" id="profile" method="post" action="{% url 'registration' %}">
{% csrf_token %}
<table>
{{ user_form | bootstrap }}
{{ profile_form | bootstrap }}
</table>
<hr />
{% if login_clipper or member %}
<input type="hidden" name="user_exists" value="1" />
{% endif %}
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer l'inscription" />
</form>

View file

@ -0,0 +1,8 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Inscription d'un nouveau membre</h2>
<div id="form-placeholder">
{% include "gestioncof/registration_kf_form.html" %}
</div>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends "base_title.html" %}
{% load static %}
{% block page_size %}col-sm-8{% endblock %}
{% load bootstrap %}
{% block realcontent %}
{% if member %}
<h3>Inscription K-Fêt du compte GestioCOF existant <tt>{{ member.username }}</tt></h3>
{% else %}
<h3>Inscription K-Fêt d'un nouveau compte (extérieur ?)</h3>
{% endif %}
<form role="form" id="profile" method="post" action="{% url 'self.kf_registration' %}">
{% csrf_token %}
<table>
{{ user_form | bootstrap }}
{{ profile_form | bootstrap }}
{{ agreement_form | bootstrap }}
</table>
<hr />
{% if login_clipper or member %}
<input type="hidden" name="user_exists" value="1" />
{% endif %}
<input type="submit" class="btn btn-primary pull-right" value="Adhérer" />
</form>
{% endblock %}

View file

@ -0,0 +1,5 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Section réservée aux membres K-Fêt -- merci de vous inscrire au COF ou de passer au COF/nous envoyer un mail si vous êtes déjà membre :)</h2>
{% endblock %}

View file

@ -486,7 +486,7 @@ class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase):
u1.last_name = "last"
u1.email = "user@mail.net"
u1.save()
u1.profile.date_adhesion = date(2023, 5, 22)
u1.profile.date_adhesion_cof = date(2023, 5, 22)
u1.profile.phone = "0123456789"
u1.profile.departement = "Dept"
u1.profile.save()

View file

@ -80,6 +80,7 @@ registration_patterns = [
views.RegistrationAutocompleteView.as_view(),
name="cof.registration.autocomplete",
),
path("self_kf", views.self_kf_registration, name="self.kf_registration"),
]
urlpatterns = [
@ -99,6 +100,7 @@ urlpatterns = [
name="cof-user-autocomplete",
),
path("config", views.ConfigUpdate.as_view(), name="config.edit"),
path("carte", views.carte_kf, name="profile.carte"),
# -----
# Authentification
# -----

View file

@ -1,7 +1,6 @@
import csv
import uuid
from datetime import date, timedelta
from smtplib import SMTPRecipientsRefused
from datetime import timedelta
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from django.contrib import messages
@ -14,11 +13,10 @@ from django.contrib.auth.views import (
redirect_to_login,
)
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, TemplateView
@ -27,7 +25,14 @@ from icalendar import Calendar, Event as Vevent
from bda.models import Spectacle, Tirage
from gestioncof.autocomplete import cof_autocomplete
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
from gestioncof.decorators import (
BuroRequiredMixin,
ChefRequiredMixin,
buro_required,
chef_required,
cof_required,
kfet_required,
)
from gestioncof.forms import (
CalendarForm,
ClubsForm,
@ -38,9 +43,11 @@ from gestioncof.forms import (
GestioncofConfigForm,
PhoneForm,
ProfileForm,
RegistrationKFProfileForm,
RegistrationPassUserForm,
RegistrationProfileForm,
RegistrationUserForm,
SubscribForm,
SurveyForm,
SurveyStatusFilterForm,
UserForm,
@ -86,7 +93,9 @@ class ResetComptes(BuroRequiredMixin, TemplateView):
nb_adherents = CofProfile.objects.filter(is_cof=True).count()
CofProfile.objects.update(
is_cof=False,
date_adhesion=None,
is_kfet=False,
date_adhesion_cof=None,
date_adhesion_kfet=None,
mailing_cof=False,
mailing_bda=False,
mailing_bda_revente=False,
@ -421,13 +430,20 @@ def profile(request):
return render(request, "gestioncof/profile.html", context)
@kfet_required
def carte_kf(request):
user = request.user
return render(request, "gestioncof/carte_kf.html", {"user": user})
def registration_set_ro_fields(user_form, profile_form):
user_form.fields["username"].widget.attrs["readonly"] = True
profile_form.fields["login_clipper"].widget.attrs["readonly"] = True
@buro_required
@chef_required
def registration_form2(request, login_clipper=None, username=None, fullname=None):
is_buro = request.user.profile.is_buro
events = Event.objects.filter(old=False).all()
member = None
if login_clipper:
@ -449,85 +465,84 @@ def registration_form2(request, login_clipper=None, username=None, fullname=None
user_form.fields["first_name"].initial = bits[0]
if len(bits) > 1:
user_form.fields["last_name"].initial = " ".join(bits[1:])
# profile
profile_form = RegistrationProfileForm(
initial={"login_clipper": login_clipper}
)
if is_buro:
# profile
profile_form = RegistrationProfileForm(
initial={"login_clipper": login_clipper}
)
# events & clubs
event_formset = EventFormset(events=events, prefix="events")
clubs_form = ClubsForm()
else:
profile_form = RegistrationKFProfileForm(
initial={"login_clipper": login_clipper}
)
registration_set_ro_fields(user_form, profile_form)
# events & clubs
event_formset = EventFormset(events=events, prefix="events")
clubs_form = ClubsForm()
if username:
member = get_object_or_404(User, username=username)
(profile, _) = CofProfile.objects.get_or_create(user=member)
# already existing, prefill
user_form = RegistrationUserForm(instance=member)
profile_form = RegistrationProfileForm(instance=profile)
if is_buro:
profile_form = RegistrationProfileForm(instance=profile)
# events
current_registrations = []
for event in events:
try:
current_registrations.append(
EventRegistration.objects.get(user=member, event=event)
)
except EventRegistration.DoesNotExist:
current_registrations.append(None)
event_formset = EventFormset(
events=events,
prefix="events",
current_registrations=current_registrations,
)
# Clubs
clubs_form = ClubsForm(initial={"clubs": member.clubs.all()})
else:
profile_form = RegistrationKFProfileForm(instance=profile)
registration_set_ro_fields(user_form, profile_form)
# events
current_registrations = []
for event in events:
try:
current_registrations.append(
EventRegistration.objects.get(user=member, event=event)
)
except EventRegistration.DoesNotExist:
current_registrations.append(None)
event_formset = EventFormset(
events=events, prefix="events", current_registrations=current_registrations
)
# Clubs
clubs_form = ClubsForm(initial={"clubs": member.clubs.all()})
elif not login_clipper:
# new user
user_form = RegistrationPassUserForm()
profile_form = RegistrationProfileForm()
event_formset = EventFormset(events=events, prefix="events")
clubs_form = ClubsForm()
return render(
request,
"gestioncof/registration_form.html",
{
"member": member,
"login_clipper": login_clipper,
"user_form": user_form,
"profile_form": profile_form,
"event_formset": event_formset,
"clubs_form": clubs_form,
},
)
def notify_new_member(request, member: User):
if not member.email:
messages.warning(
if is_buro:
profile_form = RegistrationProfileForm()
event_formset = EventFormset(events=events, prefix="events")
clubs_form = ClubsForm()
else:
profile_form = RegistrationKFProfileForm()
if is_buro:
return render(
request,
"GestioCOF n'a pas d'adresse mail pour {}, ".format(member)
+ "aucun email de bienvenue n'a été envoyé",
"gestioncof/registration_form.html",
{
"member": member,
"login_clipper": login_clipper,
"user_form": user_form,
"profile_form": profile_form,
"event_formset": event_formset,
"clubs_form": clubs_form,
},
)
return
# Try to send a welcome email and report SMTP errors
try:
send_mail(
"Bienvenue au COF",
loader.render_to_string(
"gestioncof/mails/welcome.txt", context={"member": member}
),
"cof@ens.fr",
[member.email],
)
except SMTPRecipientsRefused:
messages.error(
else:
return render(
request,
"Error lors de l'envoi de l'email de bienvenue à {} ({})".format(
member, member.email
),
"gestioncof/registration_kf_form.html",
{
"member": member,
"login_clipper": login_clipper,
"user_form": user_form,
"profile_form": profile_form,
},
)
@buro_required
@chef_required
def registration(request):
is_buro = request.user.profile.is_buro
if request.POST:
request_dict = request.POST.copy()
member = None
@ -541,10 +556,15 @@ def registration(request):
user_form = RegistrationPassUserForm(request_dict)
else:
user_form = RegistrationUserForm(request_dict)
profile_form = RegistrationProfileForm(request_dict)
clubs_form = ClubsForm(request_dict)
events = Event.objects.filter(old=False).all()
event_formset = EventFormset(events=events, data=request_dict, prefix="events")
if is_buro:
profile_form = RegistrationProfileForm(request_dict)
clubs_form = ClubsForm(request_dict)
events = Event.objects.filter(old=False).all()
event_formset = EventFormset(
events=events, data=request_dict, prefix="events"
)
else:
profile_form = RegistrationKFProfileForm(request_dict)
if "user_exists" in request_dict and request_dict["user_exists"]:
username = request_dict["username"]
try:
@ -565,63 +585,72 @@ def registration(request):
member = user_form.save()
profile, _ = CofProfile.objects.get_or_create(user=member)
was_cof = profile.is_cof
was_kfet = profile.is_kfet
# Maintenant on remplit le formulaire de profil
profile_form = RegistrationProfileForm(request_dict, instance=profile)
if (
profile_form.is_valid()
and event_formset.is_valid()
and clubs_form.is_valid()
if is_buro:
profile_form = RegistrationProfileForm(request_dict, instance=profile)
else:
profile_form = RegistrationKFProfileForm(request_dict, instance=profile)
if profile_form.is_valid() and (
not is_buro or (event_formset.is_valid() and clubs_form.is_valid())
):
# Enregistrement du profil
profile = profile_form.save()
if profile.is_cof and not was_cof:
notify_new_member(request, member)
profile.date_adhesion = date.today()
profile.save()
if is_buro:
if profile.is_cof:
profile.make_adh_cof(request, was_cof)
# Enregistrement des inscriptions aux événements
for form in event_formset:
if "status" not in form.cleaned_data:
form.cleaned_data["status"] = "no"
if form.cleaned_data["status"] == "no":
try:
current_registration = EventRegistration.objects.get(
user=member, event=form.event
)
current_registration.delete()
except EventRegistration.DoesNotExist:
pass
continue
all_choices = get_event_form_choices(form.event, form)
(
current_registration,
created_reg,
) = EventRegistration.objects.get_or_create(
user=member, event=form.event
)
update_event_form_comments(form.event, form, current_registration)
current_registration.options.set(all_choices)
current_registration.paid = form.cleaned_data["status"] == "paid"
current_registration.save()
# if form.event.title == "Mega 15" and created_reg:
# field = EventCommentField.objects.get(
# event=form.event, name="Commentaires")
# try:
# comments = EventCommentValue.objects.get(
# commentfield=field,
# registration=current_registration).content
# except EventCommentValue.DoesNotExist:
# comments = field.default
# FIXME : il faut faire quelque chose de propre ici,
# par exemple écrire un mail générique pour
# l'inscription aux événements et/ou donner la
# possibilité d'associer un mail aux événements
# send_custom_mail(...)
# Enregistrement des inscriptions aux clubs
member.clubs.clear()
for club in clubs_form.cleaned_data["clubs"]:
club.membres.add(member)
club.save()
if profile.is_kfet:
profile.make_adh_kfet(request, was_kfet)
if is_buro:
# Enregistrement des inscriptions aux événements
for form in event_formset:
if "status" not in form.cleaned_data:
form.cleaned_data["status"] = "no"
if form.cleaned_data["status"] == "no":
try:
current_registration = EventRegistration.objects.get(
user=member, event=form.event
)
current_registration.delete()
except EventRegistration.DoesNotExist:
pass
continue
all_choices = get_event_form_choices(form.event, form)
(
current_registration,
created_reg,
) = EventRegistration.objects.get_or_create(
user=member, event=form.event
)
update_event_form_comments(
form.event, form, current_registration
)
current_registration.options.set(all_choices)
current_registration.paid = (
form.cleaned_data["status"] == "paid"
)
current_registration.save()
# if form.event.title == "Mega 15" and created_reg:
# field = EventCommentField.objects.get(
# event=form.event, name="Commentaires")
# try:
# comments = EventCommentValue.objects.get(
# commentfield=field,
# registration=current_registration).content
# except EventCommentValue.DoesNotExist:
# comments = field.default
# FIXME : il faut faire quelque chose de propre ici,
# par exemple écrire un mail générique pour
# l'inscription aux événements et/ou donner la
# possibilité d'associer un mail aux événements
# send_custom_mail(...)
# Enregistrement des inscriptions aux clubs
member.clubs.clear()
for club in clubs_form.cleaned_data["clubs"]:
club.membres.add(member)
club.save()
# ---
# Success
@ -633,27 +662,91 @@ def registration(request):
member.get_full_name(), member.email
)
)
if profile.is_cof:
if is_buro and profile.is_cof:
msg += "\nIl est désormais membre du COF n°{:d} !".format(
member.profile.id
)
messages.success(request, msg, extra_tags="safe")
return render(
request,
"gestioncof/registration_post.html",
{
"user_form": user_form,
"profile_form": profile_form,
"member": member,
"login_clipper": login_clipper,
"event_formset": event_formset,
"clubs_form": clubs_form,
},
)
if is_buro:
return render(
request,
"gestioncof/registration_post.html",
{
"user_form": user_form,
"profile_form": profile_form,
"member": member,
"login_clipper": login_clipper,
"event_formset": event_formset,
"clubs_form": clubs_form,
},
)
else:
return render(
request,
"gestioncof/registration_kf_post.html",
{
"user_form": user_form,
"profile_form": profile_form,
"member": member,
"login_clipper": login_clipper,
},
)
else:
return render(request, "registration.html")
# TODO: without login
@login_required
def self_kf_registration(request):
member = request.user
(profile, _) = CofProfile.objects.get_or_create(user=member)
if profile.is_kfet or profile.is_cof:
msg = "Vous êtes déjà adhérent du COF !"
messages.success(request, msg)
response = HttpResponse(content="", status=303)
response["Location"] = reverse("profile")
return response
was_kfet = profile.is_kfet
if request.POST:
user_form = RegistrationUserForm(request.POST, instance=member)
profile_form = PhoneForm(request.POST, instance=profile)
agreement_form = SubscribForm(request.POST)
if (
user_form.is_valid()
and profile_form.is_valid()
and agreement_form.is_valid()
):
member = user_form.save()
profile = profile_form.save()
profile.is_kfet = True
profile.save()
profile.make_adh_kfet(request, was_kfet)
msg = "Votre adhésion a été enregistrée avec succès."
messages.success(request, msg, extra_tags="safe")
response = HttpResponse(content="", status=303)
response["Location"] = reverse("profile")
return response
else:
user_form = RegistrationUserForm(instance=member)
profile_form = PhoneForm(instance=profile)
agreement_form = SubscribForm()
user_form.fields["username"].widget.attrs["readonly"] = True
return render(
request,
"gestioncof/self_registration.html",
{
"user_form": user_form,
"profile_form": profile_form,
"agreement_form": agreement_form,
"member": member,
},
)
# -----
# Clubs
# -----
@ -707,7 +800,7 @@ def export_members(request):
response["Content-Disposition"] = "attachment; filename=membres_cof.csv"
writer = csv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all():
for profile in CofProfile.objects.filter(Q(is_cof=True) | Q(is_kfet=True)).all():
user = profile.user
bits = [
user.id,
@ -718,8 +811,10 @@ def export_members(request):
profile.phone,
profile.occupation,
profile.departement,
"COF" if profile.is_cof else "K-Fêt",
profile.type_cotiz,
profile.date_adhesion,
profile.date_adhesion_cof,
profile.date_adhesion_kfet,
]
writer.writerow([str(bit) for bit in bits])
@ -975,6 +1070,6 @@ class UserAutocompleteView(BuroRequiredMixin, Select2QuerySetView):
search_fields = ("username", "first_name", "last_name")
class RegistrationAutocompleteView(BuroRequiredMixin, AutocompleteView):
class RegistrationAutocompleteView(ChefRequiredMixin, AutocompleteView):
template_name = "gestioncof/search_results.html"
search_composer = cof_autocomplete

View file

@ -195,7 +195,13 @@ class CofForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = ["login_clipper", "is_cof", "departement"]
fields = ["login_clipper", "is_cof", "is_kfet", "departement"]
class CofKFForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = ["is_kfet"]
class UserForm(forms.ModelForm):
@ -350,6 +356,7 @@ class ArticleForm(forms.ModelForm):
fields = [
"name",
"is_sold",
"no_exte",
"hidden",
"price",
"stock",
@ -364,6 +371,7 @@ class ArticleRestrictForm(ArticleForm):
fields = [
"name",
"is_sold",
"no_exte",
"hidden",
"price",
"category",

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.16 on 2025-01-06 16:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("kfet", "0080_accountnegative_last_rappel"),
]
operations = [
migrations.AddField(
model_name="article",
name="no_exte",
field=models.BooleanField(
default=False, verbose_name="Réservé au adhérents"
),
),
]

View file

@ -0,0 +1,34 @@
# Generated by Django 4.2.16 on 2025-01-18 10:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("kfet", "0081_article_no_exte"),
]
operations = [
migrations.AlterModelOptions(
name="operation",
options={
"permissions": (
("perform_deposit", "Effectuer une charge"),
(
"perform_negative_operations",
"Enregistrer des commandes en négatif",
),
(
"perform_liq_reserved",
"Effectuer une opération réservé aux adhérents sur LIQ",
),
("cancel_old_operations", "Annuler des commandes non récentes"),
(
"perform_commented_operations",
"Enregistrer des commandes avec commentaires",
),
)
},
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-03-18 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("kfet", "0082_alter_operation_options"),
]
operations = [
migrations.AddField(
model_name="operationgroup",
name="is_kfet",
field=models.BooleanField(default=False),
),
]

View file

@ -119,6 +119,10 @@ class Account(models.Model):
def is_cof(self):
return self.cofprofile.is_cof
@property
def is_kfet(self):
return self.cofprofile.is_kfet
# Propriétés supplémentaires
@property
def balance_ukf(self):
@ -494,6 +498,7 @@ class ArticleCategory(models.Model):
class Article(models.Model):
name = models.CharField("nom", max_length=45)
is_sold = models.BooleanField("en vente", default=True)
no_exte = models.BooleanField("Réservé au adhérents", default=False)
hidden = models.BooleanField(
"caché",
default=False,
@ -682,6 +687,7 @@ class OperationGroup(models.Model):
at = models.DateTimeField(default=timezone.now)
amount = models.DecimalField(max_digits=6, decimal_places=2, default=0)
is_cof = models.BooleanField(default=False)
is_kfet = models.BooleanField(default=False)
# Optional
comment = models.CharField(max_length=255, blank=True, default="")
valid_by = models.ForeignKey(
@ -754,6 +760,10 @@ class Operation(models.Model):
permissions = (
("perform_deposit", "Effectuer une charge"),
("perform_negative_operations", "Enregistrer des commandes en négatif"),
(
"perform_liq_reserved",
"Effectuer une opération réservé aux adhérents sur LIQ",
),
("cancel_old_operations", "Annuler des commandes non récentes"),
(
"perform_commented_operations",

View file

@ -5,6 +5,7 @@ var Account = Backbone.Model.extend({
'name': '',
'email': '',
'is_cof': '',
'is_kfet': '',
'promo': '',
'balance': '',
'is_frozen': false,
@ -69,7 +70,7 @@ var AccountView = Backbone.View.extend({
},
get_is_cof: function () {
return this.model.get("is_cof") ? 'COF' : 'Non-COF';
return this.model.get("is_cof") ? 'Membre COF' : (this.model.get("is_kfet") ? 'Membre K-Fêt' : 'Non-COF');
},
get_balance: function () {

View file

@ -34,10 +34,11 @@ Modification de mes informations
{% include 'kfet/form_snippet.html' with form=account_form %}
{% include 'kfet/form_snippet.html' with form=frozen_form %}
{% include 'kfet/form_snippet.html' with form=group_form %}
{% include 'kfet/form_snippet.html' with form=cof_form %}
{% include 'kfet/form_snippet.html' with form=pwd_form %}
{% include 'kfet/form_authentication_snippet.html' %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
{% endblock %}
{% endblock %}

View file

@ -40,6 +40,7 @@
<td class="text-right">Prix</td>
<td class="text-right">Stock</td>
<td class="text-right" data-sorter="article__is_sold">En vente</td>
<td class="text-right" data-sorter="article__is_no_exte">Reservé aux adhérents</td>
<td class="text-right" data-sorter="article__hidden">Affiché</td>
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
</tr>
@ -63,6 +64,7 @@
<td class="text-right">{{ article.price }}€</td>
<td class="text-right">{{ article.stock }}</td>
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
<td class="text-right">{{ article.no_exte | yesno:"Réservé,Non réservé"}}</td>
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
{% with last_inventory=article.inventory.0 %}
<td class="text-right" title="{{ last_inventory.at }}">
@ -88,6 +90,7 @@
<td class="text-right">Prix</td>
<td class="text-right">Stock</td>
<td class="text-right" data-sorter="article__is_sold">En vente</td>
<td class="text-right" data-sorter="article__is_no_exte">Reservé aux adhérents</td>
<td class="text-right" data-sorter="article__hidden">Affiché</td>
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
</tr>
@ -111,6 +114,7 @@
<td class="text-right">{{ article.price }}€</td>
<td class="text-right">{{ article.stock }}</td>
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
<td class="text-right">{{ article.no_exte | yesno:"Réservé,Non réservé"}}</td>
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
{% with last_inventory=article.inventory.0 %}
<td class="text-right" title="{{ last_inventory.at }}">

View file

@ -39,6 +39,7 @@
<li><b>Stock:</b> {{ article.stock }}</li>
<li><b>En vente:</b> {{ article.is_sold|yesno|title }}</li>
<li><b>Affiché:</b> {{ article.hidden|yesno|title }}</li>
<li><b>Réservé aux adhérents:</b> {{ article.no_exte|yesno|title }}</li>
</ul>
</div>
</aside>
@ -160,4 +161,4 @@
});
</script>
{% endblock %}
{% endblock %}

View file

@ -397,8 +397,8 @@ $(document).ready(function() {
// -----
var articles_container = $('#articles_data tbody');
var article_category_default_html = '<tr class="category"><td colspan="3"></td></tr>';
var article_default_html = '<tr class="article"><td class="name"></td><td class="price"></td><td class="stock"></td></tr>';
var article_category_default_html = '<tr class="category"><td colspan="4"></td></tr>';
var article_default_html = '<tr class="article"><td class="name"></td><td class="price"></td><td class="no_exte"></td><td class="stock" style="width: 0;white-space: nowrap;"></td></tr>';
function addArticle(article) {
var article_html = $(article_default_html);
@ -411,6 +411,7 @@ $(document).ready(function() {
article_html.addClass('low-stock');
}
article_html.find('.price').text(amountToUKF(article['price'], false, false)+' UKF');
article_html.find('.no_exte').text(article['no_exte'] ? "Réservé aux adhérents" : "");
var category_html = articles_container
.find('#data-category-'+article['category_id']);
if (category_html.length == 0) {

View file

@ -43,7 +43,9 @@
<li>
{% if account.is_cof %}
<span title="Réduction de {{ kfet_config.reduction_cof }} % sur tes commandes" data-toggle="tooltip"
data-placement="right">Adhérent COF</span>
data-placement="right">Membre COF</span>
{% elif account.is_kfet %}
Membre K-Fêt
{% else %}
Non-COF
{% endif %}

View file

@ -66,6 +66,7 @@ from kfet.forms import (
CheckoutStatementCreateForm,
CheckoutStatementUpdateForm,
CofForm,
CofKFForm,
ContactForm,
DemandeSoireeForm,
FilterHistoryForm,
@ -187,13 +188,17 @@ def account(request):
positive_accounts = Account.objects.filter(balance__gte=0).exclude(trigramme="#13")
negative_accounts = Account.objects.filter(balance__lt=0).exclude(trigramme="#13")
return render(request, "kfet/account.html", {
"accounts": accounts,
"positive_count": positive_accounts.count(),
"positives_sum": sum(acc.balance for acc in positive_accounts),
"negative_count": negative_accounts.count(),
"negatives_sum": sum(acc.balance for acc in negative_accounts),
})
return render(
request,
"kfet/account.html",
{
"accounts": accounts,
"positive_count": positive_accounts.count(),
"positives_sum": sum(acc.balance for acc in positive_accounts),
"negative_count": negative_accounts.count(),
"negatives_sum": sum(acc.balance for acc in negative_accounts),
},
)
@login_required
@ -252,6 +257,11 @@ def account_create(request):
account = trigramme_form.save(data=data)
account_form = AccountNoTriForm(request.POST, instance=account)
account_form.save()
was_kfet = account.is_kfet
account.cofprofile.is_kfet = cof_form.cleaned_data["is_kfet"]
account.cofprofile.save()
if account.cofprofile.is_kfet:
account.cofprofile.make_adh_kfet(request, was_kfet)
messages.success(request, "Compte créé : %s" % account.trigramme)
account.send_creation_email()
return redirect("kfet.account.create")
@ -432,6 +442,7 @@ def account_update(request, trigramme):
account_form = AccountForm(instance=account)
group_form = UserGroupForm(instance=account.user)
frozen_form = AccountFrozenForm(instance=account)
cof_form = CofKFForm(instance=account.cofprofile)
pwd_form = AccountPwdForm()
if request.method == "POST":
@ -439,6 +450,7 @@ def account_update(request, trigramme):
account_form = AccountForm(request.POST, instance=account)
group_form = UserGroupForm(request.POST, instance=account.user)
frozen_form = AccountFrozenForm(request.POST, instance=account)
cof_form = CofKFForm(request.POST, instance=account.cofprofile)
pwd_form = AccountPwdForm(request.POST, account=account)
forms = []
@ -455,6 +467,11 @@ def account_update(request, trigramme):
elif group_form.has_changed():
warnings.append("statut d'équipe")
if request.user.has_perm("kfet.change_account"):
forms.append(cof_form)
elif cof_form.has_changed():
warnings.append("adhésion kfet")
# Il ne faut pas valider `pwd_form` si elle est inchangée
if pwd_form.has_changed():
if self_update or request.user.has_perm("kfet.change_account_password"):
@ -471,8 +488,11 @@ def account_update(request, trigramme):
)
else:
if all(form.is_valid() for form in forms):
was_kfet = account.is_kfet
for form in forms:
form.save()
if account.is_kfet:
account.cofprofile.make_adh_kfet(request, was_kfet)
if len(warnings):
messages.warning(
@ -504,6 +524,7 @@ def account_update(request, trigramme):
"frozen_form": frozen_form,
"group_form": group_form,
"pwd_form": pwd_form,
"cof_form": cof_form,
},
)
@ -988,6 +1009,7 @@ def account_read_json(request, trigramme):
"name": account.name,
"email": account.email,
"is_cof": account.is_cof,
"is_kfet": account.is_kfet,
"promo": account.promo,
"balance": account.balance,
"is_frozen": account.is_frozen,
@ -1164,6 +1186,22 @@ def kpsul_perform_operations(request):
if is_addcost and operation.article.category.has_addcost:
operation.addcost_amount /= cof_grant_divisor
operation.amount = operation.amount / cof_grant_divisor
if not on_acc.is_cof and not on_acc.is_kfet and operation.article.no_exte:
if on_acc.is_cash:
required_perms.add("kfet.perform_liq_reserved")
else:
data["errors"].append(
{
"code": "reserved",
"message": (
"L'article "
+ operation.article.name
+ " est réservé aux adhérents du COF, or "
+ on_acc.trigramme
+ " ne l'est pas"
),
}
)
to_articles_stocks[operation.article] -= operation.article_nb
else:
if on_acc.is_cash:
@ -1221,6 +1259,7 @@ def kpsul_perform_operations(request):
operationgroup.valid_by = request.user.profile.account_kfet
# Filling cof status for statistics
operationgroup.is_cof = on_acc.is_cof
operationgroup.is_kfet = on_acc.is_kfet
# Starting transaction to ensure data consistency
with transaction.atomic():
@ -1271,6 +1310,7 @@ def kpsul_perform_operations(request):
"checkout__name": operationgroup.checkout.name,
"at": operationgroup.at,
"is_cof": operationgroup.is_cof,
"is_kfet": operationgroup.is_kfet,
"comment": operationgroup.comment,
"valid_by__trigramme": (
operationgroup.valid_by and operationgroup.valid_by.trigramme or None
@ -1486,7 +1526,7 @@ def cancel_operations(request):
# Sort objects by pk to get deterministic responses.
opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts]
opegroups = (
OperationGroup.objects.values("id", "amount", "is_cof")
OperationGroup.objects.values("id", "amount", "is_cof", "is_kfet")
.filter(pk__in=opegroups_pk)
.order_by("pk")
)
@ -1715,6 +1755,7 @@ def kpsul_articles_data(request):
"id",
"name",
"price",
"no_exte",
"stock",
"category_id",
"category__name",