Merge remote-tracking branch 'origin/master' into import_nk15

# Conflicts:
#	apps/treasury/signals.py
This commit is contained in:
Yohann D'ANELLO 2020-05-07 19:01:23 +02:00
commit fdf373d1d5
126 changed files with 11144 additions and 2592 deletions

View File

@ -11,3 +11,5 @@ DJANGO_SETTINGS_MODULE=note_kfet.settings
DOMAIN=localhost DOMAIN=localhost
CONTACT_EMAIL=tresorerie.bde@localhost CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost NOTE_URL=localhost
NOTE_MAIL=notekfet@localhost
WEBMASTER_MAIL=notekfet@localhost

View File

@ -118,6 +118,8 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
DOMAIN=localhost # note.example.com DOMAIN=localhost # note.example.com
CONTACT_EMAIL=tresorerie.bde@localhost CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost # serveur cas note.example.com si auto-hébergé. NOTE_URL=localhost # serveur cas note.example.com si auto-hébergé.
NOTE_MAIL=notekfet@localhost # Adresse expéditrice des mails
WEBMASTER_MAIL=notekfet@localhost # Adresse sur laquelle contacter les webmasters de la note
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations

View File

@ -4,7 +4,7 @@ from datetime import timedelta, datetime
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType 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 member.models import Club
from note.models import NoteUser, Note from note.models import NoteUser, Note
from note_kfet.inputs import DateTimePickerInput, Autocomplete from note_kfet.inputs import DateTimePickerInput, Autocomplete

View File

@ -139,7 +139,7 @@ class Entry(models.Model):
verbose_name = _("entry") verbose_name = _("entry")
verbose_name_plural = _("entries") 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) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
@ -153,7 +153,7 @@ class Entry(models.Model):
if self.note.balance < 0: if self.note.balance < 0:
raise ValidationError(_("The balance is negative.")) raise ValidationError(_("The balance is negative."))
ret = super().save(*args,**kwargs) ret = super().save(*args, **kwargs)
if insert and self.guest: if insert and self.guest:
GuestTransaction.objects.create( GuestTransaction.objects.create(

View File

@ -3,6 +3,7 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Q from django.db.models import F, Q
@ -45,8 +46,8 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
context['title'] = _("Activities") context['title'] = _("Activities")
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
context['upcoming'] = ActivityTable(data=upcoming_activities context['upcoming'] = ActivityTable(
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))) data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
return context return context
@ -138,8 +139,14 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
| Q(note__noteuser__user__last_name__regex=pattern) | Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex=pattern) | Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern)))) \ | Q(normalized_name__regex=Alias.normalize(pattern)))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
.distinct()[:20] 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: for note in note_qs:
note.type = "Adhérent" note.type = "Adhérent"
note.activity = activity note.activity = activity

View File

@ -15,6 +15,7 @@ from note.api.urls import register_note_urls
from treasury.api.urls import register_treasury_urls from treasury.api.urls import register_treasury_urls
from logs.api.urls import register_logs_urls from logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls from permission.api.urls import register_permission_urls
from wei.api.urls import register_wei_urls
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -78,6 +79,7 @@ register_note_urls(router, 'note')
register_treasury_urls(router, 'treasury') register_treasury_urls(router, 'treasury')
register_permission_urls(router, 'permission') register_permission_urls(router, 'permission')
register_logs_urls(router, 'logs') register_logs_urls(router, 'logs')
register_wei_urls(router, 'wei')
app_name = 'api' app_name = 'api'

View File

