mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-20 17:41:55 +02:00
Merge remote-tracking branch 'origin/master' into import_nk15
# Conflicts: # apps/treasury/signals.py
This commit is contained in:
@ -4,7 +4,7 @@ from datetime import timedelta, datetime
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Club
|
||||
from note.models import NoteUser, Note
|
||||
from note_kfet.inputs import DateTimePickerInput, Autocomplete
|
||||
|
@ -139,7 +139,7 @@ class Entry(models.Model):
|
||||
verbose_name = _("entry")
|
||||
verbose_name_plural = _("entries")
|
||||
|
||||
def save(self, *args,**kwargs):
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
|
||||
if qs.exists():
|
||||
@ -153,7 +153,7 @@ class Entry(models.Model):
|
||||
if self.note.balance < 0:
|
||||
raise ValidationError(_("The balance is negative."))
|
||||
|
||||
ret = super().save(*args,**kwargs)
|
||||
ret = super().save(*args, **kwargs)
|
||||
|
||||
if insert and self.guest:
|
||||
GuestTransaction.objects.create(
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import F, Q
|
||||
@ -45,8 +46,8 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||
context['title'] = _("Activities")
|
||||
|
||||
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
|
||||
context['upcoming'] = ActivityTable(data=upcoming_activities
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
|
||||
context['upcoming'] = ActivityTable(
|
||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
|
||||
|
||||
return context
|
||||
|
||||
@ -138,8 +139,14 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
| Q(note__noteuser__user__last_name__regex=pattern)
|
||||
| Q(name__regex=pattern)
|
||||
| Q(normalized_name__regex=Alias.normalize(pattern)))) \
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\
|
||||
.distinct()[:20]
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
|
||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
|
||||
note_qs = note_qs.distinct('note__pk')[:20]
|
||||
else:
|
||||
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
|
||||
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
|
||||
# In production mode, please use PostgreSQL.
|
||||
note_qs = note_qs.distinct()[:20]
|
||||
for note in note_qs:
|
||||
note.type = "Adhérent"
|
||||
note.activity = activity
|
||||
@ -153,9 +160,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
context["title"] = _('Entry for activity "{}"').format(activity.name)
|
||||
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
|
||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||
|
||||
|
||||
context["activities_open"] = Activity.objects.filter(open=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
|
||||
|
||||
return context
|
||||
return context
|
||||
|
@ -15,6 +15,7 @@ from note.api.urls import register_note_urls
|
||||
from treasury.api.urls import register_treasury_urls
|
||||
from logs.api.urls import register_logs_urls
|
||||
from permission.api.urls import register_permission_urls
|
||||
from wei.api.urls import register_wei_urls
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@ -78,6 +79,7 @@ register_note_urls(router, 'note')
|
||||
register_treasury_urls(router, 'treasury')
|
||||
register_permission_urls(router, 'permission')
|
||||
register_logs_urls(router, 'logs')
|
||||
register_wei_urls(router, 'wei')
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
|
@ -9,7 +9,7 @@ from note.models import NoteSpecial
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from permission.models import PermissionMask
|
||||
|
||||
from .models import Profile, Club, Membership
|
||||
from .models import Profile, Club, Membership, Role
|
||||
|
||||
|
||||
class CustomAuthenticationForm(AuthenticationForm):
|
||||
@ -25,10 +25,16 @@ class ProfileForm(forms.ModelForm):
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
"""
|
||||
|
||||
def save(self, commit=True):
|
||||
if not self.instance.section or (("department" in self.changed_data
|
||||
or "promotion" in self.changed_data) and "section" not in self.changed_data):
|
||||
self.instance.section = self.instance.section_generated
|
||||
return super().save(commit)
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', )
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', )
|
||||
|
||||
|
||||
class ClubForm(forms.ModelForm):
|
||||
@ -50,6 +56,8 @@ class ClubForm(forms.ModelForm):
|
||||
|
||||
|
||||
class MembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
|
||||
|
||||
soge = forms.BooleanField(
|
||||
label=_("Inscription paid by Société Générale"),
|
||||
required=False,
|
||||
|
@ -23,18 +23,20 @@ class Profile(models.Model):
|
||||
|
||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||
so this model add an user profile with additional information.
|
||||
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
phone_number = models.CharField(
|
||||
verbose_name=_('phone number'),
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
section = models.CharField(
|
||||
verbose_name=_('section'),
|
||||
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
|
||||
@ -42,12 +44,44 @@ class Profile(models.Model):
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
department = models.CharField(
|
||||
max_length=8,
|
||||
verbose_name=_("department"),
|
||||
choices=[
|
||||
('A0', _("Informatics (A0)")),
|
||||
('A1', _("Mathematics (A1)")),
|
||||
('A2', _("Physics (A2)")),
|
||||
("A'2", _("Applied physics (A'2)")),
|
||||
('A''2', _("Chemistry (A''2)")),
|
||||
('A3', _("Biology (A3)")),
|
||||
('B1234', _("SAPHIRE (B1234)")),
|
||||
('B1', _("Mechanics (B1)")),
|
||||
('B2', _("Civil engineering (B2)")),
|
||||
('B3', _("Mechanical engineering (B3)")),
|
||||
('B4', _("EEA (B4)")),
|
||||
('C', _("Design (C)")),
|
||||
('D2', _("Economy-management (D2)")),
|
||||
('D3', _("Social sciences (D3)")),
|
||||
('E', _("English (E)")),
|
||||
('EXT', _("External (EXT)")),
|
||||
]
|
||||
)
|
||||
|
||||
promotion = models.PositiveIntegerField(
|
||||
null=True,
|
||||
default=datetime.date.today().year,
|
||||
verbose_name=_("promotion"),
|
||||
help_text=_("Year of entry to the school (None if not ENS student)"),
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
verbose_name=_('address'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
paid = models.BooleanField(
|
||||
verbose_name=_("paid"),
|
||||
help_text=_("Tells if the user receive a salary."),
|
||||
@ -64,11 +98,29 @@ class Profile(models.Model):
|
||||
default=False,
|
||||
)
|
||||
|
||||
soge = models.BooleanField(
|
||||
verbose_name=_("Société générale"),
|
||||
help_text=_("Has the user ever be paid by the Société générale?"),
|
||||
default=False,
|
||||
)
|
||||
@property
|
||||
def ens_year(self):
|
||||
"""
|
||||
Number of years since the 1st august of the entry year, rounded up.
|
||||
"""
|
||||
if self.promotion is None:
|
||||
return 0
|
||||
today = datetime.date.today()
|
||||
years = today.year - self.promotion
|
||||
if today.month >= 8:
|
||||
years += 1
|
||||
return years
|
||||
|
||||
@property
|
||||
def section_generated(self):
|
||||
return str(self.ens_year) + self.department
|
||||
|
||||
@property
|
||||
def soge(self):
|
||||
if "treasury" in settings.INSTALLED_APPS:
|
||||
from treasury.models import SogeCredit
|
||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
@ -85,7 +137,7 @@ class Profile(models.Model):
|
||||
'user': self.user,
|
||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||
'token': email_validation_token.make_token(self.user),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)).decode('UTF-8'),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
||||
})
|
||||
self.user.email_user(subject, message)
|
||||
|
||||
@ -171,6 +223,7 @@ class Club(models.Model):
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
self._force_save = True
|
||||
self.save(force_update=True)
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
@ -220,6 +273,7 @@ class Membership(models.Model):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
@ -308,7 +362,20 @@ class Membership(models.Model):
|
||||
reason="Adhésion " + self.club.name,
|
||||
)
|
||||
transaction._force_save = True
|
||||
transaction.save(force_insert=True)
|
||||
print(hasattr(self, '_soge'))
|
||||
if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS:
|
||||
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
|
||||
# to treasurers.
|
||||
transaction.valid = False
|
||||
from treasury.models import SogeCredit
|
||||
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
|
||||
soge_credit.refresh_from_db()
|
||||
transaction.save(force_insert=True)
|
||||
transaction.refresh_from_db()
|
||||
soge_credit.transactions.add(transaction)
|
||||
soge_credit.save()
|
||||
else:
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
@ -18,7 +18,7 @@ from django.views.generic.edit import FormMixin
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from note.forms import ImageForm
|
||||
from note.models import Alias, NoteUser, NoteSpecial
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from permission.backends import PermissionBackend
|
||||
@ -128,7 +128,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||
.order_by("-created_at", "-id")\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||
@ -165,7 +166,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(profile__section__iregex=pattern)
|
||||
| Q(profile__username__iregex="^" + pattern)
|
||||
| Q(username__iregex="^" + pattern)
|
||||
| Q(note__alias__name__iregex="^" + pattern)
|
||||
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
|
||||
)
|
||||
@ -314,7 +315,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
club.update_membership_dates()
|
||||
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||
.order_by('-created_at', '-id')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
context['history_list'] = history_table
|
||||
@ -365,6 +367,15 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form_class = ClubForm
|
||||
template_name = "member/club_form.html"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
|
||||
# Don't update a WEI club through this view
|
||||
if "wei" in settings.INSTALLED_APPS:
|
||||
qs = qs.filter(weiclub=None)
|
||||
|
||||
return qs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
@ -396,7 +407,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
if "club_pk" in self.kwargs:
|
||||
# We create a new membership.
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||
.get(pk=self.kwargs["club_pk"])
|
||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
|
||||
|
||||
@ -463,17 +474,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
bank = form.cleaned_data["bank"]
|
||||
soge = form.cleaned_data["soge"] and not user.profile.soge and club.name == "BDE"
|
||||
|
||||
# If Société générale pays, then we auto-fill some data
|
||||
# If Société générale pays, then we store that information but the payment must be controlled by treasurers
|
||||
# later. The membership transaction will be invalidated.
|
||||
if soge:
|
||||
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire")
|
||||
bde = club
|
||||
kfet = Club.objects.get(name="Kfet")
|
||||
if user.profile.paid:
|
||||
fee = bde.membership_fee_paid + kfet.membership_fee_paid
|
||||
else:
|
||||
fee = bde.membership_fee_unpaid + kfet.membership_fee_unpaid
|
||||
credit_amount = fee
|
||||
bank = "Société générale"
|
||||
credit_type = None
|
||||
form.instance._soge = True
|
||||
|
||||
if credit_type is None:
|
||||
credit_amount = 0
|
||||
@ -521,6 +526,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
|
||||
# Now, all is fine, the membership can be created.
|
||||
|
||||
if club.name == "BDE":
|
||||
# When we renew the BDE membership, we update the profile section.
|
||||
# We could automate that and remove the section field from the Profile model,
|
||||
# but with this way users can customize their section as they want.
|
||||
user.profile.section = user.profile.section_generated
|
||||
user.profile.save()
|
||||
|
||||
# Credit note before the membership is created.
|
||||
if credit_amount > 0:
|
||||
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
||||
@ -544,11 +556,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
valid=True,
|
||||
)
|
||||
|
||||
# If Société générale pays, then we store the information: the bank can't pay twice to a same person.
|
||||
if soge:
|
||||
user.profile.soge = True
|
||||
user.profile.save()
|
||||
ret = super().form_valid(form)
|
||||
|
||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||
# Kfet membership.
|
||||
if soge:
|
||||
kfet = Club.objects.get(name="Kfet")
|
||||
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||
|
||||
@ -560,20 +572,23 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
date_end__gte=datetime.today(),
|
||||
)
|
||||
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=kfet,
|
||||
user=user,
|
||||
fee=kfet_fee,
|
||||
date_start=old_membership.get().date_end + timedelta(days=1)
|
||||
if old_membership.exists() else form.instance.date_start,
|
||||
)
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
if old_membership.exists():
|
||||
membership.roles.set(old_membership.get().roles.all())
|
||||
else:
|
||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||
membership.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||
@ -97,6 +99,35 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
|
||||
model = Note
|
||||
|
||||
|
||||
class ConsumerSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Nested Serializer for Consumers.
|
||||
return Alias, and the note Associated to it in
|
||||
"""
|
||||
note = serializers.SerializerMethodField()
|
||||
|
||||
email_confirmed = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = '__all__'
|
||||
|
||||
def get_note(self, obj):
|
||||
"""
|
||||
Display information about the associated note
|
||||
"""
|
||||
# If the user has no right to see the note, then we only display the note identifier
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
|
||||
print(obj.pk)
|
||||
return NotePolymorphicSerializer().to_representation(obj.note)
|
||||
return dict(id=obj.id)
|
||||
|
||||
def get_email_confirmed(self, obj):
|
||||
if isinstance(obj.note, NoteUser):
|
||||
return obj.note.user.profile.email_confirmed
|
||||
return True
|
||||
|
||||
|
||||
class TemplateCategorySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transaction templates.
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, \
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ def register_note_urls(router, path):
|
||||
"""
|
||||
router.register(path + '/note', NotePolymorphicViewSet)
|
||||
router.register(path + '/alias', AliasViewSet)
|
||||
router.register(path + '/consumer', ConsumerViewSet)
|
||||
|
||||
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
||||
router.register(path + '/transaction/transaction', TransactionViewSet)
|
||||
|
@ -10,8 +10,8 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
|
||||
TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||
from ..models.notes import Note, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||
|
||||
@ -90,6 +90,30 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
queryset = Alias.objects.all()
|
||||
serializer_class = ConsumerSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
ordering_fields = ['name', 'normalized_name']
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested aliases
|
||||
"""
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
Q(name__regex="^" + alias)
|
||||
| Q(normalized_name__regex="^" + Alias.normalize(alias))
|
||||
| Q(normalized_name__regex="^" + alias.lower()))
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
|
@ -4,7 +4,7 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.inputs import Autocomplete, AmountInput
|
||||
|
||||
from .models import TransactionTemplate, NoteClub
|
||||
|
||||
@ -24,11 +24,6 @@ class TransactionTemplateForm(forms.ModelForm):
|
||||
model = TransactionTemplate
|
||||
fields = '__all__'
|
||||
|
||||
# Le champ de destination est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les aliases valides
|
||||
# Pour force le type d'une note, il faut rajouter le paramètre :
|
||||
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
|
||||
widgets = {
|
||||
'destination':
|
||||
Autocomplete(
|
||||
@ -41,4 +36,5 @@ class TransactionTemplateForm(forms.ModelForm):
|
||||
'placeholder': 'Note ...',
|
||||
},
|
||||
),
|
||||
'amount': AmountInput(),
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .notes import Note, NoteClub, NoteSpecial
|
||||
from ..templatetags.pretty_money import pretty_money
|
||||
|
||||
"""
|
||||
Defines transactions
|
||||
@ -198,6 +199,14 @@ class Transaction(PolymorphicModel):
|
||||
self.source.save()
|
||||
self.destination.save()
|
||||
|
||||
def delete(self, **kwargs):
|
||||
"""
|
||||
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
|
||||
"""
|
||||
self.valid = False
|
||||
self.save(**kwargs)
|
||||
super().delete(**kwargs)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.amount * self.quantity
|
||||
@ -206,6 +215,10 @@ class Transaction(PolymorphicModel):
|
||||
def type(self):
|
||||
return _('Transfer')
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
|
||||
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
|
||||
|
||||
|
||||
class RecurrentTransaction(Transaction):
|
||||
"""
|
||||
@ -214,8 +227,7 @@ class RecurrentTransaction(Transaction):
|
||||
|
||||
template = models.ForeignKey(
|
||||
TransactionTemplate,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
category = models.ForeignKey(
|
||||
TemplateCategory,
|
||||
|
@ -10,14 +10,14 @@ def save_user_note(instance, raw, **_kwargs):
|
||||
# When provisionning data, do not try to autocreate
|
||||
return
|
||||
|
||||
if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active:
|
||||
if instance.is_superuser or instance.profile.registration_valid:
|
||||
# Create note only when the registration is validated
|
||||
from note.models import NoteUser
|
||||
NoteUser.objects.get_or_create(user=instance)
|
||||
instance.note.save()
|
||||
|
||||
|
||||
def save_club_note(instance, created, raw, **_kwargs):
|
||||
def save_club_note(instance, raw, **_kwargs):
|
||||
"""
|
||||
Hook to create and save a note when a club is updated
|
||||
"""
|
||||
@ -25,7 +25,6 @@ def save_club_note(instance, created, raw, **_kwargs):
|
||||
# When provisionning data, do not try to autocreate
|
||||
return
|
||||
|
||||
if created:
|
||||
from .models import NoteClub
|
||||
NoteClub.objects.create(club=instance)
|
||||
from .models import NoteClub
|
||||
NoteClub.objects.get_or_create(club=instance)
|
||||
instance.note.save()
|
||||
|
@ -55,7 +55,7 @@ class HistoryTable(tables.Table):
|
||||
"class": lambda record: str(record.valid).lower() + ' validate',
|
||||
"data-toggle": "tooltip",
|
||||
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"),
|
||||
"onclick": lambda record: 'in_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
|
||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
|
||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||
+ str(record.id) + '").show();$("#invalidity_reason_'
|
||||
+ str(record.id) + '").focus();',
|
||||
@ -129,13 +129,14 @@ class ButtonTable(tables.Table):
|
||||
'table table-bordered condensed table-hover'
|
||||
}
|
||||
row_attrs = {
|
||||
'class': lambda record: 'table-row ' + 'table-success' if record.display else 'table-danger',
|
||||
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
model = TransactionTemplate
|
||||
exclude = ('id',)
|
||||
order_by = ('type', '-display', 'destination__name', 'name',)
|
||||
|
||||
edit = tables.LinkColumn('note:template_update',
|
||||
args=[A('pk')],
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
@ -30,7 +31,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
||||
table_class = HistoryTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
|
||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
@ -80,6 +81,33 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
|
||||
form_class = TransactionTemplateForm
|
||||
success_url = reverse_lazy('note:template_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if "logs" in settings.INSTALLED_APPS:
|
||||
from logs.models import Changelog
|
||||
update_logs = Changelog.objects.filter(
|
||||
model=ContentType.objects.get_for_model(TransactionTemplate),
|
||||
instance_pk=self.object.pk,
|
||||
action="edit",
|
||||
)
|
||||
price_history = []
|
||||
for log in update_logs.all():
|
||||
old_dict = json.loads(log.previous)
|
||||
new_dict = json.loads(log.data)
|
||||
old_price = old_dict["amount"]
|
||||
new_price = new_dict["amount"]
|
||||
if old_price != new_price:
|
||||
price_history.append(dict(price=old_price, time=log.timestamp))
|
||||
|
||||
price_history.append(dict(price=self.object.amount, time=None))
|
||||
|
||||
price_history.reverse()
|
||||
|
||||
context["price_history"] = price_history
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
@ -93,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
table_class = HistoryTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
|
||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
|
@ -8,6 +8,7 @@ from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q, F
|
||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
||||
from note_kfet import settings
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from member.models import Membership, Club
|
||||
|
||||
@ -36,13 +37,15 @@ class PermissionBackend(ModelBackend):
|
||||
# Unauthenticated users have no permissions
|
||||
return Permission.objects.none()
|
||||
|
||||
return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
|
||||
.filter(
|
||||
rolepermissions__role__membership__user=user,
|
||||
rolepermissions__role__membership__date_start__lte=datetime.date.today(),
|
||||
rolepermissions__role__membership__date_end__gte=datetime.date.today(),
|
||||
type=t,
|
||||
mask__rank__lte=get_current_session().get("permission_mask", 0),
|
||||
return Permission.objects.annotate(
|
||||
club=F("rolepermissions__role__membership__club"),
|
||||
membership=F("rolepermissions__role__membership"),
|
||||
).filter(
|
||||
rolepermissions__role__membership__user=user,
|
||||
rolepermissions__role__membership__date_start__lte=datetime.date.today(),
|
||||
rolepermissions__role__membership__date_end__gte=datetime.date.today(),
|
||||
type=t,
|
||||
mask__rank__lte=get_current_session().get("permission_mask", 0),
|
||||
).distinct()
|
||||
|
||||
@staticmethod
|
||||
@ -55,6 +58,7 @@ class PermissionBackend(ModelBackend):
|
||||
:return: A generator of the requested permissions
|
||||
"""
|
||||
clubs = {}
|
||||
memberships = {}
|
||||
|
||||
for permission in PermissionBackend.get_raw_permissions(user, type):
|
||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
|
||||
@ -64,9 +68,16 @@ class PermissionBackend(ModelBackend):
|
||||
clubs[permission.club] = club = Club.objects.get(pk=permission.club)
|
||||
else:
|
||||
club = clubs[permission.club]
|
||||
|
||||
if permission.membership not in memberships:
|
||||
memberships[permission.membership] = membership = Membership.objects.get(pk=permission.membership)
|
||||
else:
|
||||
membership = memberships[permission.membership]
|
||||
|
||||
permission = permission.about(
|
||||
user=user,
|
||||
club=club,
|
||||
membership=membership,
|
||||
User=User,
|
||||
Club=Club,
|
||||
Membership=Membership,
|
||||
@ -75,7 +86,9 @@ class PermissionBackend(ModelBackend):
|
||||
NoteClub=NoteClub,
|
||||
NoteSpecial=NoteSpecial,
|
||||
F=F,
|
||||
Q=Q
|
||||
Q=Q,
|
||||
now=datetime.datetime.now(),
|
||||
today=datetime.date.today(),
|
||||
)
|
||||
yield permission
|
||||
|
||||
@ -95,7 +108,7 @@ class PermissionBackend(ModelBackend):
|
||||
# Anonymous users can't do anything
|
||||
return Q(pk=-1)
|
||||
|
||||
if user.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
|
||||
if user.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
||||
# Superusers have all rights
|
||||
return Q()
|
||||
|
||||
@ -129,9 +142,9 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
sess = get_current_session()
|
||||
if sess is not None and sess.session_key is None:
|
||||
return Permission.objects.none()
|
||||
return False
|
||||
|
||||
if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
|
||||
if user_obj.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
||||
return True
|
||||
|
||||
if obj is None:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -120,7 +120,12 @@ class Permission(models.Model):
|
||||
('delete', 'delete')
|
||||
]
|
||||
|
||||
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
|
||||
model = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+',
|
||||
verbose_name=_("model"),
|
||||
)
|
||||
|
||||
# A json encoded Q object with the following grammar
|
||||
# query -> [] | {} (the empty query representing all objects)
|
||||
@ -142,18 +147,34 @@ class Permission(models.Model):
|
||||
# Examples:
|
||||
# Q(is_superuser=True) := {"is_superuser": true}
|
||||
# ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
|
||||
query = models.TextField()
|
||||
query = models.TextField(
|
||||
verbose_name=_("query"),
|
||||
)
|
||||
|
||||
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
|
||||
type = models.CharField(
|
||||
max_length=15,
|
||||
choices=PERMISSION_TYPES,
|
||||
verbose_name=_("type"),
|
||||
)
|
||||
|
||||
mask = models.ForeignKey(
|
||||
PermissionMask,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="permissions",
|
||||
verbose_name=_("mask"),
|
||||
)
|
||||
|
||||
field = models.CharField(max_length=255, blank=True)
|
||||
field = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("field"),
|
||||
)
|
||||
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('model', 'query', 'type', 'field')
|
||||
@ -277,24 +298,22 @@ class Permission(models.Model):
|
||||
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.field:
|
||||
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
||||
else:
|
||||
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
||||
return self.description
|
||||
|
||||
|
||||
class RolePermissions(models.Model):
|
||||
"""
|
||||
Permissions associated with a Role
|
||||
"""
|
||||
role = models.ForeignKey(
|
||||
role = models.OneToOneField(
|
||||
Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
related_name='permissions',
|
||||
verbose_name=_('role'),
|
||||
)
|
||||
permissions = models.ManyToManyField(
|
||||
Permission,
|
||||
verbose_name=_("permissions"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
@ -57,13 +58,19 @@ def pre_save_object(sender, instance, **kwargs):
|
||||
if old_value == new_value:
|
||||
continue
|
||||
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to change the field {field} on this instance of model"
|
||||
" {app_label}.{model_name}.")
|
||||
.format(field=field_name, app_label=app_label, model_name=model_name, )
|
||||
)
|
||||
else:
|
||||
# We check if the user has right to add the object
|
||||
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
|
||||
|
||||
if not has_perm:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to add this instance of model {app_label}.{model_name}.")
|
||||
.format(app_label=app_label, model_name=model_name, ))
|
||||
|
||||
|
||||
def pre_delete_object(instance, **kwargs):
|
||||
@ -88,4 +95,6 @@ def pre_delete_object(instance, **kwargs):
|
||||
|
||||
# We check if the user has rights to delete the object
|
||||
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
|
||||
.format(app_label=app_label, model_name=model_name))
|
||||
|
10
apps/permission/urls.py
Normal file
10
apps/permission/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
from permission.views import RightsView
|
||||
|
||||
app_name = 'permission'
|
||||
urlpatterns = [
|
||||
path('rights', RightsView.as_view(), name="rights"),
|
||||
]
|
@ -1,11 +1,60 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import date
|
||||
|
||||
from permission.backends import PermissionBackend
|
||||
from django.forms import HiddenInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import UpdateView, TemplateView
|
||||
from member.models import Role, Membership
|
||||
|
||||
from .backends import PermissionBackend
|
||||
|
||||
|
||||
class ProtectQuerysetMixin:
|
||||
"""
|
||||
This is a View class decorator and not a proper View class.
|
||||
Ensure that the user has the right to see or update objects.
|
||||
Display 404 error if the user can't see an object, remove the fields the user can't
|
||||
update on an update form (useful if the user can't change only specified fields).
|
||||
"""
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
|
||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
|
||||
if not isinstance(self, UpdateView):
|
||||
return form
|
||||
|
||||
# If we are in an UpdateView, we display only the fields the user has right to see.
|
||||
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
|
||||
# a custom request.
|
||||
# We could also delete the field, but some views might be affected.
|
||||
for key in form.base_fields:
|
||||
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object):
|
||||
form.fields[key].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class RightsView(TemplateView):
|
||||
template_name = "permission/all_rights.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = _("All rights")
|
||||
roles = Role.objects.all()
|
||||
context["roles"] = roles
|
||||
if self.request.user.is_authenticated:
|
||||
active_memberships = Membership.objects.filter(user=self.request.user,
|
||||
date_start__lte=date.today(),
|
||||
date_end__gte=date.today()).all()
|
||||
else:
|
||||
active_memberships = Membership.objects.none()
|
||||
|
||||
for role in roles:
|
||||
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
|
||||
|
||||
return context
|
||||
|
@ -27,6 +27,15 @@ class SignUpForm(UserCreationForm):
|
||||
fields = ('first_name', 'last_name', 'username', 'email', )
|
||||
|
||||
|
||||
class WEISignupForm(forms.Form):
|
||||
wei_registration = forms.BooleanField(
|
||||
label=_("Register to the WEI"),
|
||||
required=False,
|
||||
help_text=_("Check this case if you want to register to the WEI. If you hesitate, you will be able to register"
|
||||
" later, after validating your account in the Kfet."),
|
||||
)
|
||||
|
||||
|
||||
class ValidationForm(forms.Form):
|
||||
"""
|
||||
Validate the inscription of the new users and pay memberships.
|
||||
|
@ -11,12 +11,12 @@ from django.urls import reverse_lazy
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, TemplateView, DetailView, FormView
|
||||
from django.views.generic import CreateView, TemplateView, DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2 import SingleTableView
|
||||
from member.forms import ProfileForm
|
||||
from member.models import Membership, Club, Role
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
@ -32,13 +32,13 @@ class UserCreateView(CreateView):
|
||||
"""
|
||||
|
||||
form_class = SignUpForm
|
||||
success_url = reverse_lazy('registration:email_validation_sent')
|
||||
template_name = 'registration/signup.html'
|
||||
second_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["profile_form"] = self.second_form()
|
||||
del context["profile_form"].fields["section"]
|
||||
|
||||
return context
|
||||
|
||||
@ -67,6 +67,9 @@ class UserCreateView(CreateView):
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('registration:email_validation_sent')
|
||||
|
||||
|
||||
class UserValidateView(TemplateView):
|
||||
"""
|
||||
@ -112,7 +115,7 @@ class UserValidateView(TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['user'] = self.get_user(self.kwargs["uidb64"])
|
||||
context['user_object'] = self.get_user(self.kwargs["uidb64"])
|
||||
context['login_url'] = resolve_url(settings.LOGIN_URL)
|
||||
if self.validlink:
|
||||
context['validlink'] = True
|
||||
@ -263,17 +266,17 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
fee += kfet_fee
|
||||
|
||||
if soge:
|
||||
# Fill payment information if Société Générale pays the inscription
|
||||
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire")
|
||||
credit_amount = fee
|
||||
bank = "Société générale"
|
||||
# If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
||||
# and credit the note later.
|
||||
credit_type = None
|
||||
|
||||
print("OK")
|
||||
if credit_type is None:
|
||||
credit_amount = 0
|
||||
|
||||
if join_Kfet and not join_BDE:
|
||||
form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club."))
|
||||
|
||||
if fee > credit_amount:
|
||||
if fee > credit_amount and not soge:
|
||||
# Check if the user credits enough money
|
||||
form.add_error('credit_type',
|
||||
_("The entered amount is not enough for the memberships, should be at least {}")
|
||||
@ -295,10 +298,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
ret = super().form_valid(form)
|
||||
user.is_active = user.profile.email_confirmed or user.is_superuser
|
||||
user.profile.registration_valid = True
|
||||
# Store if Société générale paid for next years
|
||||
user.profile.soge = soge
|
||||
user.save()
|
||||
user.profile.save()
|
||||
user.refresh_from_db()
|
||||
|
||||
if credit_type is not None and credit_amount > 0:
|
||||
# Credit the note
|
||||
@ -316,21 +318,29 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
|
||||
if join_BDE:
|
||||
# Create membership for the user to the BDE starting today
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=bde,
|
||||
user=user,
|
||||
fee=bde_fee,
|
||||
)
|
||||
if soge:
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||
membership.save()
|
||||
|
||||
if join_Kfet:
|
||||
# Create membership for the user to the Kfet starting today
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=kfet,
|
||||
user=user,
|
||||
fee=kfet_fee,
|
||||
)
|
||||
if soge:
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||
membership.save()
|
||||
|
||||
|
Submodule apps/scripts updated: b9db26fa49...f0aa426950
@ -3,7 +3,7 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import RemittanceType, Remittance
|
||||
from .models import RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
@admin.register(RemittanceType)
|
||||
@ -25,3 +25,6 @@ class RemittanceAdmin(admin.ModelAdmin):
|
||||
if not obj:
|
||||
return True
|
||||
return not obj.closed and super().has_change_permission(request, obj)
|
||||
|
||||
|
||||
admin.site.register(SogeCredit)
|
||||
|
@ -4,7 +4,7 @@
|
||||
from rest_framework import serializers
|
||||
from note.api.serializers import SpecialTransactionSerializer
|
||||
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
@ -60,3 +60,14 @@ class RemittanceSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_transactions(self, obj):
|
||||
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions)
|
||||
|
||||
|
||||
class SogeCreditSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for SogeCredit types.
|
||||
The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SogeCredit
|
||||
fields = '__all__'
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet
|
||||
from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, SogeCreditViewSet
|
||||
|
||||
|
||||
def register_treasury_urls(router, path):
|
||||
@ -12,3 +12,4 @@ def register_treasury_urls(router, path):
|
||||
router.register(path + '/product', ProductViewSet)
|
||||
router.register(path + '/remittance_type', RemittanceTypeViewSet)
|
||||
router.register(path + '/remittance', RemittanceViewSet)
|
||||
router.register(path + '/soge_credit', SogeCreditViewSet)
|
||||
|
@ -5,8 +5,9 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer,\
|
||||
SogeCreditSerializer
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||
@ -39,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
||||
then render it on /api/treasury/remittance_type/
|
||||
"""
|
||||
queryset = RemittanceType.objects.all()
|
||||
queryset = RemittanceType.objects
|
||||
serializer_class = RemittanceTypeSerializer
|
||||
|
||||
|
||||
@ -49,5 +50,15 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/remittance/
|
||||
"""
|
||||
queryset = Remittance.objects.all()
|
||||
queryset = Remittance.objects
|
||||
serializer_class = RemittanceSerializer
|
||||
|
||||
|
||||
class SogeCreditViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/soge_credit/
|
||||
"""
|
||||
queryset = SogeCredit.objects
|
||||
serializer_class = SogeCreditSerializer
|
||||
|
@ -1,11 +1,13 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, SpecialTransaction
|
||||
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
@ -207,3 +209,101 @@ class SpecialTransactionProxy(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("special transaction proxy")
|
||||
verbose_name_plural = _("special transaction proxies")
|
||||
|
||||
|
||||
class SogeCredit(models.Model):
|
||||
"""
|
||||
Manage the credits from the Société générale.
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
transactions = models.ManyToManyField(
|
||||
MembershipTransaction,
|
||||
related_name="+",
|
||||
verbose_name=_("membership transactions"),
|
||||
)
|
||||
|
||||
credit_transaction = models.OneToOneField(
|
||||
SpecialTransaction,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("credit transaction"),
|
||||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self.credit_transaction is not None
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
return sum(transaction.total for transaction in self.transactions.all())
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Invalidating a Société générale delete the transaction of the bank if it was already created.
|
||||
Treasurers must know what they do, With Great Power Comes Great Responsibility...
|
||||
"""
|
||||
if self.valid:
|
||||
self.credit_transaction.valid = False
|
||||
self.credit_transaction._force_save = True
|
||||
self.credit_transaction.save()
|
||||
self.credit_transaction._force_delete = True
|
||||
self.credit_transaction.delete()
|
||||
self.credit_transaction = None
|
||||
for transaction in self.transactions.all():
|
||||
transaction.valid = False
|
||||
transaction._force_save = True
|
||||
transaction.save()
|
||||
|
||||
def validate(self, force=False):
|
||||
if self.valid and not force:
|
||||
# The credit is already done
|
||||
return
|
||||
|
||||
# First invalidate all transaction and delete the credit if already did (and force mode)
|
||||
self.invalidate()
|
||||
self.credit_transaction = SpecialTransaction.objects.create(
|
||||
source=NoteSpecial.objects.get(special_type="Virement bancaire"),
|
||||
destination=self.user.note,
|
||||
quantity=1,
|
||||
amount=self.amount,
|
||||
reason="Crédit société générale",
|
||||
last_name=self.user.last_name,
|
||||
first_name=self.user.first_name,
|
||||
bank="Société générale",
|
||||
)
|
||||
self.save()
|
||||
|
||||
for transaction in self.transactions.all():
|
||||
transaction.valid = True
|
||||
transaction._force_save = True
|
||||
transaction.created_at = datetime.now()
|
||||
transaction.save()
|
||||
|
||||
def delete(self, **kwargs):
|
||||
"""
|
||||
Deleting a SogeCredit is equivalent to say that the Société générale didn't pay.
|
||||
Treasurers must know what they do, this is difficult to undo this operation.
|
||||
With Great Power Comes Great Responsibility...
|
||||
"""
|
||||
|
||||
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."))
|
||||
|
||||
self.invalidate()
|
||||
for transaction in self.transactions.all():
|
||||
transaction._force_save = True
|
||||
transaction.valid = True
|
||||
transaction.created_at = datetime.now()
|
||||
transaction.save()
|
||||
super().delete(**kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Credit from the Société générale")
|
||||
verbose_name_plural = _("Credits from the Société générale")
|
||||
|
@ -7,7 +7,7 @@ from django_tables2 import A
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
|
||||
from .models import Invoice, Remittance
|
||||
from .models import Invoice, Remittance, SogeCredit
|
||||
|
||||
|
||||
class InvoiceTable(tables.Table):
|
||||
@ -101,3 +101,28 @@ class SpecialTransactionTable(tables.Table):
|
||||
model = SpecialTransaction
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
|
||||
|
||||
|
||||
class SogeCreditTable(tables.Table):
|
||||
user = tables.LinkColumn(
|
||||
'treasury:manage_soge_credit',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
amount = tables.Column(
|
||||
verbose_name=_("Amount"),
|
||||
)
|
||||
|
||||
valid = tables.Column(
|
||||
verbose_name=_("Valid"),
|
||||
)
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
def render_valid(self, value):
|
||||
return _("Yes") if value else _("No")
|
||||
|
||||
class Meta:
|
||||
model = SogeCredit
|
||||
fields = ('user', 'amount', 'valid', )
|
||||
|
@ -4,7 +4,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\
|
||||
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView
|
||||
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView,\
|
||||
SogeCreditListView, SogeCreditManageView
|
||||
|
||||
app_name = 'treasury'
|
||||
urlpatterns = [
|
||||
@ -21,4 +22,7 @@ urlpatterns = [
|
||||
path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'),
|
||||
path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(),
|
||||
name='unlink_transaction'),
|
||||
|
||||
path('soge-credits/list/', SogeCreditListView.as_view(), name='soge_credits'),
|
||||
path('soge-credits/manage/<int:pk>/', SogeCreditManageView.as_view(), name='manage_soge_credit'),
|
||||
]
|
||||
|
@ -10,21 +10,23 @@ from crispy_forms.helper import FormHelper
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms import Form
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.views.generic.base import View, TemplateView
|
||||
from django.views.generic.edit import BaseFormView
|
||||
from django_tables2 import SingleTableView
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from note.models import SpecialTransaction, NoteSpecial, Alias
|
||||
from note_kfet.settings.base import BASE_DIR
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
|
||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
|
||||
|
||||
|
||||
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
@ -180,7 +182,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
||||
# Display the generated pdf as a HTTP Response
|
||||
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk)
|
||||
response['Content-Disposition'] = "inline;filename=Facture%20n°{:d}.pdf".format(pk)
|
||||
except IOError as e:
|
||||
raise e
|
||||
finally:
|
||||
@ -203,9 +205,9 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["table"] = RemittanceTable(data=Remittance.objects
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
|
||||
.all())
|
||||
context["table"] = RemittanceTable(
|
||||
data=Remittance.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
|
||||
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
||||
|
||||
return context
|
||||
@ -307,3 +309,61 @@ class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
|
||||
transaction.save()
|
||||
|
||||
return redirect('treasury:remittance_list')
|
||||
|
||||
|
||||
class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
|
||||
"""
|
||||
List all Société Générale credits
|
||||
"""
|
||||
model = SogeCredit
|
||||
table_class = SogeCreditTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Filter the table with the given parameter.
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
if "valid" in self.request.GET:
|
||||
q = Q(credit_transaction=None)
|
||||
if not self.request.GET["valid"]:
|
||||
q = ~q
|
||||
qs = qs.filter(q)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
|
||||
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
|
||||
"""
|
||||
Manage credits from the Société générale.
|
||||
"""
|
||||
model = SogeCredit
|
||||
form_class = Form
|
||||
|
||||
def form_valid(self, form):
|
||||
if "validate" in form.data:
|
||||
self.get_object().validate(True)
|
||||
elif "delete" in form.data:
|
||||
self.get_object().delete()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
if "validate" in self.request.POST:
|
||||
return reverse_lazy('treasury:manage_soge_credit', args=(self.get_object().pk,))
|
||||
return reverse_lazy('treasury:soge_credits')
|
||||
|
4
apps/wei/__init__.py
Normal file
4
apps/wei/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'wei.apps.WeiConfig'
|
13
apps/wei/admin.py
Normal file
13
apps/wei/admin.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
|
||||
|
||||
admin.site.register(WEIClub)
|
||||
admin.site.register(WEIRegistration)
|
||||
admin.site.register(WEIMembership)
|
||||
admin.site.register(WEIRole)
|
||||
admin.site.register(Bus)
|
||||
admin.site.register(BusTeam)
|
0
apps/wei/api/__init__.py
Normal file
0
apps/wei/api/__init__.py
Normal file
72
apps/wei/api/serializers.py
Normal file
72
apps/wei/api/serializers.py
Normal file
@ -0,0 +1,72 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
|
||||
|
||||
|
||||
class WEIClubSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Clubs.
|
||||
The djangorestframework plugin will analyse the model `WEIClub` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIClub
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BusSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Bus.
|
||||
The djangorestframework plugin will analyse the model `Bus` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Bus
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BusTeamSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Bus teams.
|
||||
The djangorestframework plugin will analyse the model `BusTeam` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = BusTeam
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIRoleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI roles.
|
||||
The djangorestframework plugin will analyse the model `WEIRole` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIRole
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIRegistrationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI registrations.
|
||||
The djangorestframework plugin will analyse the model `WEIRegistration` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIRegistration
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIMembershipSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI memberships.
|
||||
The djangorestframework plugin will analyse the model `WEIMembership` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIMembership
|
||||
fields = '__all__'
|
17
apps/wei/api/urls.py
Normal file
17
apps/wei/api/urls.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import WEIClubViewSet, BusViewSet, BusTeamViewSet, WEIRoleViewSet, WEIRegistrationViewSet, \
|
||||
WEIMembershipViewSet
|
||||
|
||||
|
||||
def register_wei_urls(router, path):
|
||||
"""
|
||||
Configure router for Member REST API.
|
||||
"""
|
||||
router.register(path + '/club', WEIClubViewSet)
|
||||
router.register(path + '/bus', BusViewSet)
|
||||
router.register(path + '/team', BusTeamViewSet)
|
||||
router.register(path + '/role', WEIRoleViewSet)
|
||||
router.register(path + '/registration', WEIRegistrationViewSet)
|
||||
router.register(path + '/membership', WEIMembershipViewSet)
|
86
apps/wei/api/views.py
Normal file
86
apps/wei/api/views.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
|
||||
WEIRegistrationSerializer, WEIMembershipSerializer
|
||||
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
|
||||
|
||||
|
||||
class WEIClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/club/
|
||||
"""
|
||||
queryset = WEIClub.objects.all()
|
||||
serializer_class = WEIClubSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'year', ]
|
||||
|
||||
|
||||
class BusViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/bus/
|
||||
"""
|
||||
queryset = Bus.objects
|
||||
serializer_class = BusSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'wei', ]
|
||||
|
||||
|
||||
class BusTeamViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/team/
|
||||
"""
|
||||
queryset = BusTeam.objects
|
||||
serializer_class = BusTeamSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'bus', 'bus__wei', ]
|
||||
|
||||
|
||||
class WEIRoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/role/
|
||||
"""
|
||||
queryset = WEIRole.objects
|
||||
serializer_class = WEIRoleSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class WEIRegistrationViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/registration/
|
||||
"""
|
||||
queryset = WEIRegistration.objects
|
||||
serializer_class = WEIRegistrationSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$user__username', ]
|
||||
filterset_fields = ['user', 'wei', ]
|
||||
|
||||
|
||||
class WEIMembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/membership/
|
||||
"""
|
||||
queryset = WEIMembership.objects
|
||||
serializer_class = WEIMembershipSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$user__username', '$bus__name', '$team__name', ]
|
||||
filterset_fields = ['user', 'club', 'bus', 'team', ]
|
10
apps/wei/apps.py
Normal file
10
apps/wei/apps.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class WeiConfig(AppConfig):
|
||||
name = 'wei'
|
||||
verbose_name = _('WEI')
|
10
apps/wei/forms/__init__.py
Normal file
10
apps/wei/forms/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .registration import WEIForm, WEIRegistrationForm, WEIMembershipForm, BusForm, BusTeamForm
|
||||
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
|
||||
|
||||
__all__ = [
|
||||
'WEIForm', 'WEIRegistrationForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
124
apps/wei/forms/registration.py
Normal file
124
apps/wei/forms/registration.py
Normal file
@ -0,0 +1,124 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
||||
|
||||
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
||||
|
||||
|
||||
class WEIForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WEIClub
|
||||
exclude = ('parent_club', 'require_memberships', 'membership_duration', )
|
||||
widgets = {
|
||||
"membership_fee_paid": AmountInput(),
|
||||
"membership_fee_unpaid": AmountInput(),
|
||||
"membership_start": DatePickerInput(),
|
||||
"membership_end": DatePickerInput(),
|
||||
"date_start": DatePickerInput(),
|
||||
"date_end": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class WEIRegistrationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WEIRegistration
|
||||
exclude = ('wei', )
|
||||
widgets = {
|
||||
"user": Autocomplete(
|
||||
User,
|
||||
attrs={
|
||||
'api_url': '/api/user/',
|
||||
'name_field': 'username',
|
||||
'placeholder': 'Nom ...',
|
||||
},
|
||||
),
|
||||
"birth_date": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class WEIChooseBusForm(forms.Form):
|
||||
bus = forms.ModelMultipleChoiceField(
|
||||
queryset=Bus.objects,
|
||||
label=_("bus"),
|
||||
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
|
||||
+ " in particular if you are a free eletron."),
|
||||
)
|
||||
|
||||
team = forms.ModelMultipleChoiceField(
|
||||
queryset=BusTeam.objects,
|
||||
label=_("Team"),
|
||||
required=False,
|
||||
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
|
||||
)
|
||||
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=WEIRole.objects.filter(~Q(name="1A")),
|
||||
label=_("WEI Roles"),
|
||||
help_text=_("Select the roles that you are interested in."),
|
||||
)
|
||||
|
||||
|
||||
class WEIMembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects, label=_("WEI Roles"))
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]:
|
||||
self.add_error('bus', _("This team doesn't belong to the given bus."))
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = WEIMembership
|
||||
fields = ('roles', 'bus', 'team',)
|
||||
widgets = {
|
||||
"bus": Autocomplete(
|
||||
Bus,
|
||||
attrs={
|
||||
'api_url': '/api/wei/bus/',
|
||||
'placeholder': 'Bus ...',
|
||||
}
|
||||
),
|
||||
"team": Autocomplete(
|
||||
BusTeam,
|
||||
attrs={
|
||||
'api_url': '/api/wei/team/',
|
||||
'placeholder': 'Équipe ...',
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bus
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
"wei": Autocomplete(
|
||||
WEIClub,
|
||||
attrs={
|
||||
'api_url': '/api/wei/club/',
|
||||
'placeholder': 'WEI ...',
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BusTeamForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BusTeam
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
"bus": Autocomplete(
|
||||
Bus,
|
||||
attrs={
|
||||
'api_url': '/api/wei/bus/',
|
||||
'placeholder': 'Bus ...',
|
||||
},
|
||||
),
|
||||
"color": ColorWidget(),
|
||||
}
|
12
apps/wei/forms/surveys/__init__.py
Normal file
12
apps/wei/forms/surveys/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from .wei2020 import WEISurvey2020
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
||||
|
||||
CurrentSurvey = WEISurvey2020
|
189
apps/wei/forms/surveys/base.py
Normal file
189
apps/wei/forms/surveys/base.py
Normal file
@ -0,0 +1,189 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Form
|
||||
|
||||
from ...models import WEIClub, WEIRegistration, Bus
|
||||
|
||||
|
||||
class WEISurveyInformation:
|
||||
"""
|
||||
Abstract data of the survey.
|
||||
"""
|
||||
valid = False
|
||||
selected_bus_pk = None
|
||||
selected_bus_name = None
|
||||
|
||||
def __init__(self, registration):
|
||||
self.__dict__.update(registration.information)
|
||||
|
||||
def get_selected_bus(self) -> Optional[Bus]:
|
||||
"""
|
||||
If the algorithm ran, return the prefered bus according to the survey.
|
||||
In the other case, return None.
|
||||
"""
|
||||
if not self.valid:
|
||||
return None
|
||||
return Bus.objects.get(pk=self.selected_bus_pk)
|
||||
|
||||
def save(self, registration) -> None:
|
||||
"""
|
||||
Store the data of the survey into the database, with the information of the registration.
|
||||
"""
|
||||
registration.information = self.__dict__
|
||||
|
||||
|
||||
class WEIBusInformation:
|
||||
"""
|
||||
Abstract data of the bus.
|
||||
"""
|
||||
|
||||
def __init__(self, bus: Bus):
|
||||
self.__dict__.update(bus.information)
|
||||
self.bus = bus
|
||||
|
||||
|
||||
class WEISurveyAlgorithm:
|
||||
"""
|
||||
Abstract algorithm that attributes a bus to each new member.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_survey_class(cls):
|
||||
"""
|
||||
The class of the survey associated with this algorithm.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_bus_information_class(cls):
|
||||
"""
|
||||
The class of the information associated to a bus extending WEIBusInformation.
|
||||
Default: WEIBusInformation (contains nothing)
|
||||
"""
|
||||
return WEIBusInformation
|
||||
|
||||
@classmethod
|
||||
def get_registrations(cls) -> QuerySet:
|
||||
"""
|
||||
Queryset of all first year registrations
|
||||
"""
|
||||
return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True)
|
||||
|
||||
@classmethod
|
||||
def get_buses(cls) -> QuerySet:
|
||||
"""
|
||||
Queryset of all buses of the associated wei.
|
||||
"""
|
||||
return Bus.objects.filter(wei__year=cls.get_survey_class().get_year())
|
||||
|
||||
@classmethod
|
||||
def get_bus_information(cls, bus):
|
||||
"""
|
||||
Return the WEIBusInformation object containing the data stored in a given bus.
|
||||
"""
|
||||
return cls.get_bus_information_class()(bus)
|
||||
|
||||
def run_algorithm(self) -> None:
|
||||
"""
|
||||
Once this method implemented, run the algorithm that attributes a bus to each first year member.
|
||||
This method can be run in command line through ``python manage.py wei_algorithm``
|
||||
See ``wei.management.commmands.wei_algorithm``
|
||||
This method must call Survey.select_bus for each survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WEISurvey:
|
||||
"""
|
||||
Survey associated to a first year WEI registration.
|
||||
The data is stored into WEISurveyInformation, this class acts as a manager.
|
||||
This is an abstract class: this has to be extended each year to implement custom methods.
|
||||
"""
|
||||
|
||||
def __init__(self, registration: WEIRegistration):
|
||||
self.registration = registration
|
||||
self.information = self.get_survey_information_class()(registration)
|
||||
|
||||
@classmethod
|
||||
def get_year(cls) -> int:
|
||||
"""
|
||||
Get year of the wei concerned by the type of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_wei(cls) -> WEIClub:
|
||||
"""
|
||||
The WEI associated to this kind of survey.
|
||||
"""
|
||||
return WEIClub.objects.get(year=cls.get_year())
|
||||
|
||||
@classmethod
|
||||
def get_survey_information_class(cls):
|
||||
"""
|
||||
The class of the data (extending WEISurveyInformation).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
"""
|
||||
The form class of the survey.
|
||||
This is proper to the status of the survey: the form class can evolve according to the progress of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_form(self, form) -> None:
|
||||
"""
|
||||
Once the form is instanciated, the information can be updated with the information of the registration
|
||||
and the information of the survey.
|
||||
This method is called once the form is created.
|
||||
"""
|
||||
pass
|
||||
|
||||
def form_valid(self, form) -> None:
|
||||
"""
|
||||
Called when the information of the form are validated.
|
||||
This method should update the information of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""
|
||||
Return True if the survey is complete.
|
||||
If the survey is complete, then the button "Next" will display some text for the end of the survey.
|
||||
If not, the survey is reloaded and continues.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Store the information of the survey into the database.
|
||||
"""
|
||||
self.information.save(self.registration)
|
||||
# The information is forced-saved.
|
||||
# We don't want that anyone can update manually the information, so since most users don't have the
|
||||
# right to save the information of a registration, we force save.
|
||||
self.registration._force_save = True
|
||||
self.registration.save()
|
||||
|
||||
@classmethod
|
||||
def get_algorithm_class(cls):
|
||||
"""
|
||||
Algorithm class associated to the survey.
|
||||
The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member.
|
||||
The association is not permanent: that's only a suggestion.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def select_bus(self, bus) -> None:
|
||||
"""
|
||||
Set the suggestion into the data of the membership.
|
||||
:param bus: The bus suggested.
|
||||
"""
|
||||
self.information.selected_bus_pk = bus.pk
|
||||
self.information.selected_bus_name = bus.name
|
||||
self.information.valid = True
|
89
apps/wei/forms/surveys/wei2020.py
Normal file
89
apps/wei/forms/surveys/wei2020.py
Normal file
@ -0,0 +1,89 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import forms
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from ...models import Bus
|
||||
|
||||
|
||||
class WEISurveyForm2020(forms.Form):
|
||||
"""
|
||||
Survey form for the year 2020.
|
||||
For now, that's only a Bus selector.
|
||||
TODO: Do a better survey (later)
|
||||
"""
|
||||
bus = forms.ModelChoiceField(
|
||||
Bus.objects,
|
||||
)
|
||||
|
||||
def set_registration(self, registration):
|
||||
"""
|
||||
Filter the bus selector with the buses of the current WEI.
|
||||
"""
|
||||
self.fields["bus"].queryset = Bus.objects.filter(wei=registration.wei)
|
||||
|
||||
|
||||
class WEISurveyInformation2020(WEISurveyInformation):
|
||||
"""
|
||||
We store the id of the selected bus. We store only the name, but is not used in the selection:
|
||||
that's only for humans that try to read data.
|
||||
"""
|
||||
chosen_bus_pk = None
|
||||
chosen_bus_name = None
|
||||
|
||||
|
||||
class WEISurvey2020(WEISurvey):
|
||||
"""
|
||||
Survey for the year 2020.
|
||||
"""
|
||||
@classmethod
|
||||
def get_year(cls):
|
||||
return 2020
|
||||
|
||||
@classmethod
|
||||
def get_survey_information_class(cls):
|
||||
return WEISurveyInformation2020
|
||||
|
||||
def get_form_class(self):
|
||||
return WEISurveyForm2020
|
||||
|
||||
def update_form(self, form):
|
||||
"""
|
||||
Filter the bus selector with the buses of the WEI.
|
||||
"""
|
||||
form.set_registration(self.registration)
|
||||
|
||||
def form_valid(self, form):
|
||||
bus = form.cleaned_data["bus"]
|
||||
self.information.chosen_bus_pk = bus.pk
|
||||
self.information.chosen_bus_name = bus.name
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def get_algorithm_class(cls):
|
||||
return WEISurveyAlgorithm2020
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""
|
||||
The survey is complete once the bus is chosen.
|
||||
"""
|
||||
return self.information.chosen_bus_pk is not None
|
||||
|
||||
|
||||
class WEISurveyAlgorithm2020(WEISurveyAlgorithm):
|
||||
"""
|
||||
The algorithm class for the year 2020.
|
||||
For now, the algorithm is quite simple: the selected bus is the chosen bus.
|
||||
TODO: Improve this algorithm.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_survey_class(cls):
|
||||
return WEISurvey2020
|
||||
|
||||
def run_algorithm(self):
|
||||
for registration in self.get_registrations():
|
||||
survey = self.get_survey_class()(registration)
|
||||
survey.select_bus(Bus.objects.get(pk=survey.information.chosen_bus_pk))
|
||||
survey.save()
|
0
apps/wei/migrations/__init__.py
Normal file
0
apps/wei/migrations/__init__.py
Normal file
329
apps/wei/models.py
Normal file
329
apps/wei/models.py
Normal file
@ -0,0 +1,329 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Role, Club, Membership
|
||||
from note.models import MembershipTransaction
|
||||
|
||||
|
||||
class WEIClub(Club):
|
||||
"""
|
||||
The WEI is a club. Register to the WEI is equivalent than be member of the club.
|
||||
"""
|
||||
year = models.PositiveIntegerField(
|
||||
unique=True,
|
||||
default=date.today().year,
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
date_start = models.DateField(
|
||||
verbose_name=_("date start"),
|
||||
)
|
||||
|
||||
date_end = models.DateField(
|
||||
verbose_name=_("date end"),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_current_wei(self):
|
||||
"""
|
||||
We consider that this is the current WEI iff there is no future WEI planned.
|
||||
"""
|
||||
return not WEIClub.objects.filter(date_start__gt=self.date_start).exists()
|
||||
|
||||
def update_membership_dates(self):
|
||||
"""
|
||||
We can't join the WEI next years.
|
||||
"""
|
||||
return
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI")
|
||||
verbose_name_plural = _("WEI")
|
||||
|
||||
|
||||
class Bus(models.Model):
|
||||
"""
|
||||
The best bus for the best WEI
|
||||
"""
|
||||
wei = models.ForeignKey(
|
||||
WEIClub,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="buses",
|
||||
verbose_name=_("WEI"),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
information_json = models.TextField(
|
||||
default="{}",
|
||||
verbose_name=_("survey information"),
|
||||
help_text=_("Information about the survey for new members, encoded in JSON"),
|
||||
)
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information about the survey for new members are stored in a dictionary that can evolve following the years.
|
||||
The dictionary is stored as a JSON string.
|
||||
"""
|
||||
return json.loads(self.information_json)
|
||||
|
||||
@information.setter
|
||||
def information(self, information):
|
||||
"""
|
||||
Store information as a JSON string
|
||||
"""
|
||||
self.information_json = json.dumps(information)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Bus")
|
||||
verbose_name_plural = _("Buses")
|
||||
unique_together = ('wei', 'name',)
|
||||
|
||||
|
||||
class BusTeam(models.Model):
|
||||
"""
|
||||
A bus has multiple teams
|
||||
"""
|
||||
bus = models.ForeignKey(
|
||||
Bus,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="teams",
|
||||
verbose_name=_("bus"),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
||||
verbose_name=_("color"),
|
||||
help_text=_("The color of the T-Shirt, stored with its number equivalent"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name + " (" + str(self.bus) + ")"
|
||||
|
||||
class Meta:
|
||||
unique_together = ('bus', 'name',)
|
||||
verbose_name = _("Bus team")
|
||||
verbose_name_plural = _("Bus teams")
|
||||
|
||||
|
||||
class WEIRole(Role):
|
||||
"""
|
||||
A Role for the WEI can be bus chief, team chief, free electron, ...
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI Role")
|
||||
verbose_name_plural = _("WEI Roles")
|
||||
|
||||
|
||||
class WEIRegistration(models.Model):
|
||||
"""
|
||||
Store personal data that can be useful for the WEI.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="wei",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
wei = models.ForeignKey(
|
||||
WEIClub,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="users",
|
||||
verbose_name=_("WEI"),
|
||||
)
|
||||
|
||||
soge_credit = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Credit from Société générale"),
|
||||
)
|
||||
|
||||
caution_check = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Caution check given")
|
||||
)
|
||||
|
||||
birth_date = models.DateField(
|
||||
verbose_name=_("birth date"),
|
||||
)
|
||||
|
||||
gender = models.CharField(
|
||||
max_length=16,
|
||||
choices=(
|
||||
('male', _("Male")),
|
||||
('female', _("Female")),
|
||||
('nonbinary', _("Non binary")),
|
||||
),
|
||||
verbose_name=_("gender"),
|
||||
)
|
||||
|
||||
health_issues = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("health issues"),
|
||||
)
|
||||
|
||||
emergency_contact_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("emergency contact name"),
|
||||
)
|
||||
|
||||
emergency_contact_phone = models.CharField(
|
||||
max_length=32,
|
||||
verbose_name=_("emergency contact phone"),
|
||||
)
|
||||
|
||||
ml_events_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
ml_sport_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
ml_art_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
first_year = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("first year"),
|
||||
help_text=_("Tells if the user is new in the school.")
|
||||
)
|
||||
|
||||
information_json = models.TextField(
|
||||
default="{}",
|
||||
verbose_name=_("registration information"),
|
||||
help_text=_("Information about the registration (buses for old members, survey fot the new members), "
|
||||
"encoded in JSON"),
|
||||
)
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information about the registration (the survey for the new members, the bus for the older members, ...)
|
||||
are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string.
|
||||
"""
|
||||
return json.loads(self.information_json)
|
||||
|
||||
@information.setter
|
||||
def information(self, information):
|
||||
"""
|
||||
Store information as a JSON string
|
||||
"""
|
||||
self.information_json = json.dumps(information)
|
||||
|
||||
@property
|
||||
def is_validated(self):
|
||||
try:
|
||||
return self.membership is not None
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'wei',)
|
||||
verbose_name = _("WEI User")
|
||||
verbose_name_plural = _("WEI Users")
|
||||
|
||||
|
||||
class WEIMembership(Membership):
|
||||
bus = models.ForeignKey(
|
||||
Bus,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("bus"),
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
BusTeam,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
registration = models.OneToOneField(
|
||||
WEIRegistration,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
related_name="membership",
|
||||
verbose_name=_("WEI registration"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI membership")
|
||||
verbose_name_plural = _("WEI memberships")
|
||||
|
||||
def make_transaction(self):
|
||||
"""
|
||||
Create Membership transaction associated to this membership.
|
||||
"""
|
||||
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
|
||||
return
|
||||
|
||||
if self.fee:
|
||||
transaction = MembershipTransaction(
|
||||
membership=self,
|
||||
source=self.user.note,
|
||||
destination=self.club.note,
|
||||
quantity=1,
|
||||
amount=self.fee,
|
||||
reason="Adhésion WEI " + self.club.name,
|
||||
valid=not self.registration.soge_credit # Soge transactions are by default invalidated
|
||||
)
|
||||
transaction._force_save = True
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS:
|
||||
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
|
||||
# to treasurers.
|
||||
transaction.refresh_from_db()
|
||||
from treasury.models import SogeCredit
|
||||
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
|
||||
soge_credit.refresh_from_db()
|
||||
transaction.save()
|
||||
soge_credit.transactions.add(transaction)
|
||||
soge_credit.save()
|
201
apps/wei/tables.py
Normal file
201
apps/wei/tables.py
Normal file
@ -0,0 +1,201 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2 import A
|
||||
|
||||
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
|
||||
|
||||
|
||||
class WEITable(tables.Table):
|
||||
"""
|
||||
List all WEI.
|
||||
"""
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIClub
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'year', 'date_start', 'date_end',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: reverse_lazy('wei:wei_detail', args=(record.pk,))
|
||||
}
|
||||
|
||||
|
||||
class WEIRegistrationTable(tables.Table):
|
||||
"""
|
||||
List all WEI registrations.
|
||||
"""
|
||||
user = tables.LinkColumn(
|
||||
'member:user_detail',
|
||||
args=[A('user.pk')],
|
||||
)
|
||||
|
||||
edit = tables.LinkColumn(
|
||||
'wei:wei_update_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("Edit"),
|
||||
text=_("Edit"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-warning'
|
||||
}
|
||||
}
|
||||
)
|
||||
validate = tables.LinkColumn(
|
||||
'wei:validate_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("Validate"),
|
||||
text=_("Validate"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-success'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
delete = tables.LinkColumn(
|
||||
'wei:wei_delete_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("delete"),
|
||||
text=_("Delete"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-danger'
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIRegistration
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'user.first_name', 'user.last_name', 'first_year',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
|
||||
class WEIMembershipTable(tables.Table):
|
||||
user = tables.LinkColumn(
|
||||
'wei:wei_update_registration',
|
||||
args=[A('registration.pk')],
|
||||
)
|
||||
|
||||
year = tables.Column(
|
||||
accessor=A("pk"),
|
||||
verbose_name=_("Year"),
|
||||
)
|
||||
|
||||
bus = tables.LinkColumn(
|
||||
'wei:manage_bus',
|
||||
args=[A('bus.pk')],
|
||||
)
|
||||
|
||||
team = tables.LinkColumn(
|
||||
'wei:manage_bus_team',
|
||||
args=[A('bus.pk')],
|
||||
)
|
||||
|
||||
def render_year(self, record):
|
||||
return str(record.user.profile.ens_year) + "A"
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIMembership
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'user.last_name', 'user.first_name', 'registration.gender', 'user.profile.department',
|
||||
'year', 'bus', 'team', )
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
}
|
||||
|
||||
|
||||
class BusTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'wei:manage_bus',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
teams = tables.Column(
|
||||
accessor=A("teams"),
|
||||
verbose_name=_("Teams"),
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-truncate",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
count = tables.Column(
|
||||
verbose_name=_("Members count"),
|
||||
)
|
||||
|
||||
def render_teams(self, value):
|
||||
return ", ".join(team.name for team in value.all())
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = Bus
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'teams', )
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
}
|
||||
|
||||
|
||||
class BusTeamTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'wei:manage_bus_team',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
color = tables.Column(
|
||||
attrs={
|
||||
"td": {
|
||||
"style": lambda record: "background-color: #{:06X}; color: #{:06X};"
|
||||
.format(record.color, 0xFFFFFF - record.color, )
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
|
||||
count = tables.Column(
|
||||
verbose_name=_("Members count"),
|
||||
)
|
||||
|
||||
def render_color(self, value):
|
||||
return "#{:06X}".format(value)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = BusTeam
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'color',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, ))
|
||||
}
|
43
apps/wei/urls.py
Normal file
43
apps/wei/urls.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\
|
||||
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView,\
|
||||
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\
|
||||
WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIDeleteRegistrationView,\
|
||||
WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
|
||||
|
||||
|
||||
app_name = 'wei'
|
||||
urlpatterns = [
|
||||
path('detail/', CurrentWEIDetailView.as_view(), name="current_wei_detail"),
|
||||
path('list/', WEIListView.as_view(), name="wei_list"),
|
||||
path('create/', WEICreateView.as_view(), name="wei_create"),
|
||||
path('detail/<int:pk>/', WEIDetailView.as_view(), name="wei_detail"),
|
||||
path('update/<int:pk>/', WEIUpdateView.as_view(), name="wei_update"),
|
||||
path('detail/<int:pk>/registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"),
|
||||
path('detail/<int:pk>/memberships/', WEIMembershipsView.as_view(), name="wei_memberships"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/', MemberListRenderView.as_view(),
|
||||
name="wei_memberships_bus_pdf"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
|
||||
name="wei_memberships_team_pdf"),
|
||||
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
|
||||
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
|
||||
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
|
||||
path('add-bus-team/<int:pk>/', BusTeamCreateView.as_view(), name="add_team"),
|
||||
path('manage-bus-team/<int:pk>/', BusTeamManageView.as_view(), name="manage_bus_team"),
|
||||
path('update-bus-team/<int:pk>/', BusTeamUpdateView.as_view(), name="update_bus_team"),
|
||||
path('register/<int:wei_pk>/1A/', WEIRegister1AView.as_view(), name="wei_register_1A"),
|
||||
path('register/<int:wei_pk>/2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"),
|
||||
path('register/<int:wei_pk>/1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"),
|
||||
path('register/<int:wei_pk>/2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"),
|
||||
path('edit-registration/<int:pk>/', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"),
|
||||
path('delete-registration/<int:pk>/', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"),
|
||||
path('validate/<int:pk>/', WEIValidateRegistrationView.as_view(), name="validate_registration"),
|
||||
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
|
||||
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
|
||||
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
|
||||
]
|
946
apps/wei/views.py
Normal file
946
apps/wei/views.py
Normal file
@ -0,0 +1,946 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime, date
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models.functions.text import Lower
|
||||
from django.forms import HiddenInput
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import BaseFormView, DeleteView
|
||||
from django_tables2 import SingleTableView
|
||||
from member.models import Membership, Club
|
||||
from note.models import Transaction, NoteClub, Alias
|
||||
from note.tables import HistoryTable
|
||||
from note_kfet.settings import BASE_DIR
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms.registration import WEIChooseBusForm
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
|
||||
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembershipForm, CurrentSurvey
|
||||
from .tables import WEITable, WEIRegistrationTable, BusTable, BusTeamTable, WEIMembershipTable
|
||||
|
||||
|
||||
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
wei = WEIClub.objects.filter(membership_start__lte=date.today()).order_by('date_start')
|
||||
if wei.exists():
|
||||
wei = wei.last()
|
||||
return reverse_lazy('wei:wei_detail', args=(wei.pk,))
|
||||
else:
|
||||
return reverse_lazy('wei:wei_list')
|
||||
|
||||
|
||||
class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List existing WEI
|
||||
"""
|
||||
model = WEIClub
|
||||
table_class = WEITable
|
||||
ordering = '-year'
|
||||
|
||||
|
||||
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create WEI
|
||||
"""
|
||||
model = WEIClub
|
||||
form_class = WEIForm
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.requires_membership = True
|
||||
form.instance.parent_club = Club.objects.get(name="Kfet")
|
||||
ret = super().form_valid(form)
|
||||
NoteClub.objects.create(club=form.instance)
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View WEI information
|
||||
"""
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
club = context["club"]
|
||||
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \
|
||||
.order_by('-created_at', '-id')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
context['history_list'] = history_table
|
||||
|
||||
club_member = WEIMembership.objects.filter(
|
||||
club=club,
|
||||
date_end__gte=datetime.today(),
|
||||
).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
||||
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
|
||||
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||
context['member_list'] = membership_table
|
||||
|
||||
pre_registrations = WEIRegistration.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter(
|
||||
membership=None,
|
||||
wei=club
|
||||
)
|
||||
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
|
||||
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||
context['pre_registrations'] = pre_registrations_table
|
||||
|
||||
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
||||
if my_registration.exists():
|
||||
my_registration = my_registration.get()
|
||||
else:
|
||||
my_registration = None
|
||||
context["my_registration"] = my_registration
|
||||
|
||||
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
|
||||
.filter(wei=self.object).annotate(count=Count("memberships"))
|
||||
bus_table = BusTable(data=buses, prefix="bus-")
|
||||
context['buses'] = bus_table
|
||||
|
||||
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
|
||||
|
||||
if random_user is None:
|
||||
# This case occurs when all users are registered to the WEI.
|
||||
# Don't worry, Pikachu never went to the WEI.
|
||||
# This bug can arrive only in dev mode.
|
||||
context["can_add_first_year_member"] = True
|
||||
context["can_add_any_member"] = True
|
||||
else:
|
||||
# Check if the user has the right to create a registration of a random first year member.
|
||||
empty_fy_registration = WEIRegistration(
|
||||
user=random_user,
|
||||
first_year=True,
|
||||
birth_date="1970-01-01",
|
||||
gender="No",
|
||||
emergency_contact_name="No",
|
||||
emergency_contact_phone="No",
|
||||
)
|
||||
context["can_add_first_year_member"] = PermissionBackend \
|
||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration)
|
||||
|
||||
# Check if the user has the right to create a registration of a random old member.
|
||||
empty_old_registration = WEIRegistration(
|
||||
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
|
||||
first_year=False,
|
||||
birth_date="1970-01-01",
|
||||
gender="No",
|
||||
emergency_contact_name="No",
|
||||
emergency_contact_phone="No",
|
||||
)
|
||||
context["can_add_any_member"] = PermissionBackend \
|
||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration)
|
||||
|
||||
empty_bus = Bus(
|
||||
wei=club,
|
||||
name="",
|
||||
)
|
||||
context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus)
|
||||
|
||||
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List all WEI memberships
|
||||
"""
|
||||
model = WEIMembership
|
||||
table_class = WEIMembershipTable
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(club=self.club)
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
| Q(bus__name__iregex=pattern)
|
||||
| Q(team__name__iregex=pattern)
|
||||
)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.club
|
||||
context["title"] = _("Find WEI Membership")
|
||||
return context
|
||||
|
||||
|
||||
class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List all non-validated WEI registrations.
|
||||
"""
|
||||
model = WEIRegistration
|
||||
table_class = WEIRegistrationTable
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.club
|
||||
context["title"] = _("Find WEI Registration")
|
||||
return context
|
||||
|
||||
|
||||
class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update the information of the WEI.
|
||||
"""
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
form_class = WEIForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object()
|
||||
today = date.today()
|
||||
# We can't update a past WEI
|
||||
# But we can update it while it is not officially opened
|
||||
if today > wei.membership_end:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create Bus
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
today = date.today()
|
||||
# We can't add a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["wei"].initial = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update Bus
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().wei
|
||||
today = date.today()
|
||||
# We can't update a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["wei"].disabled = True
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Manage Bus
|
||||
"""
|
||||
model = Bus
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
|
||||
bus = self.object
|
||||
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
||||
.filter(bus=bus).annotate(count=Count("memberships"))
|
||||
teams_table = BusTeamTable(data=teams, prefix="team-")
|
||||
context["teams"] = teams_table
|
||||
|
||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request.user, WEIMembership, "view")).filter(bus=bus)
|
||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||
context["memberships"] = memberships_table
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create BusTeam
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
|
||||
today = date.today()
|
||||
# We can't add a team once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
bus = Bus.objects.get(pk=self.kwargs["pk"])
|
||||
context["club"] = bus.wei
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["bus"].initial = Bus.objects.get(pk=self.kwargs["pk"])
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.pk})
|
||||
|
||||
|
||||
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update Bus team
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().bus.wei
|
||||
today = date.today()
|
||||
# We can't update a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.bus.wei
|
||||
context["bus"] = self.object.bus
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["bus"].disabled = True
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Manage Bus team
|
||||
"""
|
||||
model = BusTeam
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["bus"] = self.object.bus
|
||||
context["club"] = self.object.bus.wei
|
||||
|
||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request.user, WEIMembership, "view")).filter(team=self.object)
|
||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||
context["memberships"] = memberships_table
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register a new user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
today = date.today()
|
||||
# We can't register someone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Register 1A")
|
||||
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
if "myself" in self.request.path:
|
||||
context["form"].fields["user"].disabled = True
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].initial = self.request.user
|
||||
del form.fields["first_year"]
|
||||
del form.fields["caution_check"]
|
||||
del form.fields["information_json"]
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
form.instance.first_year = True
|
||||
|
||||
if not form.instance.pk:
|
||||
# Check if the user is not already registered to the WEI
|
||||
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user is already registered to this WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Check if the user can be in her/his first year (yeah, no cheat)
|
||||
if WEIRegistration.objects.filter(user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user can't be in her/his first year since he/she has already"
|
||||
" participed to a WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register an old user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
today = date.today()
|
||||
# We can't register someone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Register 2A+")
|
||||
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
|
||||
if "myself" in self.request.path:
|
||||
context["form"].fields["user"].disabled = True
|
||||
|
||||
choose_bus_form = WEIChooseBusForm()
|
||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
||||
context['membership_form'] = choose_bus_form
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].initial = self.request.user
|
||||
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.")
|
||||
|
||||
del form.fields["caution_check"]
|
||||
del form.fields["first_year"]
|
||||
del form.fields["ml_events_registration"]
|
||||
del form.fields["ml_art_registration"]
|
||||
del form.fields["ml_sport_registration"]
|
||||
del form.fields["information_json"]
|
||||
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
form.instance.first_year = False
|
||||
|
||||
if not form.instance.pk:
|
||||
# Check if the user is not already registered to the WEI
|
||||
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user is already registered to this WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
||||
if not choose_bus_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
information = form.instance.information
|
||||
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
|
||||
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
form.instance.information = information
|
||||
form.instance.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update a registration for the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return WEIRegistration.objects
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().wei
|
||||
today = date.today()
|
||||
# We can't update a registration once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
|
||||
if self.object.is_validated:
|
||||
membership_form = WEIMembershipForm(instance=self.object.membership)
|
||||
for field_name, field in membership_form.fields.items():
|
||||
if not PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
|
||||
field.widget = HiddenInput()
|
||||
context["membership_form"] = membership_form
|
||||
elif not self.object.first_year and PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
||||
choose_bus_form = WEIChooseBusForm(
|
||||
dict(
|
||||
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
||||
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
|
||||
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
|
||||
)
|
||||
)
|
||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
||||
context["membership_form"] = choose_bus_form
|
||||
|
||||
if not self.object.soge_credit and self.object.user.profile.soge:
|
||||
form = context["form"]
|
||||
form.fields["soge_credit"].disabled = True
|
||||
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].disabled = True
|
||||
if not self.object.first_year:
|
||||
del form.fields["information_json"]
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
# If the membership is already validated, then we update the bus and the team (and the roles)
|
||||
if form.instance.is_validated:
|
||||
membership_form = WEIMembershipForm(self.request.POST, instance=form.instance.membership)
|
||||
if not membership_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
membership_form.save()
|
||||
# If it is not validated and if this is an old member, then we update the choices
|
||||
elif not form.instance.first_year and PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
||||
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
||||
if not choose_bus_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
information = form.instance.information
|
||||
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
|
||||
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
form.instance.information = information
|
||||
form.instance.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
if self.object.first_year:
|
||||
survey = CurrentSurvey(self.object)
|
||||
if not survey.is_complete():
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
|
||||
|
||||
|
||||
class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||
"""
|
||||
Delete a non-validated WEI registration
|
||||
"""
|
||||
model = WEIRegistration
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
wei = object.wei
|
||||
today = date.today()
|
||||
# We can't delete a registration of a past WEI
|
||||
if today > wei.membership_end:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
|
||||
if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object):
|
||||
raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,))
|
||||
|
||||
|
||||
class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Validate WEI Registration
|
||||
"""
|
||||
model = WEIMembership
|
||||
form_class = WEIMembershipForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
today = date.today()
|
||||
# We can't validate anyone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
context["registration"] = registration
|
||||
survey = CurrentSurvey(registration)
|
||||
if survey.information.valid:
|
||||
context["suggested_bus"] = survey.information.get_selected_bus()
|
||||
context["club"] = registration.wei
|
||||
context["fee"] = registration.wei.membership_fee_paid if registration.user.profile.paid \
|
||||
else registration.wei.membership_fee_unpaid
|
||||
context["kfet_member"] = Membership.objects.filter(
|
||||
club__name="Kfet",
|
||||
user=registration.user,
|
||||
date_start__lte=datetime.now().date(),
|
||||
date_end__gte=datetime.now().date(),
|
||||
).exists()
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
|
||||
if registration.first_year:
|
||||
# Use the results of the survey to fill initial data
|
||||
# A first year has no other role than "1A"
|
||||
del form.fields["roles"]
|
||||
survey = CurrentSurvey(registration)
|
||||
if survey.information.valid:
|
||||
form.fields["bus"].initial = survey.information.get_selected_bus()
|
||||
else:
|
||||
# Use the choice of the member to fill initial data
|
||||
information = registration.information
|
||||
if "preferred_bus_pk" in information and len(information["preferred_bus_pk"]) == 1:
|
||||
form["bus"].initial = Bus.objects.get(pk=information["preferred_bus_pk"][0])
|
||||
if "preferred_team_pk" in information and len(information["preferred_team_pk"]) == 1:
|
||||
form["team"].initial = Bus.objects.get(pk=information["preferred_team_pk"][0])
|
||||
if "preferred_roles_pk" in information:
|
||||
form["roles"].initial = WEIRole.objects.filter(
|
||||
Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI")
|
||||
).all()
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Create membership, check that all is good, make transactions
|
||||
"""
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
club = registration.wei
|
||||
user = registration.user
|
||||
|
||||
membership = form.instance
|
||||
membership.user = user
|
||||
membership.club = club
|
||||
membership.date_start = min(date.today(), club.date_start)
|
||||
membership.registration = registration
|
||||
|
||||
if user.profile.paid:
|
||||
fee = club.membership_fee_paid
|
||||
else:
|
||||
fee = club.membership_fee_unpaid
|
||||
|
||||
if not registration.soge_credit and user.note.balance < fee:
|
||||
# Users must have money before registering to the WEI.
|
||||
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
|
||||
form.add_error('bus',
|
||||
_("This user don't have enough money to join this club, and can't have a negative balance."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if not registration.caution_check and not registration.first_year:
|
||||
form.add_error('bus', _("This user didn't give her/his caution check."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if club.parent_club is not None:
|
||||
if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
|
||||
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
||||
return super().form_invalid(form)
|
||||
|
||||
# Now, all is fine, the membership can be created.
|
||||
|
||||
if registration.first_year:
|
||||
membership = form.instance
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.set(WEIRole.objects.filter(name="1A").all())
|
||||
membership.save()
|
||||
|
||||
ret = super().form_valid(form)
|
||||
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
|
||||
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk})
|
||||
|
||||
|
||||
class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||
"""
|
||||
Display the survey for the WEI for first year members.
|
||||
Warning: this page is accessible for anyone that is connected, the view doesn't extend ProtectQuerySetMixin.
|
||||
"""
|
||||
model = WEIRegistration
|
||||
template_name = "wei/survey.html"
|
||||
survey = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
|
||||
wei = obj.wei
|
||||
today = date.today()
|
||||
# We can't access to the WEI survey once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
|
||||
if not self.survey:
|
||||
self.survey = CurrentSurvey(obj)
|
||||
# If the survey is complete, then display the end page.
|
||||
if self.survey.is_complete():
|
||||
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
|
||||
# Non first year members don't have a survey
|
||||
if not obj.first_year:
|
||||
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_class(self):
|
||||
"""
|
||||
Get the survey form. It may depend on the current state of the survey.
|
||||
"""
|
||||
return self.survey.get_form_class()
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""
|
||||
Update the form with the data of the survey.
|
||||
"""
|
||||
form = super().get_form(form_class)
|
||||
self.survey.update_form(form)
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Update the survey with the data of the form.
|
||||
"""
|
||||
self.survey.form_valid(form)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,))
|
||||
|
||||
|
||||
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_end.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
class WEIClosedView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_closed.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
class MemberListRenderView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Render Invoice as a generated PDF with the given information and a LaTeX template
|
||||
"""
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
||||
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
|
||||
Lower('bus__name'),
|
||||
Lower('team__name'),
|
||||
'user__profile__promotion',
|
||||
Lower('user__last_name'),
|
||||
Lower('user__first_name'),
|
||||
'id',
|
||||
)
|
||||
|
||||
if "bus_pk" in self.kwargs:
|
||||
qs = qs.filter(bus__pk=self.kwargs["bus_pk"])
|
||||
|
||||
if "team_pk" in self.kwargs:
|
||||
qs = qs.filter(team__pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
qs = self.get_queryset()
|
||||
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
bus = team = None
|
||||
if "bus_pk" in self.kwargs:
|
||||
bus = Bus.objects.get(pk=self.kwargs["bus_pk"])
|
||||
if "team_pk" in self.kwargs:
|
||||
team = BusTeam.objects.filter(pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
|
||||
if team.exists():
|
||||
team = team.get()
|
||||
bus = team.bus
|
||||
else:
|
||||
team = dict(name="Staff")
|
||||
|
||||
# Fill the template with the information
|
||||
tex = render_to_string("wei/weilist_sample.tex", dict(memberships=qs.all(), wei=wei, bus=bus, team=team))
|
||||
|
||||
try:
|
||||
os.mkdir(BASE_DIR + "/tmp")
|
||||
except FileExistsError:
|
||||
pass
|
||||
# We render the file in a temporary directory
|
||||
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
|
||||
|
||||
try:
|
||||
with open("{}/wei-list.tex".format(tmp_dir), "wb") as f:
|
||||
f.write(tex.encode("UTF-8"))
|
||||
del tex
|
||||
|
||||
error = subprocess.Popen(
|
||||
["pdflatex", "{}/wei-list.tex".format(tmp_dir)],
|
||||
cwd=tmp_dir,
|
||||
stdin=open(os.devnull, "r"),
|
||||
stderr=open(os.devnull, "wb"),
|
||||
stdout=open(os.devnull, "wb"),
|
||||
).wait()
|
||||
|
||||
if error:
|
||||
raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")")
|
||||
|
||||
# Display the generated pdf as a HTTP Response
|
||||
pdf = open("{}/wei-list.pdf".format(tmp_dir), 'rb').read()
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response['Content-Disposition'] = "inline;filename=Liste%20des%20participants%20au%20WEI.pdf"
|
||||
except IOError as e:
|
||||
raise e
|
||||
finally:
|
||||
# Delete all temporary files
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
return response
|
Reference in New Issue
Block a user