1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Merge branch 'wei' into 'master'

WEI

Closes #32 and #33

See merge request bde/nk20!78
This commit is contained in:
ynerant 2020-05-07 18:46:00 +02:00
commit 138bdbd444
114 changed files with 10448 additions and 2141 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

@ -45,8 +45,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

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,7 +362,20 @@ class Membership(models.Model):
reason="Adhésion " + self.club.name, reason="Adhésion " + self.club.name,
) )
transaction._force_save = True transaction._force_save = True
transaction.save(force_insert=True) print(hasattr(self, '_soge'))
if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS:
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
# to treasurers.
transaction.valid = False
from treasury.models import SogeCredit
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
soge_credit.refresh_from_db()
transaction.save(force_insert=True)
transaction.refresh_from_db()
soge_credit.transactions.add(transaction)
soge_credit.save()
else:
transaction.save(force_insert=True)
def __str__(self): def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )

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

@ -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):
""" """

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.get_or_create(club=instance)
NoteClub.objects.create(club=instance)
instance.note.save() instance.note.save()

View File

@ -30,7 +30,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):
""" """
@ -93,7 +93,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

@ -36,13 +36,15 @@ 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"),
rolepermissions__role__membership__user=user, membership=F("rolepermissions__role__membership"),
rolepermissions__role__membership__date_start__lte=datetime.date.today(), ).filter(
rolepermissions__role__membership__date_end__gte=datetime.date.today(), rolepermissions__role__membership__user=user,
type=t, rolepermissions__role__membership__date_start__lte=datetime.date.today(),
mask__rank__lte=get_current_session().get("permission_mask", 0), rolepermissions__role__membership__date_end__gte=datetime.date.today(),
type=t,
mask__rank__lte=get_current_session().get("permission_mask", 0),
).distinct() ).distinct()
@staticmethod @staticmethod
@ -55,6 +57,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 +67,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 +85,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

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 b9fdced3c2ce34168b8f0d6004a20a69ca16e0de 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

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