@ -9,7 +9,7 @@ from note.models import NoteSpecial
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask from permission.models import PermissionMask
from .models import Profile, Club, Membership from .models import Profile, Club, Membership, Role
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
@ -25,10 +25,16 @@ class ProfileForm(forms.ModelForm):
A form for the extras field provided by the :model:`member.Profile` model. 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: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', ) exclude = ('user', 'email_confirmed', 'registration_valid', )
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):
@ -50,6 +56,8 @@ class ClubForm(forms.ModelForm):
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
soge = forms.BooleanField( soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"), label=_("Inscription paid by Société Générale"),
required=False, required=False,

View File

@ -23,18 +23,20 @@ class Profile(models.Model):
We do not want to patch the Django Contrib :model:`auth.User`model; We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information. so this model add an user profile with additional information.
""" """
user = models.OneToOneField( user = models.OneToOneField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
phone_number = models.CharField( phone_number = models.CharField(
verbose_name=_('phone number'), verbose_name=_('phone number'),
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
) )
section = models.CharField( section = models.CharField(
verbose_name=_('section'), verbose_name=_('section'),
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'), help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
@ -42,12 +44,44 @@ class Profile(models.Model):
blank=True, blank=True,
null=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( address = models.CharField(
verbose_name=_('address'), verbose_name=_('address'),
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
) )
paid = models.BooleanField( paid = models.BooleanField(
verbose_name=_("paid"), verbose_name=_("paid"),
help_text=_("Tells if the user receive a salary."), help_text=_("Tells if the user receive a salary."),
@ -64,11 +98,29 @@ class Profile(models.Model):
default=False, default=False,
) )
soge = models.BooleanField( @property
verbose_name=_("Société générale"), def ens_year(self):
help_text=_("Has the user ever be paid by the Société générale?"), """
default=False, 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: class Meta:
verbose_name = _('user profile') verbose_name = _('user profile')
@ -85,7 +137,7 @@ class Profile(models.Model):
'user': self.user, 'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"), 'domain': os.getenv("NOTE_URL", "note.example.com"),
'token': email_validation_token.make_token(self.user), '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) self.user.email_user(subject, message)
@ -171,6 +223,7 @@ class Club(models.Model):
self.membership_start.month, self.membership_start.day) self.membership_start.month, self.membership_start.day)
self.membership_end = datetime.date(self.membership_end.year + 1, self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day) self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True) self.save(force_update=True)
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
@ -220,6 +273,7 @@ class Membership(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
User, User,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="memberships",
verbose_name=_("user"), verbose_name=_("user"),
) )
@ -308,6 +362,19 @@ class Membership(models.Model):
reason="Adhésion " + self.club.name, reason="Adhésion " + self.club.name,
) )
transaction._force_save = True transaction._force_save = 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) transaction.save(force_insert=True)
def __str__(self): def __str__(self):

View File

@ -18,7 +18,7 @@ from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from note.forms import ImageForm 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.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable from note.tables import HistoryTable, AliasTable
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -128,7 +128,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['user_object'] user = context['user_object']
history_list = \ 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")) .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-') history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) 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(first_name__iregex=pattern)
| Q(last_name__iregex=pattern) | Q(last_name__iregex=pattern)
| Q(profile__section__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__name__iregex="^" + pattern)
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
) )
@ -314,7 +315,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club.update_membership_dates() club.update_membership_dates()
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ 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 = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table context['history_list'] = history_table
@ -365,6 +367,15 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form_class = ClubForm form_class = ClubForm
template_name = "member/club_form.html" 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): def get_success_url(self):
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) 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: if "club_pk" in self.kwargs:
# We create a new membership. # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ 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['credit_amount'].initial = club.membership_fee_paid
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() 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"] bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and club.name == "BDE" 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: if soge:
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire") credit_type = None
bde = club form.instance._soge = True
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"
if credit_type is None: if credit_type is None:
credit_amount = 0 credit_amount = 0
@ -521,6 +526,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
# Now, all is fine, the membership can be created. # 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. # Credit note before the membership is created.
if credit_amount > 0: if credit_amount > 0:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): 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, valid=True,
) )
# If Société générale pays, then we store the information: the bank can't pay twice to a same person. ret = super().form_valid(form)
if soge:
user.profile.soge = True
user.profile.save()
# 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 = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid 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(), date_end__gte=datetime.today(),
) )
membership = Membership.objects.create( membership = Membership(
club=kfet, club=kfet,
user=user, user=user,
fee=kfet_fee, fee=kfet_fee,
date_start=old_membership.get().date_end + timedelta(days=1) date_start=old_membership.get().date_end + timedelta(days=1)
if old_membership.exists() else form.instance.date_start, if old_membership.exists() else form.instance.date_start,
) )
membership._soge = True
membership.save()
membership.refresh_from_db()
if old_membership.exists(): if old_membership.exists():
membership.roles.set(old_membership.get().roles.all()) membership.roles.set(old_membership.get().roles.all())
else: else:
membership.roles.add(Role.objects.get(name="Adhérent Kfet")) membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save() membership.save()
return super().form_valid(form) return ret
def get_success_url(self): def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})

View File

@ -3,6 +3,8 @@
from rest_framework import serializers from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer 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.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
@ -97,6 +99,35 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
model = Note 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): class TemplateCategorySerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Transaction templates. REST API Serializer for Transaction templates.

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, \ from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
@ -11,6 +11,7 @@ def register_note_urls(router, path):
""" """
router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet) router.register(path + '/alias', AliasViewSet)
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/transaction', TransactionViewSet)

View File

@ -10,8 +10,8 @@ from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TransactionTemplateSerializer, TransactionPolymorphicSerializer TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias from ..models.notes import Note, Alias
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@ -90,6 +90,30 @@ class AliasViewSet(ReadProtectedModelViewSet):
return queryset 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): class TemplateCategoryViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.

View File

@ -4,7 +4,7 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ 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 from .models import TransactionTemplate, NoteClub
@ -24,11 +24,6 @@ class TransactionTemplateForm(forms.ModelForm):
model = TransactionTemplate model = TransactionTemplate
fields = '__all__' 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 = { widgets = {
'destination': 'destination':
Autocomplete( Autocomplete(
@ -41,4 +36,5 @@ class TransactionTemplateForm(forms.ModelForm):
'placeholder': 'Note ...', 'placeholder': 'Note ...',
}, },
), ),
'amount': AmountInput(),
} }

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from .notes import Note, NoteClub, NoteSpecial from .notes import Note, NoteClub, NoteSpecial
from ..templatetags.pretty_money import pretty_money
""" """
Defines transactions Defines transactions
@ -198,6 +199,14 @@ class Transaction(PolymorphicModel):
self.source.save() self.source.save()
self.destination.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 @property
def total(self): def total(self):
return self.amount * self.quantity return self.amount * self.quantity
@ -206,6 +215,10 @@ class Transaction(PolymorphicModel):
def type(self): def type(self):
return _('Transfer') 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): class RecurrentTransaction(Transaction):
""" """
@ -214,8 +227,7 @@ class RecurrentTransaction(Transaction):
template = models.ForeignKey( template = models.ForeignKey(
TransactionTemplate, TransactionTemplate,
null=True, on_delete=models.PROTECT,
on_delete=models.SET_NULL,
) )
category = models.ForeignKey( category = models.ForeignKey(
TemplateCategory, TemplateCategory,

View File

@ -10,14 +10,14 @@ def save_user_note(instance, raw, **_kwargs):
# When provisionning data, do not try to autocreate # When provisionning data, do not try to autocreate
return 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 # Create note only when the registration is validated
from note.models import NoteUser from note.models import NoteUser
NoteUser.objects.get_or_create(user=instance) NoteUser.objects.get_or_create(user=instance)
instance.note.save() 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 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 # When provisionning data, do not try to autocreate
return return
if created:
from .models import NoteClub from .models import NoteClub
NoteClub.objects.create(club=instance) NoteClub.objects.get_or_create(club=instance)
instance.note.save() instance.note.save()

View File

@ -55,7 +55,7 @@ class HistoryTable(tables.Table):
"class": lambda record: str(record.valid).lower() + ' validate', "class": lambda record: str(record.valid).lower() + ' validate',
"data-toggle": "tooltip", "data-toggle": "tooltip",
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"), "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_' "onmouseover": lambda record: '$("#invalidity_reason_'
+ str(record.id) + '").show();$("#invalidity_reason_' + str(record.id) + '").show();$("#invalidity_reason_'
+ str(record.id) + '").focus();', + str(record.id) + '").focus();',
@ -129,13 +129,14 @@ class ButtonTable(tables.Table):
'table table-bordered condensed table-hover' 'table table-bordered condensed table-hover'
} }
row_attrs = { 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), 'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk 'data-href': lambda record: record.pk
} }
model = TransactionTemplate model = TransactionTemplate
exclude = ('id',) exclude = ('id',)
order_by = ('type', '-display', 'destination__name', 'name',)
edit = tables.LinkColumn('note:template_update', edit = tables.LinkColumn('note:template_update',
args=[A('pk')], args=[A('pk')],

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@ -30,7 +31,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
table_class = HistoryTable table_class = HistoryTable
def get_queryset(self, **kwargs): 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): def get_context_data(self, **kwargs):
""" """
@ -80,6 +81,33 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list') 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): class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
@ -93,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
table_class = HistoryTable table_class = HistoryTable
def get_queryset(self, **kwargs): 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): def get_context_data(self, **kwargs):
""" """

View File

@ -8,6 +8,7 @@ from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, F from django.db.models import Q, F
from note.models import Note, NoteUser, NoteClub, NoteSpecial from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet import settings
from note_kfet.middlewares import get_current_session from note_kfet.middlewares import get_current_session
from member.models import Membership, Club from member.models import Membership, Club
@ -36,8 +37,10 @@ class PermissionBackend(ModelBackend):
# Unauthenticated users have no permissions # Unauthenticated users have no permissions
return Permission.objects.none() return Permission.objects.none()
return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ return Permission.objects.annotate(
.filter( club=F("rolepermissions__role__membership__club"),
membership=F("rolepermissions__role__membership"),
).filter(
rolepermissions__role__membership__user=user, rolepermissions__role__membership__user=user,
rolepermissions__role__membership__date_start__lte=datetime.date.today(), rolepermissions__role__membership__date_start__lte=datetime.date.today(),
rolepermissions__role__membership__date_end__gte=datetime.date.today(), rolepermissions__role__membership__date_end__gte=datetime.date.today(),
@ -55,6 +58,7 @@ class PermissionBackend(ModelBackend):
:return: A generator of the requested permissions :return: A generator of the requested permissions
""" """
clubs = {} clubs = {}
memberships = {}
for permission in PermissionBackend.get_raw_permissions(user, type): for permission in PermissionBackend.get_raw_permissions(user, type):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club: 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) clubs[permission.club] = club = Club.objects.get(pk=permission.club)
else: else:
club = clubs[permission.club] 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( permission = permission.about(
user=user, user=user,
club=club, club=club,
membership=membership,
User=User, User=User,
Club=Club, Club=Club,
Membership=Membership, Membership=Membership,
@ -75,7 +86,9 @@ class PermissionBackend(ModelBackend):
NoteClub=NoteClub, NoteClub=NoteClub,
NoteSpecial=NoteSpecial, NoteSpecial=NoteSpecial,
F=F, F=F,
Q=Q Q=Q,
now=datetime.datetime.now(),
today=datetime.date.today(),
) )
yield permission yield permission
@ -95,7 +108,7 @@ class PermissionBackend(ModelBackend):
# Anonymous users can't do anything # Anonymous users can't do anything
return Q(pk=-1) 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 # Superusers have all rights
return Q() return Q()
@ -129,9 +142,9 @@ class PermissionBackend(ModelBackend):
sess = get_current_session() sess = get_current_session()
if sess is not None and sess.session_key is None: 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 return True
if obj is None: if obj is None:

File diff suppressed because it is too large Load Diff

View File

@ -120,7 +120,12 @@ class Permission(models.Model):
('delete', 'delete') ('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 # A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects) # query -> [] | {} (the empty query representing all objects)
@ -142,18 +147,34 @@ class Permission(models.Model):
# Examples: # Examples:
# Q(is_superuser=True) := {"is_superuser": true} # Q(is_superuser=True) := {"is_superuser": true}
# ~Q(is_superuser=True) := ["NOT", {"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( mask = models.ForeignKey(
PermissionMask, PermissionMask,
on_delete=models.PROTECT, 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: class Meta:
unique_together = ('model', 'query', 'type', 'field') 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) return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
def __str__(self): def __str__(self):
if self.field: return self.description
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)
class RolePermissions(models.Model): class RolePermissions(models.Model):
""" """
Permissions associated with a Role Permissions associated with a Role
""" """
role = models.ForeignKey( role = models.OneToOneField(
Role, Role,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+', related_name='permissions',
verbose_name=_('role'), verbose_name=_('role'),
) )
permissions = models.ManyToManyField( permissions = models.ManyToManyField(
Permission, Permission,
verbose_name=_("permissions"),
) )
def __str__(self): def __str__(self):

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -57,13 +58,19 @@ def pre_save_object(sender, instance, **kwargs):
if old_value == new_value: if old_value == new_value:
continue continue
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): 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: else:
# We check if the user has right to add the object # We check if the user has right to add the object
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
if not has_perm: 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): 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 # We check if the user has rights to delete the object
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): 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
View 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"),
]

View File

@ -1,11 +1,60 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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: 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): def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs) qs = super().get_queryset(**kwargs)
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) 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

View File

@ -27,6 +27,15 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', ) 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): class ValidationForm(forms.Form):
""" """
Validate the inscription of the new users and pay memberships. Validate the inscription of the new users and pay memberships.

View File

@ -11,12 +11,12 @@ from django.urls import reverse_lazy
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View 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.views.generic.edit import FormMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from member.forms import ProfileForm from member.forms import ProfileForm
from member.models import Membership, Club, Role 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 note.templatetags.pretty_money import pretty_money
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
@ -32,13 +32,13 @@ class UserCreateView(CreateView):
""" """
form_class = SignUpForm form_class = SignUpForm
success_url = reverse_lazy('registration:email_validation_sent')
template_name = 'registration/signup.html' template_name = 'registration/signup.html'
second_form = ProfileForm second_form = ProfileForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form() context["profile_form"] = self.second_form()
del context["profile_form"].fields["section"]
return context return context
@ -67,6 +67,9 @@ class UserCreateView(CreateView):
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('registration:email_validation_sent')
class UserValidateView(TemplateView): class UserValidateView(TemplateView):
""" """
@ -112,7 +115,7 @@ class UserValidateView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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) context['login_url'] = resolve_url(settings.LOGIN_URL)
if self.validlink: if self.validlink:
context['validlink'] = True context['validlink'] = True
@ -263,17 +266,17 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += kfet_fee fee += kfet_fee
if soge: if soge:
# Fill payment information if Société Générale pays the inscription # If the bank pays, then we don't credit now. Treasurers will validate the transaction
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire") # and credit the note later.
credit_amount = fee credit_type = None
bank = "Société générale"
print("OK") if credit_type is None:
credit_amount = 0
if join_Kfet and not join_BDE: if join_Kfet and not join_BDE:
form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club.")) 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 # Check if the user credits enough money
form.add_error('credit_type', form.add_error('credit_type',
_("The entered amount is not enough for the memberships, should be at least {}") _("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) ret = super().form_valid(form)
user.is_active = user.profile.email_confirmed or user.is_superuser user.is_active = user.profile.email_confirmed or user.is_superuser
user.profile.registration_valid = True user.profile.registration_valid = True
# Store if Société générale paid for next years
user.profile.soge = soge
user.save() user.save()
user.profile.save() user.profile.save()
user.refresh_from_db()
if credit_type is not None and credit_amount > 0: if credit_type is not None and credit_amount > 0:
# Credit the note # Credit the note
@ -316,21 +318,29 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
if join_BDE: if join_BDE:
# Create membership for the user to the BDE starting today # Create membership for the user to the BDE starting today
membership = Membership.objects.create( membership = Membership(
club=bde, club=bde,
user=user, user=user,
fee=bde_fee, 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.roles.add(Role.objects.get(name="Adhérent BDE"))
membership.save() membership.save()
if join_Kfet: if join_Kfet:
# Create membership for the user to the Kfet starting today # Create membership for the user to the Kfet starting today
membership = Membership.objects.create( membership = Membership(
club=kfet, club=kfet,
user=user, user=user,
fee=kfet_fee, 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.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save() membership.save()

@ -1 +1 @@
Subproject commit b9db26fa494870b02fc1b4b463a2322395a278a1 Subproject commit f0aa426950b9b867bf99233795e260871be2cb99

View File

@ -3,7 +3,7 @@
from django.contrib import admin from django.contrib import admin
from .models import RemittanceType, Remittance from .models import RemittanceType, Remittance, SogeCredit
@admin.register(RemittanceType) @admin.register(RemittanceType)
@ -25,3 +25,6 @@ class RemittanceAdmin(admin.ModelAdmin):
if not obj: if not obj:
return True return True
return not obj.closed and super().has_change_permission(request, obj) return not obj.closed and super().has_change_permission(request, obj)
admin.site.register(SogeCredit)

View File

@ -4,7 +4,7 @@
from rest_framework import serializers from rest_framework import serializers
from note.api.serializers import SpecialTransactionSerializer 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): class ProductSerializer(serializers.ModelSerializer):
@ -60,3 +60,14 @@ class RemittanceSerializer(serializers.ModelSerializer):
def get_transactions(self, obj): def get_transactions(self, obj):
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions) 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__'

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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): def register_treasury_urls(router, path):
@ -12,3 +12,4 @@ def register_treasury_urls(router, path):
router.register(path + '/product', ProductViewSet) router.register(path + '/product', ProductViewSet)
router.register(path + '/remittance_type', RemittanceTypeViewSet) router.register(path + '/remittance_type', RemittanceTypeViewSet)
router.register(path + '/remittance', RemittanceViewSet) router.register(path + '/remittance', RemittanceViewSet)
router.register(path + '/soge_credit', SogeCreditViewSet)

View File

@ -5,8 +5,9 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer,\
from ..models import Invoice, Product, RemittanceType, Remittance SogeCreditSerializer
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
class InvoiceViewSet(ReadProtectedModelViewSet): 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 The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
then render it on /api/treasury/remittance_type/ then render it on /api/treasury/remittance_type/
""" """
queryset = RemittanceType.objects.all() queryset = RemittanceType.objects
serializer_class = RemittanceTypeSerializer 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, The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/remittance/ then render it on /api/treasury/remittance/
""" """
queryset = Remittance.objects.all() queryset = Remittance.objects
serializer_class = RemittanceSerializer 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

View File

@ -1,11 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ 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): class Invoice(models.Model):
@ -207,3 +209,101 @@ class SpecialTransactionProxy(models.Model):
class Meta: class Meta:
verbose_name = _("special transaction proxy") verbose_name = _("special transaction proxy")
verbose_name_plural = _("special transaction proxies") 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")

View File

@ -7,7 +7,7 @@ from django_tables2 import A
from note.models import SpecialTransaction from note.models import SpecialTransaction
from note.templatetags.pretty_money import pretty_money from note.templatetags.pretty_money import pretty_money
from .models import Invoice, Remittance from .models import Invoice, Remittance, SogeCredit
class InvoiceTable(tables.Table): class InvoiceTable(tables.Table):
@ -101,3 +101,28 @@ class SpecialTransactionTable(tables.Table):
model = SpecialTransaction model = SpecialTransaction
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',) 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', )

View File

@ -4,7 +4,8 @@
from django.urls import path from django.urls import path
from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\ from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView,\
SogeCreditListView, SogeCreditManageView
app_name = 'treasury' app_name = 'treasury'
urlpatterns = [ urlpatterns = [
@ -21,4 +22,7 @@ urlpatterns = [
path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'),
path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(), path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(),
name='unlink_transaction'), 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'),
] ]

View File

@ -10,21 +10,23 @@ from crispy_forms.helper import FormHelper
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.forms import Form
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy 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.base import View, TemplateView
from django.views.generic.edit import BaseFormView
from django_tables2 import SingleTableView 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 note_kfet.settings.base import BASE_DIR
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
from .models import Invoice, Product, Remittance, SpecialTransactionProxy from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
@ -180,7 +182,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
# Display the generated pdf as a HTTP Response # Display the generated pdf as a HTTP Response
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
response = HttpResponse(pdf, content_type="application/pdf") 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: except IOError as e:
raise e raise e
finally: finally:
@ -203,9 +205,9 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["table"] = RemittanceTable(data=Remittance.objects context["table"] = RemittanceTable(
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) data=Remittance.objects.filter(
.all()) PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
return context return context
@ -307,3 +309,61 @@ class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
transaction.save() transaction.save()
return redirect('treasury:remittance_list') 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
View 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
View 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
View File

View 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
View 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
View 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
View 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')

View 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',
]

View 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(),
}

View 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

View 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

View 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()

View File

329
apps/wei/models.py Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -3,7 +3,7 @@
from json import dumps as json_dumps from json import dumps as json_dumps
from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput, Widget
class AmountInput(NumberInput): class AmountInput(NumberInput):
@ -41,6 +41,29 @@ class Autocomplete(TextInput):
return "" return ""
class ColorWidget(Widget):
"""
Pulled from django-colorfield.
Select a color.
"""
template_name = 'colorfield/color.html'
class Media:
js = [
'colorfield/jscolor/jscolor.min.js',
'colorfield/colorfield.js',
]
def format_value(self, value):
if value is None:
value = 0xFFFFFF
return "#{:06X}".format(value)
def value_from_datadict(self, data, files, name):
val = super().value_from_datadict(data, files, name)
return int(val[1:], 16)
""" """
The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github: The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github:
https://github.com/monim67/django-bootstrap-datepicker-plus https://github.com/monim67/django-bootstrap-datepicker-plus

View File

@ -39,41 +39,20 @@ else:
from .development import * from .development import *
try: try:
#in secrets.py defines everything you want # in secrets.py defines everything you want
from .secrets import * from .secrets import *
INSTALLED_APPS += OPTIONAL_APPS INSTALLED_APPS += OPTIONAL_APPS
except ImportError: except ImportError:
pass pass
if "cas" in INSTALLED_APPS: if "cas_server" in INSTALLED_APPS:
MIDDLEWARE += ['cas.middleware.CASMiddleware']
# CAS Settings # CAS Settings
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
CAS_AUTO_CREATE_USER = False CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png" CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png" CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
CAS_SHOW_SERVICE_MESSAGES = True
CAS_SHOW_POWERED = False CAS_SHOW_POWERED = False
CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
CAS_PROVIDE_URL_TO_LOGOUT = True
CAS_INFO_MESSAGES = {
"cas_explained": {
"message": _(
u"The Central Authentication Service grants you access to most of our websites by "
u"authenticating only once, so you don't need to type your credentials again unless "
u"your session expires or you logout."
),
"discardable": True,
"type": "info", # one of info, success, info, warning, danger
},
}
CAS_INFO_MESSAGES_ORDER = [
'cas_explained',
]
AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',)
if "logs" in INSTALLED_APPS: if "logs" in INSTALLED_APPS:
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)

View File

@ -62,6 +62,7 @@ INSTALLED_APPS = [
'permission', 'permission',
'registration', 'registration',
'treasury', 'treasury',
'wei',
] ]
LOGIN_REDIRECT_URL = '/note/transfer/' LOGIN_REDIRECT_URL = '/note/transfer/'

View File

@ -62,10 +62,6 @@ CSRF_COOKIE_HTTPONLY = False
X_FRAME_OPTIONS = 'DENY' X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_AGE = 60 * 60 * 3 SESSION_COOKIE_AGE = 60 * 60 * 3
# CAS Client settings
# Can be modified in secrets.py
CAS_SERVER_URL = "http://localhost:8000/cas/"
STATIC_ROOT = '' # not needed in development settings STATIC_ROOT = '' # not needed in development settings
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')] os.path.join(BASE_DIR, 'static')]

View File

@ -51,6 +51,3 @@ CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False CSRF_COOKIE_HTTPONLY = False
X_FRAME_OPTIONS = 'DENY' X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_AGE = 60 * 60 * 3 SESSION_COOKIE_AGE = 60 * 60 * 3
# CAS Client settings
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
from django.views.generic import RedirectView from django.views.generic import RedirectView
from member.views import CustomLoginView from member.views import CustomLoginView
@ -19,14 +20,16 @@ urlpatterns = [
path('registration/', include('registration.urls')), path('registration/', include('registration.urls')),
path('activity/', include('activity.urls')), path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')), path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls, name="admin"),
path('accounts/login/', CustomLoginView.as_view()), path('accounts/login/', CustomLoginView.as_view()),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('api/', include('api.urls')), path('api/', include('api.urls')),
path('permission/', include('permission.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
@ -44,3 +47,11 @@ if "debug_toolbar" in settings.INSTALLED_APPS:
urlpatterns = [ urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)), path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns ] + urlpatterns
handler400 = bad_request
handler403 = permission_denied
# Only displayed in production, when debug mode is set to False
handler404 = page_not_found
handler500 = server_error

View File

@ -1,2 +1 @@
django-cas-client==1.5.3
django-cas-server==1.1.0 django-cas-server==1.1.0

View File

@ -0,0 +1,12 @@
/** global: django */
window.onload = function() {
if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined') {
(function($) {
// add colopicker to inlines added dynamically
$(document).on('formset:added', function onFormsetAdded(event, row) {
jscolor.installByClassName('jscolor');
});
}(django.jQuery));
}
};

File diff suppressed because it is too large Load Diff

1
static/colorfield/jscolor/jscolor.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ $(document).ready(function () {
name_field = "name"; name_field = "name";
let input = target.val(); let input = target.val();
$.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) { $.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) {
let html = ""; let html = "";
objects.results.forEach(function (obj) { objects.results.forEach(function (obj) {

View File

@ -21,7 +21,7 @@ function pretty_money(value) {
* @param alert_type The type of the alert. Choices: info, success, warning, danger * @param alert_type The type of the alert. Choices: info, success, warning, danger
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/ */
function addMsg(msg, alert_type, timeout=-1) { function addMsg(msg, alert_type, timeout = -1) {
let msgDiv = $("#messages"); let msgDiv = $("#messages");
let html = msgDiv.html(); let html = msgDiv.html();
let id = Math.floor(10000 * Math.random() + 1); let id = Math.floor(10000 * Math.random() + 1);
@ -42,18 +42,18 @@ function addMsg(msg, alert_type, timeout=-1) {
* @param errs_obj [{error_code:erro_message}] * @param errs_obj [{error_code:erro_message}]
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/ */
function errMsg(errs_obj, timeout=-1) { function errMsg(errs_obj, timeout = -1) {
for (const err_msg of Object.values(errs_obj)) { for (const err_msg of Object.values(errs_obj)) {
addMsg(err_msg,'danger', timeout); addMsg(err_msg, 'danger', timeout);
} }
} }
var reloadWithTurbolinks = (function () { var reloadWithTurbolinks = (function () {
var scrollPosition; var scrollPosition;
function reload () { function reload() {
scrollPosition = [window.scrollX, window.scrollY]; scrollPosition = [window.scrollX, window.scrollY];
Turbolinks.visit(window.location.toString(), { action: 'replace' }) Turbolinks.visit(window.location.toString(), {action: 'replace'})
} }
document.addEventListener('turbolinks:load', function () { document.addEventListener('turbolinks:load', function () {
@ -79,17 +79,36 @@ function refreshBalance() {
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/ */
function getMatchedNotes(pattern, fun) { function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun); $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
} }
/** /**
* Generate a <li> entry with a given id and text * Generate a <li> entry with a given id and text
*/ */
function li(id, text) { function li(id, text, extra_css) {
return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" + return "<li class=\"list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate " + extra_css + "\"" +
" id=\"" + id + "\">" + text + "</li>\n"; " id=\"" + id + "\">" + text + "</li>\n";
} }
/**
* Return style to apply according to the balance of the note and the validation status of the email address
* @param note The concerned note.
*/
function displayStyle(note) {
let balance = note.balance;
var css = "";
if (balance < -5000)
css += " text-danger bg-dark";
else if (balance < -1000)
css += " text-danger";
else if (balance < 0)
css += " text-warning";
if (!note.email_confirmed)
css += " text-white bg-primary";
return css;
}
/** /**
* Render note name and picture * Render note name and picture
* @param note The note to render * @param note The note to render
@ -97,27 +116,29 @@ function li(id, text) {
* @param user_note_field * @param user_note_field
* @param profile_pic_field * @param profile_pic_field
*/ */
function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { function displayNote(note, alias, user_note_field = null, profile_pic_field = null) {
if (!note.display_image) { if (!note.display_image) {
note.display_image = '/media/pic/default.png'; note.display_image = '/media/pic/default.png';
$.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) {
note.display_image = new_note.display_image.replace("http:", "https:");
note.name = new_note.name;
note.balance = new_note.balance;
note.user = new_note.user;
displayNote(note, alias, user_note_field, profile_pic_field);
});
return;
} }
let img = note.display_image; let img = note.display_image;
if (alias !== note.name) if (alias !== note.name && note.name)
alias += " (aka. " + note.name + ")"; alias += " (aka. " + note.name + ")";
if (user_note_field !== null) if (user_note_field !== null) {
$("#" + user_note_field).text(alias + (note.balance == null ? "" : (" : " + pretty_money(note.balance)))); $("#" + user_note_field).removeAttr('class');
if (profile_pic_field != null) $("#" + user_note_field).addClass(displayStyle(note));
$("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance))));
if (profile_pic_field != null) {
$("#" + profile_pic_field).attr('src', img); $("#" + profile_pic_field).attr('src', img);
$("#" + profile_pic_field).click(function () {
console.log(note);
if (note.resourcetype === "NoteUser") {
document.location.href = "/accounts/user/" + note.user;
} else if (note.resourcetype === "NoteClub") {
document.location.href = "/accounts/club/" + note.club;
}
});
}
}
} }
/** /**
@ -132,8 +153,8 @@ function displayNote(note, alias, user_note_field=null, profile_pic_field=null)
* (useful in consumptions, put null if not used) * (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events * @returns an anonymous function to be compatible with jQuery events
*/ */
function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) { function removeNote(d, note_prefix = "note", notes_display, note_list_id, user_note_field = null, profile_pic_field = null) {
return (function() { return (function () {
let new_notes_display = []; let new_notes_display = [];
let html = ""; let html = "";
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
@ -141,12 +162,13 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not
disp.quantity -= disp.id === d.id ? 1 : 0; disp.quantity -= disp.id === d.id ? 1 : 0;
new_notes_display.push(disp); new_notes_display.push(disp);
html += li(note_prefix + "_" + disp.id, disp.name html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>",
displayStyle(disp.note));
} }
}); });
notes_display.length = 0; notes_display.length = 0;
new_notes_display.forEach(function(disp) { new_notes_display.forEach(function (disp) {
notes_display.push(disp); notes_display.push(disp);
}); });
@ -154,7 +176,7 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id); let obj = $("#" + note_prefix + "_" + disp.id);
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function() { obj.hover(function () {
if (disp.note) if (disp.note)
displayNote(disp.note, disp.name, user_note_field, profile_pic_field); displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
}); });
@ -165,7 +187,6 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not
/** /**
* Generate an auto-complete field to query a note with its alias * Generate an auto-complete field to query a note with its alias
* @param field_id The identifier of the text field where the alias is typed * @param field_id The identifier of the text field where the alias is typed
* @param alias_matched_id The div block identifier where the matched aliases are displayed
* @param note_list_id The div block identifier where the notes of the buyers are displayed * @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param notes An array containing the note objects of the buyers * @param notes An array containing the note objects of the buyers
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
@ -179,77 +200,87 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not
* the associated note is not displayed. * the associated note is not displayed.
* Useful for a consumption if the item is selected before. * Useful for a consumption if the item is selected before.
*/ */
function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias", function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_prefix = "alias",
note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) { note_prefix = "note", user_note_field = null, profile_pic_field = null, alias_click = null) {
let field = $("#" + field_id); let field = $("#" + field_id);
// When the user clicks on the search field, it is immediately cleared
field.click(function() { // Configure tooltip
field.tooltip({
html: true,
placement: 'bottom',
title: 'Loading...',
trigger: 'manual',
container: field.parent()
});
// Clear search on click
field.click(function () {
field.tooltip('hide');
field.val(""); field.val("");
}); });
let old_pattern = null; let old_pattern = null;
// When the user type "Enter", the first alias is clicked, and the informations are displayed // When the user type "Enter", the first alias is clicked
field.keypress(function(event) { field.keypress(function (event) {
if (event.originalEvent.charCode === 13) { if (event.originalEvent.charCode === 13 && notes.length > 0) {
let li_obj = $("#" + alias_matched_id + " li").first(); let li_obj = field.parent().find("ul li").first();
displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field);
li_obj.trigger("click"); li_obj.trigger("click");
} }
}); });
// When the user type something, the matched aliases are refreshed // When the user type something, the matched aliases are refreshed
field.keyup(function(e) { field.keyup(function (e) {
if (e.originalEvent.charCode === 13) if (e.originalEvent.charCode === 13)
return; return;
let pattern = field.val(); let pattern = field.val();
// If the pattern is not modified, we don't query the API // If the pattern is not modified, we don't query the API
if (pattern === old_pattern || pattern === "") if (pattern === old_pattern)
return; return;
old_pattern = pattern; old_pattern = pattern;
// Clear old matched notes
notes.length = 0; notes.length = 0;
let aliases_matched_obj = $("#" + alias_matched_id); // get matched Alias with note associated
let aliases_matched_html = ""; if (pattern === "") {
field.tooltip('hide');
notes.length = 0;
return;
}
// Get matched notes with the given pattern $.getJSON("/api/note/consumer/?format=json&alias="
getMatchedNotes(pattern, function(aliases) { + pattern
+ "&search=user|club&ordering=normalized_name",
function (consumers) {
// The response arrived too late, we stop the request // The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val()) if (pattern !== $("#" + field_id).val())
return; return;
aliases.results.forEach(function (alias) { // Build tooltip content
let note = alias.note; let aliases_matched_html = '<ul class="list-group list-group-flush">';
note = { consumers.results.forEach(function (consumer) {
id: note, let note = consumer.note;
name: alias.name, note.email_confirmed = consumer.email_confirmed;
alias: alias, let extra_css = displayStyle(note);
balance: null aliases_matched_html += li(alias_prefix + '_' + consumer.id,
}; consumer.name,
aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name); extra_css);
notes.push(note); notes.push(note);
}); });
aliases_matched_html += '</ul>';
// Display the list of matched aliases // Show tooltip
aliases_matched_obj.html(aliases_matched_html); field.attr('data-original-title', aliases_matched_html).tooltip('show');
notes.forEach(function (note) { consumers.results.forEach(function (consumer) {
let alias = note.alias; let note = consumer.note;
let alias_obj = $("#" + alias_prefix + "_" + alias.id); let consumer_obj = $("#" + alias_prefix + "_" + consumer.id);
// When an alias is hovered, the profile picture and the balance are displayed at the right place consumer_obj.hover(function () {
alias_obj.hover(function () { displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
displayNote(note, alias.name, user_note_field, profile_pic_field);
}); });
consumer_obj.click(function () {
// When the user click on an alias, the associated note is added to the emitters
alias_obj.click(function () {
field.val("");
old_pattern = "";
// If the note is already an emitter, we increase the quantity
var disp = null; var disp = null;
notes_display.forEach(function (d) { notes_display.forEach(function (d) {
// We compare the note ids // We compare the note ids
@ -261,8 +292,8 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
// In the other case, we add a new emitter // In the other case, we add a new emitter
if (disp == null) { if (disp == null) {
disp = { disp = {
name: alias.name, name: consumer.name,
id: note.id, id: consumer.id,
note: note, note: note,
quantity: 1 quantity: 1
}; };
@ -277,13 +308,19 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
let note_list = $("#" + note_list_id); let note_list = $("#" + note_list_id);
let html = ""; let html = "";
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id, disp.name html += li(note_prefix + "_" + disp.id,
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); disp.name
+ "<span class=\"badge badge-dark badge-pill\">"
+ disp.quantity + "</span>",
displayStyle(disp.note));
}); });
// Emitters are displayed // Emitters are displayed
note_list.html(html); note_list.html(html);
// Update tooltip position
field.tooltip('update');
notes_display.forEach(function (disp) { notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id); let line_obj = $("#" + note_prefix + "_" + disp.id);
// Hover an emitter display also the profile picture // Hover an emitter display also the profile picture
@ -295,27 +332,13 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field)); profile_pic_field));
}); });
})
}); });
});
});
});
}
// When a validate button is clicked, we switch the validation status $("#validate_" + id).html("<i class='fa fa-spinner'></i>");
function in_validate(id, validated) {
let invalidity_reason;
let reason_obj = $("#invalidity_reason_" + id);
if (validated)
invalidity_reason = reason_obj.val();
else
invalidity_reason = null;
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
// Perform a PATCH request to the API in order to update the transaction // Perform a PATCH request to the API in order to update the transaction
// If the user has insuffisent rights, an error message will appear // If the user has insufficient rights, an error message will appear
$.ajax({ $.ajax({
"url": "/api/note/transaction/transaction/" + id + "/", "url": "/api/note/transaction/transaction/" + id + "/",
type: "PATCH", type: "PATCH",
@ -324,19 +347,19 @@ function in_validate(id, validated) {
"X-CSRFTOKEN": CSRF_TOKEN "X-CSRFTOKEN": CSRF_TOKEN
}, },
data: { data: {
resourcetype: "RecurrentTransaction", "resourcetype": "RecurrentTransaction",
valid: !validated, "valid": !validated,
invalidity_reason: invalidity_reason, "invalidity_reason": invalidity_reason,
}, },
success: function () { success: function () {
// Refresh jQuery objects // Refresh jQuery objects
$(".validate").click(in_validate); $(".validate").click(de_validate);
refreshBalance(); refreshBalance();
// error if this method doesn't exist. Please define it. // error if this method doesn't exist. Please define it.
refreshHistory(); refreshHistory();
}, },
error: function(err) { error: function (err) {
addMsg("Une erreur est survenue lors de la validation/dévalidation " + addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + err.responseText, "danger"); "de cette transaction : " + err.responseText, "danger");

View File

@ -24,13 +24,10 @@ $(document).ready(function() {
}); });
// Switching in double consumptions mode should update the layout // Switching in double consumptions mode should update the layout
let double_conso_obj = $("#double_conso"); $("#double_conso").click(function() {
double_conso_obj.click(function() { $("#consos_list_div").removeClass('d-none');
$("#consos_list_div").show();
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
$("#note_infos_div").attr('class', 'col-xl-3');
$("#user_select_div").attr('class', 'col-xl-4'); $("#user_select_div").attr('class', 'col-xl-4');
$("#buttons_div").attr('class', 'col-sm-7 col-xl-6'); $("#infos_div").attr('class', 'col-sm-5 col-xl-6');
let note_list_obj = $("#note_list"); let note_list_obj = $("#note_list");
if (buttons.length > 0 && note_list_obj.text().length > 0) { if (buttons.length > 0 && note_list_obj.text().length > 0) {
@ -44,13 +41,10 @@ $(document).ready(function() {
} }
}); });
let single_conso_obj = $("#single_conso"); $("#single_conso").click(function() {
single_conso_obj.click(function() { $("#consos_list_div").addClass('d-none');
$("#consos_list_div").hide();
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
$("#note_infos_div").attr('class', 'col-xl-5');
$("#user_select_div").attr('class', 'col-xl-7'); $("#user_select_div").attr('class', 'col-xl-7');
$("#buttons_div").attr('class', 'col-sm-7 col-md-8'); $("#infos_div").attr('class', 'col-sm-5 col-md-4');
let consos_list_obj = $("#consos_list"); let consos_list_obj = $("#consos_list");
if (buttons.length > 0) { if (buttons.length > 0) {
@ -69,12 +63,8 @@ $(document).ready(function() {
} }
}); });
// Ensure we begin in single consumption. Removing these lines may cause problems when reloading. // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
single_conso_obj.prop('checked', 'true'); $("label[for='double_conso']").removeClass('active');
double_conso_obj.removeAttr('checked');
$("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary');
$("#consos_list_div").hide();
$("#consume_all").click(consumeAll); $("#consume_all").click(consumeAll);
}); });
@ -84,7 +74,7 @@ notes_display = [];
buttons = []; buttons = [];
// When the user searches an alias, we update the auto-completion // When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display, autoCompleteNote("note", "note_list", notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() { "alias", "note", "user_note", "profile_pic", function() {
if (buttons.length > 0 && $("#single_conso").is(":checked")) { if (buttons.length > 0 && $("#single_conso").is(":checked")) {
consumeAll(); consumeAll();
@ -152,7 +142,6 @@ function reset() {
notes.length = 0; notes.length = 0;
buttons.length = 0; buttons.length = 0;
$("#note_list").html(""); $("#note_list").html("");
$("#alias_matched").html("");
$("#consos_list").html(""); $("#consos_list").html("");
$("#user_note").text(""); $("#user_note").text("");
$("#profile_pic").attr("src", "/media/pic/default.png"); $("#profile_pic").attr("src", "/media/pic/default.png");
@ -167,7 +156,7 @@ function reset() {
function consumeAll() { function consumeAll() {
notes_display.forEach(function(note_display) { notes_display.forEach(function(note_display) {
buttons.forEach(function(button) { buttons.forEach(function(button) {
consume(note_display.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, consume(note_display.note.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
}); });
}); });

View File

@ -14,8 +14,6 @@ function reset() {
dests.length = 0; dests.length = 0;
$("#source_note_list").html(""); $("#source_note_list").html("");
$("#dest_note_list").html(""); $("#dest_note_list").html("");
$("#source_alias_matched").html("");
$("#dest_alias_matched").html("");
$("#amount").val(""); $("#amount").val("");
$("#reason").val(""); $("#reason").val("");
$("#last_name").val(""); $("#last_name").val("");
@ -28,14 +26,20 @@ function reset() {
} }
$(document).ready(function() { $(document).ready(function() {
autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display, /**
"source_alias", "source_note", "user_note", "profile_pic"); * If we are in credit/debit mode, check that only one note is entered.
autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display, * More over, get first name and last name to autocomplete fields.
"dest_alias", "dest_note", "user_note", "profile_pic", function() { */
function checkUniqueNote() {
if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) {
let last = dests_notes_display[dests_notes_display.length - 1]; let arr = $("#type_credit").is(":checked") ? dests_notes_display : sources_notes_display;
dests_notes_display.length = 0;
dests_notes_display.push(last); if (arr.length === 0)
return;
let last = arr[arr.length - 1];
arr.length = 0;
arr.push(last);
last.quantity = 1; last.quantity = 1;
@ -57,8 +61,75 @@ $(document).ready(function() {
} }
return true; return true;
}
autoCompleteNote("source_note", "source_note_list", sources, sources_notes_display,
"source_alias", "source_note", "user_note", "profile_pic", checkUniqueNote);
autoCompleteNote("dest_note", "dest_note_list", dests, dests_notes_display,
"dest_alias", "dest_note", "user_note", "profile_pic", checkUniqueNote);
let source = $("#source_note");
let dest = $("#dest_note");
$("#type_gift").click(function() {
$("#special_transaction_div").addClass('d-none');
source.attr('disabled', true);
source.val(username);
source.tooltip('hide');
$("#source_note_list").addClass('d-none');
dest.attr('disabled', false);
$("#dest_note_list").removeClass('d-none');
}); });
$("#type_transfer").click(function() {
$("#special_transaction_div").addClass('d-none');
source.attr('disabled', false);
$("#source_note_list").removeClass('d-none');
dest.attr('disabled', false);
$("#dest_note_list").removeClass('d-none');
});
$("#type_credit").click(function() {
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").addClass('d-none');
$("#dest_note_list").removeClass('d-none');
source.attr('disabled', true);
source.val($("#credit_type option:selected").text());
source.tooltip('hide');
dest.attr('disabled', false);
dest.val('');
dest.tooltip('hide');
if (dests_notes_display.length > 1) {
$("#dest_note_list").html('');
dests_notes_display.length = 0;
}
});
$("#type_debit").click(function() {
$("#special_transaction_div").removeClass('d-none');
$("#source_note_list").removeClass('d-none');
$("#dest_note_list").addClass('d-none');
source.attr('disabled', false);
source.val('');
source.tooltip('hide');
dest.attr('disabled', true);
dest.val($("#credit_type option:selected").text());
dest.tooltip('hide');
if (sources_notes_display.length > 1) {
$("#source_note_list").html('');
sources_notes_display.length = 0;
}
});
$("#credit_type").change(function() {
let type = $("#credit_type option:selected").text();
if ($("#type_credit").is(":checked"))
source.val(type);
else
dest.val(type);
});
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading. // Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
let type_gift = $("#type_gift"); // Default mode let type_gift = $("#type_gift"); // Default mode
@ -91,7 +162,7 @@ $("#btn_transfer").click(function() {
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction", "resourcetype": "Transaction",
"source": user_id, "source": user_id,
"destination": dest.id, "destination": dest.note.id,
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
addMsg("Le transfert de " addMsg("Le transfert de "
@ -99,7 +170,7 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a été fait avec succès !", "success"); + " vers la note " + dest.name + " a été fait avec succès !", "success");
reset(); reset();
}).fail(function () { }).fail(function () { // do it again but valid = false
$.post("/api/note/transaction/transaction/", $.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
@ -111,7 +182,7 @@ $("#btn_transfer").click(function() {
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction", "resourcetype": "Transaction",
"source": user_id, "source": user_id,
"destination": dest.id, "destination": dest.note.id,
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
addMsg("Le transfert de " addMsg("Le transfert de "
@ -141,9 +212,9 @@ $("#btn_transfer").click(function() {
"valid": true, "valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction", "resourcetype": "Transaction",
"source": source.id, "source": source.note.id,
"source_alias": source.name, "source_alias": source.name,
"destination": dest.id, "destination": dest.note.id,
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
addMsg("Le transfert de " addMsg("Le transfert de "
@ -151,7 +222,7 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a été fait avec succès !", "success"); + " vers la note " + dest.name + " a été fait avec succès !", "success");
reset(); reset();
}).fail(function (err) { }).fail(function (err) { // do it again but valid = false
$.post("/api/note/transaction/transaction/", $.post("/api/note/transaction/transaction/",
{ {
"csrfmiddlewaretoken": CSRF_TOKEN, "csrfmiddlewaretoken": CSRF_TOKEN,
@ -162,9 +233,9 @@ $("#btn_transfer").click(function() {
"invalidity_reason": "Solde insuffisant", "invalidity_reason": "Solde insuffisant",
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction", "resourcetype": "Transaction",
"source": source.id, "source": source.note.id,
"source_alias": source.name, "source_alias": source.name,
"destination": dest.id, "destination": dest.note.id,
"destination_alias": dest.name "destination_alias": dest.name
}).done(function () { }).done(function () {
addMsg("Le transfert de " addMsg("Le transfert de "
@ -184,10 +255,11 @@ $("#btn_transfer").click(function() {
}); });
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val(); let special_note = $("#credit_type").val();
let user_note = dests_notes_display[0].id; let user_note;
let given_reason = $("#reason").val(); let given_reason = $("#reason").val();
let source, dest, reason; let source, dest, reason;
if ($("#type_credit").is(':checked')) { if ($("#type_credit").is(':checked')) {
user_note = dests_notes_display[0].note.id;
source = special_note; source = special_note;
dest = user_note; dest = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
@ -195,6 +267,7 @@ $("#btn_transfer").click(function() {
reason += " (" + given_reason + ")"; reason += " (" + given_reason + ")";
} }
else { else {
user_note = sources_notes_display[0].note.id;
source = user_note; source = user_note;
dest = special_note; dest = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();

8
templates/400.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Bad request" %}</h1>
{% blocktrans %}Sorry, your request was bad. Don't know what could be wrong. An email has been sent to webmasters with the details of the error. You can now drink a coke.{% endblocktrans %}
{% endblock %}

13
templates/403.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Permission denied" %}</h1>
{% blocktrans %}You don't have the right to perform this request.{% endblocktrans %}
{% if exception %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

13
templates/404.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Page not found" %}</h1>
{% blocktrans %}The requested path <code>{{ request_path }}</code> was not found on the server.{% endblocktrans %}
{% if exception != "Resolver404" %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

8
templates/500.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Server error" %}</h1>
{% blocktrans %}Sorry, an error occurred when processing your request. An email has been sent to webmasters with the detail of the error, and this will be fixed soon. You can now drink a beer.{% endblocktrans %}
{% endblock %}

View File

@ -31,8 +31,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous"> crossorigin="anonymous">
<link rel="stylesheet" <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css">
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css">
{# JQuery, Bootstrap and Turbolinks JavaScript #} {# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js" <script src="https://code.jquery.com/jquery-3.4.1.min.js"
@ -58,6 +58,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
.tooltip.show {
opacity: 1; /* opaque tooltip */
}
.tooltip-inner {
background-color: #fff;
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
border: 1px solid rgba(0,0,0,.250);
color: #000;
margin: 0 .5rem .25rem .5rem;
padding: 0;
}
.bs-tooltip-bottom .arrow::before {
border-bottom-color: rgba(0,0,0,.250);
}
</style> </style>
{% block extracss %}{% endblock %} {% block extracss %}{% endblock %}
@ -76,39 +90,52 @@ SPDX-License-Identifier: GPL-3.0-or-later
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if "note.transactiontemplate"|not_empty_model_list %} {% if "note.transactiontemplate"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> <a class="nav-link" href="{% url 'note:consos' %}"><i class="fas fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "note.transaction"|not_empty_model_list %} {% if "note.transaction"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a>
</li> </li>
{% endif %} {% endif %}
{% if "auth.user"|model_list|length >= 2 %} {% if "auth.user"|model_list|length >= 2 %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fa fa-user"></i> {% trans 'Users' %}</a> <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "member.club"|not_empty_model_list %} {% if "member.club"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "member.change_profile_registration_valid"|has_perm:user %} {% if "member.change_profile_registration_valid"|has_perm:user %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'registration:future_user_list' %}"> <a class="nav-link" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %} <i class="fas fa-user-plus"></i> {% trans "Registrations" %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if "activity.activity"|not_empty_model_list %} {% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "treasury.invoice"|not_empty_model_change_list %} {% if "treasury.invoice"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a> <a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fas fa-credit-card"></i> {% trans 'Treasury' %}</a>
</li>
{% endif %}
{% if "wei.weiclub"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
</li>
{% if user.is_staff %}
<li class="nav-item active">
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Administration' %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -116,28 +143,28 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li class="dropdown"> <li class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user"></i> <i class="fas fa-user"></i>
<span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span> <span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" <div class="dropdown-menu dropdown-menu-right"
aria-labelledby="navbarDropdownMenuLink"> aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'member:user_detail' pk=user.pk %}"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=user.pk %}">
<i class="fa fa-user"></i> Mon compte <i class="fas fa-user"></i> Mon compte
</a> </a>
<a class="dropdown-item" href="{% url 'logout' %}"> <a class="dropdown-item" href="{% url 'logout' %}">
<i class="fa fa-sign-out"></i> Se déconnecter <i class="fas fa-sign-out-alt"></i> Se déconnecter
</a> </a>
</div> </div>
</li> </li>
{% else %} {% else %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'registration:signup' %}"> <a class="nav-link" href="{% url 'registration:signup' %}">
<i class="fa fa-user-plus"></i> S'inscrire <i class="fas fa-user-plus"></i> S'inscrire
</a> </a>
</li> </li>
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'login' %}"> <a class="nav-link" href="{% url 'login' %}">
<i class="fa fa-sign-in"></i> Se connecter <i class="fas fa-sign-in-alt"></i> Se connecter
</a> </a>
</li> </li>
{% endif %} {% endif %}

View File

@ -1,26 +0,0 @@
{% load cas_server %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
{{error}}
</div>
{% endfor %}
{% for field in form %}{% if not field|is_hidden %}
<div class="form-group
{% if not form.non_field_errors %}
{% if field.errors %} has-error
{% elif form.cleaned_data %} has-success
{% endif %}
{% endif %}"
>{% spaceless %}
{% if field|is_checkbox %}
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div>
{% else %}
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
{{field}}
{% endif %}
{% for error in field.errors %}
<span class="help-block">{{error}}</span>
{% endfor %}
{% endspaceless %}</div>
{% else %}{{field}}{% endif %}{% endfor %}

View File

@ -1,21 +0,0 @@
{% extends "cas_server/base.html" %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
<form class="form-signin" method="get" action="logout">
<div class="checkbox">
<label>
<input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %}
</label>
</div>
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
<div class="checkbox">
<label>
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
</label>
</div>
{% endif %}
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
</form>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "cas_server/base.html" %}
{% load i18n %}
{% block ante_messages %}
{% if auto_submit %}<noscript>{% endif %}
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
{% if auto_submit %}</noscript>{% endif %}
{% endblock %}
{% block content %}
<div class="alert alert-warning">
{% trans "If you don't have any Note Kfet account, please follow <a href='/accounts/signup'>this link to sign up</a>." %}
</div>
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
{% csrf_token %}
{% include "cas_server/form.html" %}
{% if auto_submit %}<noscript>{% endif %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
{% if auto_submit %}</noscript>{% endif %}
</form>
{% endblock %}
{% block javascript_inline %}
jQuery(function( $ ){
$("#id_warn").click(function(e){
if($("#id_warn").is(':checked')){
createCookie("warn", "on", 10 * 365);
} else {
eraseCookie("warn");
}
});
});{% if auto_submit %}
document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends "cas_server/base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{{logout_msg}}</div>
{% endblock %}

View File

@ -1,5 +0,0 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:proxySuccess>
<cas:proxyTicket>{{ticket}}</cas:proxyTicket>
</cas:proxySuccess>
</cas:serviceResponse>

View File

@ -1,59 +0,0 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"
IssueInstant="{{ IssueInstant }}"
MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}"
ResponseID="{{ ResponseID }}">
<Status>
<StatusCode Value="samlp:Success">
</StatusCode>
</Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="{{ResponseID}}"
IssueInstant="{{IssueInstant}}" Issuer="localhost" MajorVersion="1"
MinorVersion="1">
<Conditions NotBefore="{{IssueInstant}}" NotOnOrAfter="{{expireInstant}}">
<AudienceRestrictionCondition>
<Audience>
{{Recipient}}
</Audience>
</AudienceRestrictionCondition>
</Conditions>
<AttributeStatement>
<Subject>
<NameIdentifier>{{username}}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
<Attribute AttributeName="authenticationDate" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{auth_date}}</AttributeValue>
</Attribute>
<Attribute AttributeName="longTermAuthenticationRequestTokenUsed" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>false</AttributeValue>{# we do not support long-term (Remember-Me) auth #}
</Attribute>
<Attribute AttributeName="isFromNewLogin" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{is_new_login}}</AttributeValue>
</Attribute>
{% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{value}}</AttributeValue>
</Attribute>
{% endfor %} </AttributeStatement>
<AuthenticationStatement AuthenticationInstant="{{IssueInstant}}"
AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password">
<Subject>
<NameIdentifier>{{username}}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
</AuthenticationStatement>
</Assertion>
</Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View File

@ -1,14 +0,0 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"
IssueInstant="{{ IssueInstant }}"
MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}"
ResponseID="{{ ResponseID }}">
<Status>
<StatusCode Value="samlp:{{code}}">{{msg}}</StatusCode>
</Status>
</Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View File

@ -1,19 +0,0 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationSuccess>
<cas:user>{{username}}</cas:user>
<cas:attributes>
<cas:authenticationDate>{{auth_date}}</cas:authenticationDate>
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>{# we do not support long-term (Remember-Me) auth #}
<cas:isFromNewLogin>{{is_new_login}}</cas:isFromNewLogin>
{% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}>
{% endfor %} </cas:attributes>
<cas:attribute name="authenticationDate" value="{{auth_date}}"/>
<cas:attribute name="longTermAuthenticationRequestTokenUsed" value="false"/>
<cas:attribute name="isFromNewLogin" value="{{is_new_login}}"/>
{% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/>
{% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket>
{% endif %}{% if proxies %} <cas:proxies>
{% for proxy in proxies %} <cas:proxy>{{proxy}}</cas:proxy>
{% endfor %} </cas:proxies>
{% endif %} </cas:authenticationSuccess>
</cas:serviceResponse>

View File

@ -1,3 +0,0 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationFailure code="{{code}}">{{msg}}</cas:authenticationFailure>
</cas:serviceResponse>

View File

@ -1,11 +0,0 @@
{% extends "cas_server/base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
<form class="form-signin" method="post">
{% csrf_token %}
{% include "cas_server/form.html" %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,8 @@
<input type="text"
id="{{ widget.attrs.id }}"
class="form-control colorfield_field jscolor"
name="{{ widget.name }}"
value="{% firstof widget.value widget.attrs.default '' %}"
placeholder="{% firstof widget.value widget.attrs.default '' %}"
data-jscolor="{hash:true,width:225,height:150,required:{% if widget.attrs.required %}true{% else %}false{% endif %}}"
{% if widget.attrs.required %}required{% endif %} />

View File

@ -15,18 +15,24 @@
{% if club.parent_club %} {% if club.parent_club %}
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt> <dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd> <dd class="col-xl-6"> {{ club.parent_club.name }}</dd>
{% endif %} {% endif %}
{% if club.require_memberships %} {% if club.require_memberships %}
{% if club.membership_start %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd> <dd class="col-xl-6">{{ club.membership_start }}</dd>
{% endif %}
{% if club.membership_end %}
<dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_end }}</dd> <dd class="col-xl-6">{{ club.membership_end }}</dd>
{% endif %}
{% if club.membership_duration %}
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd> <dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd>
{% endif %}
{% if club.membership_fee_paid == club.membership_fee_unpaid %} {% if club.membership_fee_paid == club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
@ -40,13 +46,19 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> {% if "note.view_note"|has_perm:club.note %}
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
{% endif %}
<dt class="col-xl-3">{% trans 'email'|capfirst %}</dt> <dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd> <dd class="col-xl-6 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd>
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-8"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
</dl> </dl>
</div> </div>
{% if not club.weiclub %}
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if can_add_members %} {% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a>
@ -57,5 +69,7 @@
{% url 'member:club_detail' club.pk as club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.path_info != club_detail_url %} {%if request.path_info != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} </div> {% endif %}
</div>
{% endif %}
</div> </div>

View File

@ -16,7 +16,7 @@
<div class="col-md-10"> <div class="col-md-10">
<div class="card card-border shadow"> <div class="card card-border shadow">
<div class="card-header text-center"> <div class="card-header text-center">
<h5> {% trans "club listing "%}</h5> <h5> {% trans "Club listing" %}</h5>
</div> </div>
<div class="card-body px-0 py-0" id="club_table"> <div class="card-body px-0 py-0" id="club_table">
{% render_table table %} {% render_table table %}

View File

@ -1,17 +1,20 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %} {% load i18n %}
<div class="card"> {% if member_list.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold"> <a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-users"></i> {% trans "Member of the Club" %} <i class="fa fa-users"></i> {% trans "Member of the Club" %}
</a> </a>
</div> </div>
{% render_table member_list %} {% render_table member_list %}
</div> </div>
<hr> <hr>
{% endif %}
<div class="card"> {% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold"> <a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-euro"></i> {% trans "Transaction history" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
@ -20,4 +23,5 @@
<div id="history_list"> <div id="history_list">
{% render_table history_list %} {% render_table history_list %}
</div> </div>
</div> </div>
{% endif %}

View File

@ -6,11 +6,11 @@
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-md-3 mb-4"> <div class="col-xl-4">
{% block profile_info %} {% block profile_info %}
{% endblock %} {% endblock %}
</div> </div>
<div class="col-md-9"> <div class="col-xl-8">
{% block profile_content %} {% block profile_content %}
{% endblock %} {% endblock %}
</div> </div>
@ -19,9 +19,11 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
{% if object %}
function refreshHistory() { function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
} }
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -17,12 +17,14 @@
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.username }}</dd> <dd class="col-xl-6">{{ object.username }}</dd>
{% if object.pk == user.pk %}
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="small" href="{% url 'password_change' %}"> <a class="small" href="{% url 'password_change' %}">
{% trans 'Change password' %} {% trans 'Change password' %}
</a> </a>
</dd> </dd>
{% endif %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.section }}</dd> <dd class="col-xl-6">{{ object.profile.section }}</dd>

View File

@ -1,6 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load crispy_forms_tags%} {% load crispy_forms_tags %}
{% load i18n %}
{% block content %} {% block content %}
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ..."> <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
@ -11,7 +13,7 @@
{% render_table table %} {% render_table table %}
{% else %} {% else %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% trans "There is no pending user with this pattern." %} {% trans "There is no user with this pattern." %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -10,10 +10,10 @@
<div class="col-sm-5 col-md-4" id="infos_div"> <div class="col-sm-5 col-md-4" id="infos_div">
<div class="row"> <div class="row">
{# User details column #} {# User details column #}
<div class="col-xl-5" id="note_infos_div"> <div class="col">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4 text-center">
<img src="/media/pic/default.png" <img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> id="profile_pic" alt="" class="card-img-top">
<div class="card-body text-center"> <div class="card-body text-center">
<span id="user_note"></span> <span id="user_note"></span>
</div> </div>
@ -25,38 +25,45 @@
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "Select emitters" %} {% trans "Consum" %}
</p> </p>
</div> </div>
<div class="card-body p-0" style="min-height:125px;">
<ul class="list-group list-group-flush" id="note_list"> <ul class="list-group list-group-flush" id="note_list">
</ul> </ul>
<div class="card-body"> </div>
<input class="form-control mx-auto d-block" type="text" id="note" />
<ul class="list-group list-group-flush" id="alias_matched"> {# User search with autocompletion #}
</ul> <div class="card-footer">
<input class="form-control mx-auto d-block"
placeholder="{% trans "Name or alias..." %}" type="text" id="note" autofocus />
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-5" id="consos_list_div"> <div class="col-xl-5 d-none" id="consos_list_div">
<div class="card border-info shadow mb-4"> <div class="card border-info shadow mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "Select consumptions" %} {% trans "Select consumptions" %}
</p> </p>
</div> </div>
<div class="card-body p-0" style="min-height:125px;">
<ul class="list-group list-group-flush" id="consos_list"> <ul class="list-group list-group-flush" id="consos_list">
</ul> </ul>
<button id="consume_all" class="form-control btn btn-primary"> </div>
<div class="card-footer text-center">
<a id="consume_all" href="#" class="btn btn-primary">
{% trans "Consume!" %} {% trans "Consume!" %}
</button> </a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{# Buttons column #} {# Buttons column #}
<div class="col-sm-7 col-md-8" id="buttons_div"> <div class="col">
{# Show last used buttons #} {# Show last used buttons #}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header"> <div class="card-header">
@ -123,10 +130,12 @@
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons"> <div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
<label for="single_conso" class="btn btn-sm btn-outline-primary active"> <label for="single_conso" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="conso_type" id="single_conso" checked> <input type="radio" name="conso_type" id="single_conso" checked>
<i class="fa fa-long-arrow-left" aria-hidden="true"></i>
{% trans "Single consumptions" %} {% trans "Single consumptions" %}
</label> </label>
<label for="double_conso" class="btn btn-sm btn-outline-primary"> <label for="double_conso" class="btn btn-sm btn-outline-primary">
<input type="radio" name="conso_type" id="double_conso"> <input type="radio" name="conso_type" id="double_conso">
<i class="fa fa-arrows-h" aria-hidden="true"></i>
{% trans "Double consumptions" %} {% trans "Double consumptions" %}
</label> </label>
</div> </div>
@ -146,7 +155,7 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript" src="/static/js/consos.js"></script> <script type="text/javascript" src="{% static "js/consos.js" %}"></script>
<script type="text/javascript"> <script type="text/javascript">
{% for button in most_used %} {% for button in most_used %}
{% if button.display %} {% if button.display %}

View File

@ -38,8 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-3" id="note_infos_div">
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<img src="/media/pic/default.png" <img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
@ -48,7 +47,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4" id="emitters_div" style="display: none;">
<div class="col-md-3" id="emitters_div">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
@ -58,24 +58,53 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ul class="list-group list-group-flush" id="source_note_list"> <ul class="list-group list-group-flush" id="source_note_list">
</ul> </ul>
<div class="card-body"> <div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="source_note" /> <input class="form-control mx-auto d-block" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" />
<ul class="list-group list-group-flush" id="source_alias_matched"> </div>
</div>
</div>
<div class="col-md-3" id="dests_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold" id="dest_title">
{% trans "Select receivers" %}
</p>
</div>
<ul class="list-group list-group-flush" id="dest_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="dest_note" placeholder="{% trans "Name or alias..." %}" />
<ul class="list-group list-group-flush" id="dest_alias_matched">
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
{% if "note.notespecial"|not_empty_model_list %} <div class="col-md-3" id="external_div">
<div class="col-md-4" id="external_div" style="display: none;"> <div class="card border-warning shadow mb-4">
<div class="card border-success shadow mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "External payment" %} {% trans "Action" %}
</p> </p>
</div> </div>
<ul class="list-group list-group-flush" id="source_note_list"> <ul class="list-group list-group-flush" id="source_note_list">
</ul> </ul>
<div class="card-body"> <div class="card-body">
<div class="form-row">
<div class="col-md-12">
<label for="amount">{% trans "Amount" %} :</label>
{% include "note/amount_input.html" with widget=amount_widget %}
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="reason">{% trans "Reason" %} :</label>
<input class="form-control mx-auto d-block" type="text" id="reason" required />
</div>
</div>
<div class="d-none" id="special_transaction_div">
<div class="form-row"> <div class="form-row">
<div class="col-md-12"> <div class="col-md-12">
<label for="credit_type">{% trans "Transfer type" %} :</label> <label for="credit_type">{% trans "Transfer type" %} :</label>
@ -105,46 +134,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
</div> </div>
</div> <hr>
</div>
{% endif %}
<div class="col-md-8" id="dests_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold" id="dest_title">
{% trans "Select receivers" %}
</p>
</div>
<ul class="list-group list-group-flush" id="dest_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="dest_note" />
<ul class="list-group list-group-flush" id="dest_alias_matched">
</ul>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="amount">{% trans "Amount" %} :</label>
{% include "note/amount_input.html" with widget=amount_widget %}
</div>
<div class="form-group col-md-6">
<label for="reason">{% trans "Reason" %} :</label>
<input class="form-control mx-auto d-block" type="text" id="reason" required />
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="col-md-12"> <div class="col-md-12">
<button id="btn_transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button> <button id="btn_transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
<div class="card shadow mb-4" id="history"> <div class="card shadow mb-4" id="history">
<div class="card-header"> <div class="card-header">
@ -161,34 +160,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }}; TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }}; SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
user_id = {{ user.note.pk }}; user_id = {{ user.note.pk }};
username = "{{ user.username }}";
$("#type_gift").click(function() {
$("#emitters_div").hide();
$("#external_div").hide();
$("#dests_div").attr('class', 'col-md-8');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_transfer").click(function() {
$("#external_div").hide();
$("#emitters_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_credit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Credit note" %}");
});
$("#type_debit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Debit note" %}");
});
</script> </script>
<script src="/static/js/transfer.js"></script> <script src="/static/js/transfer.js"></script>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,28 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load pretty_money %}
{% block content %} {% block content %}
<p><a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Buttons list" %}</a></p> <p>
<form method="post"> <a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Buttons list" %}</a>
{% csrf_token %} </p>
{{form|crispy}} <form method="post">
<button class="btn btn-primary" type="submit">Submit</button> {% csrf_token %}
</form> {{form|crispy}}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% if price_history and price_history.1 %}
<hr>
<h4>{% trans "Price history" %}</h4>
<ul>
{% for price in price_history %}
<li>{{ price.price|pretty_money }} {% if price.time %}({% trans "Obsolete since" %} {{ price.time }}){% else %}({% trans "Current price" %}){% endif %}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -6,9 +6,17 @@
<div class="row justify-content-center mb-4"> <div class="row justify-content-center mb-4">
<div class="col-md-10 text-center"> <div class="col-md-10 text-center">
<h4> <h4>
{% trans "search button" %} {% trans "Search button" %}
</h4> </h4>
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/> <input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}">
<div class="form-group">
<div id="div_active_only" class="form-check">
<label for="active_only" class="form-check-label">
<input type="checkbox" name="active_only" class="checkboxinput form-check-input" checked="" id="active_only">
{% trans "Display visible buttons only" %}
</label>
</div>
</div>
<hr> <hr>
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a> <a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a>
</div> </div>
@ -29,38 +37,46 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
/* fonction appelée à la fin du timer */ /* fonction appelée à la fin du timer */
function getInfo() { function getInfo() {
var asked = $("#search_field").val(); var asked = $("#search_field").val();
/* on ne fait la requête que si on a au moins un caractère pour chercher */ /* on ne fait la requête que si on a au moins un caractère pour chercher */
var sel = $(".table-row");
if (asked.length >= 1) { if (asked.length >= 1) {
$.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){ $.getJSON("/api/note/transaction/template/?format=json&search=" + asked + ($("#active_only").is(":checked") ? "&display=true" : ""), function(buttons) {
let selected_id = buttons.results.map((a => "#row-"+a.id)); console.log(buttons);
$(".table-row,"+selected_id.join()).show(); let selected_id = buttons.results.map((a => "#row-" + a.id));
$(".table-row").not(selected_id.join()).hide(); console.log(".table-row " + selected_id.join());
$(".table-row " + selected_id.join()).removeClass('d-none');
$(".table-row").not(selected_id.join()).addClass('d-none');
}); });
}else{
// show everything
$('table tr').show();
} }
} else {
var timer; if ($("#active_only").is(":checked")) {
var timer_on; $('.table-success').removeClass('d-none');
/* Fontion appelée quand le texte change (délenche le timer) */ $('.table-danger').addClass('d-none');
function search_field_moved(secondfield) { }
else {
// show everything
$('table tr').removeClass('d-none');
}
}
}
var timer;
var timer_on;
/* Fontion appelée quand le texte change (délenche le timer) */
function search_field_moved() {
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout("getInfo(" + secondfield + ")", 300); timer = setTimeout(getInfo, 300);
} }
else { // Sinon, on le lance et on enregistre le fait qu'il tourne. else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout("getInfo(" + secondfield + ")", 300); timer = setTimeout(getInfo, 300);
timer_on = true; timer_on = true;
} }
} }
// on click of button "delete" , call the API // on click of button "delete" , call the API
function delete_button(button_id){ function delete_button(button_id) {
$.ajax({ $.ajax({
url:"/api/note/transaction/template/"+button_id+"/", url:"/api/note/transaction/template/"+button_id+"/",
method:"DELETE", method:"DELETE",
@ -74,5 +90,12 @@ function search_field_moved(secondfield) {
addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' ) addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' )
}); });
} }
$(document).ready(function() {
$("#search_field").keyup(search_field_moved);
$("#active_only").change(search_field_moved);
search_field_moved();
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if user.is_authenticated %}
<div class="form-check">
<label for="owned_only" class="form-check-label">
<input id="owned_only" name="owned_only" type="checkbox" class="checkboxinput form-check-input">
{% trans "Filter with roles that I have in at least one club" %}
</label>
</div>
{% endif %}
<ul>
{% regroup active_memberships by roles as memberships_per_role %}
{% for role in roles %}
<li class="{% if not role.clubs %}no-club{% endif %}">
{{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %}
{% if role.clubs %}
<div class="alert alert-success">
{% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }}
</div>
{% endif %}
<ul>
{% for permission in role.permissions.permissions.all %}
<li data-toggle="tooltip" title="{% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.type }} {{ permission.model }})</li>
{% empty %}
<em>{% trans "No associated permission" %}</em>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block extrajavascript %}
<script>
$(document).ready(function() {
let checkbox = $("#owned_only");
function update() {
if (checkbox.is(":checked"))
$(".no-club").addClass('d-none');
else
$(".no-club").removeClass('d-none');
}
checkbox.change(update);
update();
});
</script>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block content %} {% block content %}
{% if validlink %} {% if validlink %}
{% trans "Your email have successfully been validated." %} {% trans "Your email have successfully been validated." %}
{% if user.profile.registration_valid %} {% if user_object.profile.registration_valid %}
{% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %} {% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %}
{% else %} {% else %}
{% trans "You must pay now your membership in the Kfet to complete your registration." %} {% trans "You must pay now your membership in the Kfet to complete your registration." %}

View File

@ -31,13 +31,6 @@
</dd> </dd>
{% endif %} {% endif %}
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="small" href="{% url 'password_change' %}">
{% trans 'Change password' %}
</a>
</dd>
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.section }}</dd> <dd class="col-xl-6">{{ object.profile.section }}</dd>

View File

@ -16,13 +16,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% endif %} {% endif %}
{%url 'cas_login' as cas_url %}
{% if cas_url %}
<div class="alert alert-info">
{% trans "You can also register via the central authentification server " %}
<a href="{{ cas_url }}"> {% trans "using this link "%}</a>
</div>
{%endif%}
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary"> <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">

View File

@ -6,6 +6,11 @@
{% block content %} {% block content %}
<h2>{% trans "Sign up" %}</h2> <h2>{% trans "Sign up" %}</h2>
<div class="alert alert-warning">
{% blocktrans %}If you already signed up, your registration is taken into account. The BDE must validate your account before your can log in. You have to go to the Kfet and pay the registration fee. You must also validate your email address by following the link you received.{% endblocktrans %}
</div>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}

View File

@ -0,0 +1,2 @@
{# The data is already sent as HTML, so we return only the HTML data. Devs don't need a pretty mail... #}
{{ error }}

Some files were not shown because too many files have changed in this diff Show More