@ -312,7 +312,7 @@ function in_validate(id, validated) {
else else
invalidity_reason = null; invalidity_reason = null;
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>"); $("#validate_" + id).html("<i class='fa fa-spinner'></i>");
// 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 insuffisent rights, an error message will appear

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"
@ -76,39 +76,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 +129,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 %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> {% if club.membership_start %}
<dd class="col-xl-6">{{ club.membership_start }}</dd> <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> {% if club.membership_end %}
<dd class="col-xl-6">{{ club.membership_end }}</dd> <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_end }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> {% if club.membership_duration %}
<dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd> <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
<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,22 +46,30 @@
{% 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>
<div class="card-footer text-center"> {% if not club.weiclub %}
{% if can_add_members %} <div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a> {% if can_add_members %}
{% endif %} <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a>
{% if ".change_"|has_perm:club %} {% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> {% if ".change_"|has_perm:club %}
{% endif %} <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% url 'member:club_detail' club.pk as club_detail_url %} {% endif %}
{%if request.path_info != club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> {%if request.path_info != club_detail_url %}
{% endif %} </div> <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% 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,23 +1,27 @@
{% 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-header position-relative" id="clubListHeading"> <div class="card">
<a class="btn btn-link stretched-link font-weight-bold"> <div class="card-header position-relative" id="clubListHeading">
<i class="fa fa-users"></i> {% trans "Member of the Club" %} <a class="btn btn-link stretched-link font-weight-bold">
</a> <i class="fa fa-users"></i> {% trans "Member of the Club" %}
</div> </a>
</div>
{% render_table member_list %} {% render_table member_list %}
</div>
<hr>
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div> </div>
<hr>
{% endif %}
{% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<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>
function refreshHistory() { {% if object %}
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); function refreshHistory() {
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
} $("#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>
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> {% if object.pk == user.pk %}
<dd class="col-xl-6"> <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<a class="small" href="{% url 'password_change' %}"> <dd class="col-xl-6">
{% trans 'Change password' %} <a class="small" href="{% url 'password_change' %}">
</a> {% trans 'Change password' %}
</dd> </a>
</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

@ -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

@ -5,13 +5,18 @@
{% block title %}{% trans "Sign up" %}{% endblock %} {% block title %}{% trans "Sign up" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% trans "Sign up" %}</h2> <h2>{% trans "Sign up" %}</h2>
<form method="post">
{% csrf_token %} <div class="alert alert-warning">
{{ form|crispy }} {% 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 %}
{{ profile_form|crispy }} </div>
<button class="btn btn-success" type="submit">
{% trans "Sign up" %} <form method="post">
</button> {% csrf_token %}
</form> {{ form|crispy }}
{{ profile_form|crispy }}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
{% endblock %} {% endblock %}

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 }}

View File

@ -0,0 +1,7 @@
Une erreur est survenue dans la Note Kfet. Les détails sont ci-dessous.
Cordialement,
L'équipe de la Note Kfet.
{{ error }}

View File

@ -12,6 +12,9 @@
<a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary"> <a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Remittance" %}s {% trans "Remittance" %}s
</a> </a>
<a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary">
{% trans "Société générale credits" %}
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,6 +12,9 @@
<a href="#" class="btn btn-sm btn-outline-primary active"> <a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Remittance" %}s {% trans "Remittance" %}s
</a> </a>
<a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary">
{% trans "Société générale credits" %}
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load pretty_money %}
{% load perms %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Credit from the Société générale" %}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd>
{% if "note.view_note_balance"|has_perm:object.user.note %}
<dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.note.balance|pretty_money }}</dd>
{% endif %}
<dt class="col-xl-6 text-right">{% trans 'transactions'|capfirst %}</dt>
<dd class="col-xl-6">
{% for transaction in object.transactions.all %}
{{ transaction.membership.club }} ({{ transaction.amount|pretty_money }})<br>
{% endfor %}
</dd>
<dt class="col-xl-6 text-right">{% trans 'total amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.amount|pretty_money }}</dd>
</dl>
</div>
<div class="alert alert-warning">
{% trans 'Warning: Validating this credit implies that all membership transactions will be validated.' %}
{% trans 'If you delete this credit, there all membership transactions will be also validated, but no credit will be operated.' %}
{% trans "If this credit is validated, then the user won't be able to ask for a credit from the Société générale." %}
{% trans 'If you think there is an error, please contact the "respos info".' %}
</div>
<div class="card-footer text-center" id="buttons_footer">
{% if object.valid %}
<div class="alert alert-danger">
{% trans "This credit is already validated." %}
</div>
{% else %}
{% if object.user.note.balance < object.amount %}
<div class="alert alert-warning">
{% trans "Warning: if you don't validate this credit, the note of the user doesn't have enough money to pay its memberships." %}
{% trans "Please ask the user to credit its note before deleting this credit." %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="btn-group btn-block">
<button name="validate" class="btn btn-success">{% trans "Validate" %}</button>
{% if object.user.note.balance >= object.amount %}
<button name="delete" class="btn btn-danger">{% trans "Delete" %}</button>
{% endif %}
</div>
</form>
{% endif %}
<a href="{% url 'treasury:soge_credits' %}"><button class="btn btn-primary btn-block">{% trans "Return to credit list" %}</button></a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Invoice" %}s
</a>
<a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Remittance" %}s
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Société générale credits" %}
</a>
</div>
</div>
</div>
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
<div class="form-check">
<label for="invalid_only" class="form-check-label">
<input id="invalid_only" name="invalid_only" type="checkbox" class="checkboxinput form-check-input">
{% trans "Filter with unvalidated credits only" %}
</label>
</div>
<hr>
<div id="credits_table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no matched user that have asked for a Société générale credit." %}
</div>
{% endif %}
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#searchbar");
let invalid_only_obj = $("#invalid_only");
function reloadTable() {
let pattern = searchbar_obj.val();
if (pattern === old_pattern || pattern === "")
return;
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
searchbar_obj.keyup(reloadTable);
invalid_only_obj.change(reloadTable);
});
</script>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "member/noteowner_detail.html" %}
{% block profile_info %}
{% include "wei/weiclub_info.html" %}
{% endblock %}
{% block profile_content %}
{% include "wei/bus_tables.html" %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "member/noteowner_detail.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block profile_info %}
{% include "wei/weiclub_info.html" %}
{% endblock %}
{% block profile_content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,50 @@
{% load render_table from django_tables2 %}
{% load i18n %}
<div class="card">
<div class="card-header text-center">
<h4>{{ object.name }}</h4>
</div>
<div class="card-body">
{{ object.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}">{% trans "Add team" %}</a>
</div>
</div>
<hr>
{% if teams.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>
{% render_table teams %}
</div>
<hr>
{% endif %}
{% if memberships.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Members" %}
</a>
</div>
{% render_table memberships %}
</div>
<hr>
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
</a>
{% endif %}

View File

@ -0,0 +1,9 @@
{% extends "member/noteowner_detail.html" %}
{% block profile_info %}
{% include "wei/weiclub_info.html" %}
{% endblock %}
{% block profile_content %}
{% include "wei/busteam_tables.html" %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "member/noteowner_detail.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block profile_info %}
{% include "wei/weiclub_info.html" %}
{% endblock %}
{% block profile_content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% load render_table from django_tables2 %}
{% load i18n %}
<div class="card">
<div class="card-header text-center">
<h4>{{ bus.name }}</h4>
</div>
<div class="card-body">
{{ bus.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}">{% trans "Add team" %}</a>
</div>
</div>
<hr>
<div class="card">
<div class="card-header text-center" style="background-color: #{{ object.color|stringformat:"06X" }}; color: #{{ -16777215|add:object.color|stringformat:"06X"|slice:"1:" }};">
<h4>{{ object.name }}</h4>
</div>
<div class="card-body">
{{ object.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_team' pk=object.pk %}">{% trans "Edit" %}</a>
</div>
</div>
<hr>
{% if memberships.data or True %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>
{% render_table memberships %}
</div>
<hr>
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
</a>
{% endif %}

29
templates/wei/survey.html Normal file
View File

@ -0,0 +1,29 @@
{% extends "member/noteowner_detail.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block profile_info %}
{% include "wei/weiclub_info.html" %}
{% endblock %}
{% block profile_content %}
<div class="card">
<div class="card-header text-center">
<h4>{% trans "Survey WEI" %}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user }}</dd>
</dl>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="card-footer text-center">
<input class="btn btn-success" type="submit" value="{% trans "Next" %}"/>
</div>
</form>
</div>
</div>
{% endblock %}

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