1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-22 15:32:22 +00:00

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

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

View File

@ -11,3 +11,5 @@ DJANGO_SETTINGS_MODULE=note_kfet.settings
DOMAIN=localhost
CONTACT_EMAIL=tresorerie.bde@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
CONTACT_EMAIL=tresorerie.bde@localhost
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

View File

@ -4,7 +4,7 @@ from datetime import timedelta, datetime
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from member.models import Club
from note.models import NoteUser, Note
from note_kfet.inputs import DateTimePickerInput, Autocomplete

View File

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

View File

@ -3,6 +3,7 @@
from datetime import datetime, timezone
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Q
@ -45,8 +46,8 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
context['title'] = _("Activities")
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
context['upcoming'] = ActivityTable(data=upcoming_activities
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
return context
@ -138,8 +139,14 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
| Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern)))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\
.distinct()[:20]
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
note_qs = note_qs.distinct('note__pk')[:20]
else:
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
# In production mode, please use PostgreSQL.
note_qs = note_qs.distinct()[:20]
for note in note_qs:
note.type = "Adhérent"
note.activity = activity
@ -153,9 +160,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["title"] = _('Entry for activity "{}"').format(activity.name)
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
return context
return context

View File

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

View File

@ -9,7 +9,7 @@ from note.models import NoteSpecial
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask
from .models import Profile, Club, Membership
from .models import Profile, Club, Membership, Role
class CustomAuthenticationForm(AuthenticationForm):
@ -25,10 +25,16 @@ class ProfileForm(forms.ModelForm):
A form for the extras field provided by the :model:`member.Profile` model.
"""
def save(self, commit=True):
if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data):
self.instance.section = self.instance.section_generated
return super().save(commit)
class Meta:
model = Profile
fields = '__all__'
exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', )
exclude = ('user', 'email_confirmed', 'registration_valid', )
class ClubForm(forms.ModelForm):
@ -50,6 +56,8 @@ class ClubForm(forms.ModelForm):
class MembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,

View File

@ -23,18 +23,20 @@ class Profile(models.Model):
We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
phone_number = models.CharField(
verbose_name=_('phone number'),
max_length=50,
blank=True,
null=True,
)
section = models.CharField(
verbose_name=_('section'),
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
@ -42,12 +44,44 @@ class Profile(models.Model):
blank=True,
null=True,
)
department = models.CharField(
max_length=8,
verbose_name=_("department"),
choices=[
('A0', _("Informatics (A0)")),
('A1', _("Mathematics (A1)")),
('A2', _("Physics (A2)")),
("A'2", _("Applied physics (A'2)")),
('A''2', _("Chemistry (A''2)")),
('A3', _("Biology (A3)")),
('B1234', _("SAPHIRE (B1234)")),
('B1', _("Mechanics (B1)")),
('B2', _("Civil engineering (B2)")),
('B3', _("Mechanical engineering (B3)")),
('B4', _("EEA (B4)")),
('C', _("Design (C)")),
('D2', _("Economy-management (D2)")),
('D3', _("Social sciences (D3)")),
('E', _("English (E)")),
('EXT', _("External (EXT)")),
]
)
promotion = models.PositiveIntegerField(
null=True,
default=datetime.date.today().year,
verbose_name=_("promotion"),
help_text=_("Year of entry to the school (None if not ENS student)"),
)
address = models.CharField(
verbose_name=_('address'),
max_length=255,
blank=True,
null=True,
)
paid = models.BooleanField(
verbose_name=_("paid"),
help_text=_("Tells if the user receive a salary."),
@ -64,11 +98,29 @@ class Profile(models.Model):
default=False,
)
soge = models.BooleanField(
verbose_name=_("Société générale"),
help_text=_("Has the user ever be paid by the Société générale?"),
default=False,
)
@property
def ens_year(self):
"""
Number of years since the 1st august of the entry year, rounded up.
"""
if self.promotion is None:
return 0
today = datetime.date.today()
years = today.year - self.promotion
if today.month >= 8:
years += 1
return years
@property
def section_generated(self):
return str(self.ens_year) + self.department
@property
def soge(self):
if "treasury" in settings.INSTALLED_APPS:
from treasury.models import SogeCredit
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
return False
class Meta:
verbose_name = _('user profile')
@ -85,7 +137,7 @@ class Profile(models.Model):
'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"),
'token': email_validation_token.make_token(self.user),
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)).decode('UTF-8'),
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
})
self.user.email_user(subject, message)
@ -171,6 +223,7 @@ class Club(models.Model):
self.membership_start.month, self.membership_start.day)
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
def save(self, force_insert=False, force_update=False, using=None,
@ -220,6 +273,7 @@ class Membership(models.Model):
user = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="memberships",
verbose_name=_("user"),
)
@ -308,7 +362,20 @@ class Membership(models.Model):
reason="Adhésion " + self.club.name,
)
transaction._force_save = True
transaction.save(force_insert=True)
print(hasattr(self, '_soge'))
if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS:
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
# to treasurers.
transaction.valid = False
from treasury.models import SogeCredit
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
soge_credit.refresh_from_db()
transaction.save(force_insert=True)
transaction.refresh_from_db()
soge_credit.transactions.add(transaction)
soge_credit.save()
else:
transaction.save(force_insert=True)
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )

View File

@ -18,7 +18,7 @@ from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from note.forms import ImageForm
from note.models import Alias, NoteUser, NoteSpecial
from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable
from permission.backends import PermissionBackend
@ -128,7 +128,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs)
user = context['user_object']
history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at", "-id")\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
@ -165,7 +166,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
Q(first_name__iregex=pattern)
| Q(last_name__iregex=pattern)
| Q(profile__section__iregex=pattern)
| Q(profile__username__iregex="^" + pattern)
| Q(username__iregex="^" + pattern)
| Q(note__alias__name__iregex="^" + pattern)
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
)
@ -314,7 +315,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club.update_membership_dates()
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.order_by('-created_at', '-id')
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table
@ -365,6 +367,15 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form_class = ClubForm
template_name = "member/club_form.html"
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
# Don't update a WEI club through this view
if "wei" in settings.INSTALLED_APPS:
qs = qs.filter(weiclub=None)
return qs
def get_success_url(self):
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
@ -396,7 +407,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
if "club_pk" in self.kwargs:
# We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"])
.get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
@ -463,17 +474,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and club.name == "BDE"
# If Société générale pays, then we auto-fill some data
# If Société générale pays, then we store that information but the payment must be controlled by treasurers
# later. The membership transaction will be invalidated.
if soge:
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire")
bde = club
kfet = Club.objects.get(name="Kfet")
if user.profile.paid:
fee = bde.membership_fee_paid + kfet.membership_fee_paid
else:
fee = bde.membership_fee_unpaid + kfet.membership_fee_unpaid
credit_amount = fee
bank = "Société générale"
credit_type = None
form.instance._soge = True
if credit_type is None:
credit_amount = 0
@ -521,6 +526,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
# Now, all is fine, the membership can be created.
if club.name == "BDE":
# When we renew the BDE membership, we update the profile section.
# We could automate that and remove the section field from the Profile model,
# but with this way users can customize their section as they want.
user.profile.section = user.profile.section_generated
user.profile.save()
# Credit note before the membership is created.
if credit_amount > 0:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
@ -544,11 +556,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
valid=True,
)
# If Société générale pays, then we store the information: the bank can't pay twice to a same person.
if soge:
user.profile.soge = True
user.profile.save()
ret = super().form_valid(form)
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
# Kfet membership.
if soge:
kfet = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
@ -560,20 +572,23 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
date_end__gte=datetime.today(),
)
membership = Membership.objects.create(
membership = Membership(
club=kfet,
user=user,
fee=kfet_fee,
date_start=old_membership.get().date_end + timedelta(days=1)
if old_membership.exists() else form.instance.date_start,
)
membership._soge = True
membership.save()
membership.refresh_from_db()
if old_membership.exists():
membership.roles.set(old_membership.get().roles.all())
else:
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save()
return super().form_valid(form)
return ret
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})

View File

@ -3,6 +3,8 @@
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
@ -97,6 +99,35 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
model = Note
class ConsumerSerializer(serializers.ModelSerializer):
"""
REST API Nested Serializer for Consumers.
return Alias, and the note Associated to it in
"""
note = serializers.SerializerMethodField()
email_confirmed = serializers.SerializerMethodField()
class Meta:
model = Alias
fields = '__all__'
def get_note(self, obj):
"""
Display information about the associated note
"""
# If the user has no right to see the note, then we only display the note identifier
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
print(obj.pk)
return NotePolymorphicSerializer().to_representation(obj.note)
return dict(id=obj.id)
def get_email_confirmed(self, obj):
if isinstance(obj.note, NoteUser):
return obj.note.user.profile.email_confirmed
return True
class TemplateCategorySerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transaction templates.

View File

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

View File

@ -10,8 +10,8 @@ from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
TransactionTemplateSerializer, TransactionPolymorphicSerializer
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@ -90,6 +90,30 @@ class AliasViewSet(ReadProtectedModelViewSet):
return queryset
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects.all()
serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name']
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested aliases
"""
queryset = super().get_queryset()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
Q(name__regex="^" + alias)
| Q(normalized_name__regex="^" + Alias.normalize(alias))
| Q(normalized_name__regex="^" + alias.lower()))
return queryset
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
"""
REST API View set.

View File

@ -4,7 +4,7 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete
from note_kfet.inputs import Autocomplete, AmountInput
from .models import TransactionTemplate, NoteClub
@ -24,11 +24,6 @@ class TransactionTemplateForm(forms.ModelForm):
model = TransactionTemplate
fields = '__all__'
# Le champ de destination est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les aliases valides
# Pour force le type d'une note, il faut rajouter le paramètre :
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = {
'destination':
Autocomplete(
@ -41,4 +36,5 @@ class TransactionTemplateForm(forms.ModelForm):
'placeholder': 'Note ...',
},
),
'amount': AmountInput(),
}

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from .notes import Note, NoteClub, NoteSpecial
from ..templatetags.pretty_money import pretty_money
"""
Defines transactions
@ -198,6 +199,14 @@ class Transaction(PolymorphicModel):
self.source.save()
self.destination.save()
def delete(self, **kwargs):
"""
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
"""
self.valid = False
self.save(**kwargs)
super().delete(**kwargs)
@property
def total(self):
return self.amount * self.quantity
@ -206,6 +215,10 @@ class Transaction(PolymorphicModel):
def type(self):
return _('Transfer')
def __str__(self):
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
class RecurrentTransaction(Transaction):
"""
@ -214,8 +227,7 @@ class RecurrentTransaction(Transaction):
template = models.ForeignKey(
TransactionTemplate,
null=True,
on_delete=models.SET_NULL,
on_delete=models.PROTECT,
)
category = models.ForeignKey(
TemplateCategory,

View File

@ -10,14 +10,14 @@ def save_user_note(instance, raw, **_kwargs):
# When provisionning data, do not try to autocreate
return
if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active:
if instance.is_superuser or instance.profile.registration_valid:
# Create note only when the registration is validated
from note.models import NoteUser
NoteUser.objects.get_or_create(user=instance)
instance.note.save()
def save_club_note(instance, created, raw, **_kwargs):
def save_club_note(instance, raw, **_kwargs):
"""
Hook to create and save a note when a club is updated
"""
@ -25,7 +25,6 @@ def save_club_note(instance, created, raw, **_kwargs):
# When provisionning data, do not try to autocreate
return
if created:
from .models import NoteClub
NoteClub.objects.create(club=instance)
from .models import NoteClub
NoteClub.objects.get_or_create(club=instance)
instance.note.save()

View File

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

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
@ -30,7 +31,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
table_class = HistoryTable
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
def get_context_data(self, **kwargs):
"""
@ -80,6 +81,33 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if "logs" in settings.INSTALLED_APPS:
from logs.models import Changelog
update_logs = Changelog.objects.filter(
model=ContentType.objects.get_for_model(TransactionTemplate),
instance_pk=self.object.pk,
action="edit",
)
price_history = []
for log in update_logs.all():
old_dict = json.loads(log.previous)
new_dict = json.loads(log.data)
old_price = old_dict["amount"]
new_price = new_dict["amount"]
if old_price != new_price:
price_history.append(dict(price=old_price, time=log.timestamp))
price_history.append(dict(price=self.object.amount, time=None))
price_history.reverse()
context["price_history"] = price_history
return context
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
@ -93,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
table_class = HistoryTable
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
def get_context_data(self, **kwargs):
"""

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -120,7 +120,12 @@ class Permission(models.Model):
('delete', 'delete')
]
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
model = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='+',
verbose_name=_("model"),
)
# A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects)
@ -142,18 +147,34 @@ class Permission(models.Model):
# Examples:
# Q(is_superuser=True) := {"is_superuser": true}
# ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
query = models.TextField()
query = models.TextField(
verbose_name=_("query"),
)
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
type = models.CharField(
max_length=15,
choices=PERMISSION_TYPES,
verbose_name=_("type"),
)
mask = models.ForeignKey(
PermissionMask,
on_delete=models.PROTECT,
related_name="permissions",
verbose_name=_("mask"),
)
field = models.CharField(max_length=255, blank=True)
field = models.CharField(
max_length=255,
blank=True,
verbose_name=_("field"),
)
description = models.CharField(max_length=255, blank=True)
description = models.CharField(
max_length=255,
blank=True,
verbose_name=_("description"),
)
class Meta:
unique_together = ('model', 'query', 'type', 'field')
@ -277,24 +298,22 @@ class Permission(models.Model):
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
def __str__(self):
if self.field:
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
else:
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
return self.description
class RolePermissions(models.Model):
"""
Permissions associated with a Role
"""
role = models.ForeignKey(
role = models.OneToOneField(
Role,
on_delete=models.PROTECT,
related_name='+',
related_name='permissions',
verbose_name=_('role'),
)
permissions = models.ManyToManyField(
Permission,
verbose_name=_("permissions"),
)
def __str__(self):

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
@ -57,13 +58,19 @@ def pre_save_object(sender, instance, **kwargs):
if old_value == new_value:
continue
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
raise PermissionDenied
raise PermissionDenied(
_("You don't have the permission to change the field {field} on this instance of model"
" {app_label}.{model_name}.")
.format(field=field_name, app_label=app_label, model_name=model_name, )
)
else:
# We check if the user has right to add the object
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
if not has_perm:
raise PermissionDenied
raise PermissionDenied(
_("You don't have the permission to add this instance of model {app_label}.{model_name}.")
.format(app_label=app_label, model_name=model_name, ))
def pre_delete_object(instance, **kwargs):
@ -88,4 +95,6 @@ def pre_delete_object(instance, **kwargs):
# We check if the user has rights to delete the object
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
raise PermissionDenied
raise PermissionDenied(
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
.format(app_label=app_label, model_name=model_name))

10
apps/permission/urls.py Normal file
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
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from permission.backends import PermissionBackend
from django.forms import HiddenInput
from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView, TemplateView
from member.models import Role, Membership
from .backends import PermissionBackend
class ProtectQuerysetMixin:
"""
This is a View class decorator and not a proper View class.
Ensure that the user has the right to see or update objects.
Display 404 error if the user can't see an object, remove the fields the user can't
update on an update form (useful if the user can't change only specified fields).
"""
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
def get_form(self, form_class=None):
form = super().get_form(form_class)
if not isinstance(self, UpdateView):
return form
# If we are in an UpdateView, we display only the fields the user has right to see.
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
# a custom request.
# We could also delete the field, but some views might be affected.
for key in form.base_fields:
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object):
form.fields[key].widget = HiddenInput()
return form
class RightsView(TemplateView):
template_name = "permission/all_rights.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("All rights")
roles = Role.objects.all()
context["roles"] = roles
if self.request.user.is_authenticated:
active_memberships = Membership.objects.filter(user=self.request.user,
date_start__lte=date.today(),
date_end__gte=date.today()).all()
else:
active_memberships = Membership.objects.none()
for role in roles:
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
return context

View File

@ -27,6 +27,15 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', )
class WEISignupForm(forms.Form):
wei_registration = forms.BooleanField(
label=_("Register to the WEI"),
required=False,
help_text=_("Check this case if you want to register to the WEI. If you hesitate, you will be able to register"
" later, after validating your account in the Kfet."),
)
class ValidationForm(forms.Form):
"""
Validate the inscription of the new users and pay memberships.

View File

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

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

View File

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

View File

@ -4,7 +4,7 @@
from rest_framework import serializers
from note.api.serializers import SpecialTransactionSerializer
from ..models import Invoice, Product, RemittanceType, Remittance
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
class ProductSerializer(serializers.ModelSerializer):
@ -60,3 +60,14 @@ class RemittanceSerializer(serializers.ModelSerializer):
def get_transactions(self, obj):
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions)
class SogeCreditSerializer(serializers.ModelSerializer):
"""
REST API Serializer for SogeCredit types.
The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API.
"""
class Meta:
model = SogeCredit
fields = '__all__'

View File

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

View File

@ -5,8 +5,9 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer
from ..models import Invoice, Product, RemittanceType, Remittance
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer,\
SogeCreditSerializer
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
class InvoiceViewSet(ReadProtectedModelViewSet):
@ -39,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
then render it on /api/treasury/remittance_type/
"""
queryset = RemittanceType.objects.all()
queryset = RemittanceType.objects
serializer_class = RemittanceTypeSerializer
@ -49,5 +50,15 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/remittance/
"""
queryset = Remittance.objects.all()
queryset = Remittance.objects
serializer_class = RemittanceSerializer
class SogeCreditViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/soge_credit/
"""
queryset = SogeCredit.objects
serializer_class = SogeCreditSerializer

View File

@ -1,11 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, SpecialTransaction
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
class Invoice(models.Model):
@ -207,3 +209,101 @@ class SpecialTransactionProxy(models.Model):
class Meta:
verbose_name = _("special transaction proxy")
verbose_name_plural = _("special transaction proxies")
class SogeCredit(models.Model):
"""
Manage the credits from the Société générale.
"""
user = models.OneToOneField(
User,
on_delete=models.PROTECT,
verbose_name=_("user"),
)
transactions = models.ManyToManyField(
MembershipTransaction,
related_name="+",
verbose_name=_("membership transactions"),
)
credit_transaction = models.OneToOneField(
SpecialTransaction,
on_delete=models.SET_NULL,
verbose_name=_("credit transaction"),
null=True,
)
@property
def valid(self):
return self.credit_transaction is not None
@property
def amount(self):
return sum(transaction.total for transaction in self.transactions.all())
def invalidate(self):
"""
Invalidating a Société générale delete the transaction of the bank if it was already created.
Treasurers must know what they do, With Great Power Comes Great Responsibility...
"""
if self.valid:
self.credit_transaction.valid = False
self.credit_transaction._force_save = True
self.credit_transaction.save()
self.credit_transaction._force_delete = True
self.credit_transaction.delete()
self.credit_transaction = None
for transaction in self.transactions.all():
transaction.valid = False
transaction._force_save = True
transaction.save()
def validate(self, force=False):
if self.valid and not force:
# The credit is already done
return
# First invalidate all transaction and delete the credit if already did (and force mode)
self.invalidate()
self.credit_transaction = SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Virement bancaire"),
destination=self.user.note,
quantity=1,
amount=self.amount,
reason="Crédit société générale",
last_name=self.user.last_name,
first_name=self.user.first_name,
bank="Société générale",
)
self.save()
for transaction in self.transactions.all():
transaction.valid = True
transaction._force_save = True
transaction.created_at = datetime.now()
transaction.save()
def delete(self, **kwargs):
"""
Deleting a SogeCredit is equivalent to say that the Société générale didn't pay.
Treasurers must know what they do, this is difficult to undo this operation.
With Great Power Comes Great Responsibility...
"""
total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid)
if self.user.note.balance < total_fee:
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit."))
self.invalidate()
for transaction in self.transactions.all():
transaction._force_save = True
transaction.valid = True
transaction.created_at = datetime.now()
transaction.save()
super().delete(**kwargs)
class Meta:
verbose_name = _("Credit from the Société générale")
verbose_name_plural = _("Credits from the Société générale")

View File

@ -7,7 +7,7 @@ from django_tables2 import A
from note.models import SpecialTransaction
from note.templatetags.pretty_money import pretty_money
from .models import Invoice, Remittance
from .models import Invoice, Remittance, SogeCredit
class InvoiceTable(tables.Table):
@ -101,3 +101,28 @@ class SpecialTransactionTable(tables.Table):
model = SpecialTransaction
template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
class SogeCreditTable(tables.Table):
user = tables.LinkColumn(
'treasury:manage_soge_credit',
args=[A('pk')],
)
amount = tables.Column(
verbose_name=_("Amount"),
)
valid = tables.Column(
verbose_name=_("Valid"),
)
def render_amount(self, value):
return pretty_money(value)
def render_valid(self, value):
return _("Yes") if value else _("No")
class Meta:
model = SogeCredit
fields = ('user', 'amount', 'valid', )

View File

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

View File

@ -10,21 +10,23 @@ from crispy_forms.helper import FormHelper
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import Form
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.views.generic import CreateView, UpdateView
from django.views.generic import CreateView, UpdateView, DetailView
from django.views.generic.base import View, TemplateView
from django.views.generic.edit import BaseFormView
from django_tables2 import SingleTableView
from note.models import SpecialTransaction, NoteSpecial
from note.models import SpecialTransaction, NoteSpecial, Alias
from note_kfet.settings.base import BASE_DIR
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
@ -180,7 +182,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
# Display the generated pdf as a HTTP Response
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk)
response['Content-Disposition'] = "inline;filename=Facture%20n°{:d}.pdf".format(pk)
except IOError as e:
raise e
finally:
@ -203,9 +205,9 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["table"] = RemittanceTable(data=Remittance.objects
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
.all())
context["table"] = RemittanceTable(
data=Remittance.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
return context
@ -307,3 +309,61 @@ class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
transaction.save()
return redirect('treasury:remittance_list')
class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
"""
List all Société Générale credits
"""
model = SogeCredit
table_class = SogeCreditTable
def get_queryset(self, **kwargs):
"""
Filter the table with the given parameter.
:param kwargs:
:return:
"""
qs = super().get_queryset()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if not pattern:
return qs.none()
qs = qs.filter(
Q(user__first_name__iregex=pattern)
| Q(user__last_name__iregex=pattern)
| Q(user__note__alias__name__iregex="^" + pattern)
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
)
else:
qs = qs.none()
if "valid" in self.request.GET:
q = Q(credit_transaction=None)
if not self.request.GET["valid"]:
q = ~q
qs = qs.filter(q)
return qs[:20]
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
"""
Manage credits from the Société générale.
"""
model = SogeCredit
form_class = Form
def form_valid(self, form):
if "validate" in form.data:
self.get_object().validate(True)
elif "delete" in form.data:
self.get_object().delete()
return super().form_valid(form)
def get_success_url(self):
if "validate" in self.request.POST:
return reverse_lazy('treasury:manage_soge_credit', args=(self.get_object().pk,))
return reverse_lazy('treasury:soge_credits')

4
apps/wei/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'wei.apps.WeiConfig'

13
apps/wei/admin.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
admin.site.register(WEIClub)
admin.site.register(WEIRegistration)
admin.site.register(WEIMembership)
admin.site.register(WEIRole)
admin.site.register(Bus)
admin.site.register(BusTeam)

0
apps/wei/api/__init__.py Normal file
View File

View File

@ -0,0 +1,72 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
class WEIClubSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Clubs.
The djangorestframework plugin will analyse the model `WEIClub` and parse all fields in the API.
"""
class Meta:
model = WEIClub
fields = '__all__'
class BusSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Bus.
The djangorestframework plugin will analyse the model `Bus` and parse all fields in the API.
"""
class Meta:
model = Bus
fields = '__all__'
class BusTeamSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Bus teams.
The djangorestframework plugin will analyse the model `BusTeam` and parse all fields in the API.
"""
class Meta:
model = BusTeam
fields = '__all__'
class WEIRoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI roles.
The djangorestframework plugin will analyse the model `WEIRole` and parse all fields in the API.
"""
class Meta:
model = WEIRole
fields = '__all__'
class WEIRegistrationSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI registrations.
The djangorestframework plugin will analyse the model `WEIRegistration` and parse all fields in the API.
"""
class Meta:
model = WEIRegistration
fields = '__all__'
class WEIMembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI memberships.
The djangorestframework plugin will analyse the model `WEIMembership` and parse all fields in the API.
"""
class Meta:
model = WEIMembership
fields = '__all__'

17
apps/wei/api/urls.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import WEIClubViewSet, BusViewSet, BusTeamViewSet, WEIRoleViewSet, WEIRegistrationViewSet, \
WEIMembershipViewSet
def register_wei_urls(router, path):
"""
Configure router for Member REST API.
"""
router.register(path + '/club', WEIClubViewSet)
router.register(path + '/bus', BusViewSet)
router.register(path + '/team', BusTeamViewSet)
router.register(path + '/role', WEIRoleViewSet)
router.register(path + '/registration', WEIRegistrationViewSet)
router.register(path + '/membership', WEIMembershipViewSet)

86
apps/wei/api/views.py Normal file
View File

@ -0,0 +1,86 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
WEIRegistrationSerializer, WEIMembershipSerializer
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
class WEIClubViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/club/
"""
queryset = WEIClub.objects.all()
serializer_class = WEIClubSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'year', ]
class BusViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/bus/
"""
queryset = Bus.objects
serializer_class = BusSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'wei', ]
class BusTeamViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/team/
"""
queryset = BusTeam.objects
serializer_class = BusTeamSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'bus', 'bus__wei', ]
class WEIRoleViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/role/
"""
queryset = WEIRole.objects
serializer_class = WEIRoleSerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
class WEIRegistrationViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
then render it on /api/wei/registration/
"""
queryset = WEIRegistration.objects
serializer_class = WEIRegistrationSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', ]
filterset_fields = ['user', 'wei', ]
class WEIMembershipViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/membership/
"""
queryset = WEIMembership.objects
serializer_class = WEIMembershipSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', '$bus__name', '$team__name', ]
filterset_fields = ['user', 'club', 'bus', 'team', ]

10
apps/wei/apps.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,124 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
class WEIForm(forms.ModelForm):
class Meta:
model = WEIClub
exclude = ('parent_club', 'require_memberships', 'membership_duration', )
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
"membership_start": DatePickerInput(),
"membership_end": DatePickerInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
}
class WEIRegistrationForm(forms.ModelForm):
class Meta:
model = WEIRegistration
exclude = ('wei', )
widgets = {
"user": Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
),
"birth_date": DatePickerInput(),
}
class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects,
label=_("bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
+ " in particular if you are a free eletron."),
)
team = forms.ModelMultipleChoiceField(
queryset=BusTeam.objects,
label=_("Team"),
required=False,
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
)
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."),
)
class WEIMembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects, label=_("WEI Roles"))
def clean(self):
cleaned_data = super().clean()
if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]:
self.add_error('bus', _("This team doesn't belong to the given bus."))
return cleaned_data
class Meta:
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
}
),
}
class BusForm(forms.ModelForm):
class Meta:
model = Bus
fields = '__all__'
widgets = {
"wei": Autocomplete(
WEIClub,
attrs={
'api_url': '/api/wei/club/',
'placeholder': 'WEI ...',
},
),
}
class BusTeamForm(forms.ModelForm):
class Meta:
model = BusTeam
fields = '__all__'
widgets = {
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
},
),
"color": ColorWidget(),
}

View File

@ -0,0 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from .wei2020 import WEISurvey2020
__all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]
CurrentSurvey = WEISurvey2020

View File

@ -0,0 +1,189 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional
from django.db.models import QuerySet
from django.forms import Form
from ...models import WEIClub, WEIRegistration, Bus
class WEISurveyInformation:
"""
Abstract data of the survey.
"""
valid = False
selected_bus_pk = None
selected_bus_name = None
def __init__(self, registration):
self.__dict__.update(registration.information)
def get_selected_bus(self) -> Optional[Bus]:
"""
If the algorithm ran, return the prefered bus according to the survey.
In the other case, return None.
"""
if not self.valid:
return None
return Bus.objects.get(pk=self.selected_bus_pk)
def save(self, registration) -> None:
"""
Store the data of the survey into the database, with the information of the registration.
"""
registration.information = self.__dict__
class WEIBusInformation:
"""
Abstract data of the bus.
"""
def __init__(self, bus: Bus):
self.__dict__.update(bus.information)
self.bus = bus
class WEISurveyAlgorithm:
"""
Abstract algorithm that attributes a bus to each new member.
"""
@classmethod
def get_survey_class(cls):
"""
The class of the survey associated with this algorithm.
"""
raise NotImplementedError
@classmethod
def get_bus_information_class(cls):
"""
The class of the information associated to a bus extending WEIBusInformation.
Default: WEIBusInformation (contains nothing)
"""
return WEIBusInformation
@classmethod
def get_registrations(cls) -> QuerySet:
"""
Queryset of all first year registrations
"""
return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True)
@classmethod
def get_buses(cls) -> QuerySet:
"""
Queryset of all buses of the associated wei.
"""
return Bus.objects.filter(wei__year=cls.get_survey_class().get_year())
@classmethod
def get_bus_information(cls, bus):
"""
Return the WEIBusInformation object containing the data stored in a given bus.
"""
return cls.get_bus_information_class()(bus)
def run_algorithm(self) -> None:
"""
Once this method implemented, run the algorithm that attributes a bus to each first year member.
This method can be run in command line through ``python manage.py wei_algorithm``
See ``wei.management.commmands.wei_algorithm``
This method must call Survey.select_bus for each survey.
"""
raise NotImplementedError
class WEISurvey:
"""
Survey associated to a first year WEI registration.
The data is stored into WEISurveyInformation, this class acts as a manager.
This is an abstract class: this has to be extended each year to implement custom methods.
"""
def __init__(self, registration: WEIRegistration):
self.registration = registration
self.information = self.get_survey_information_class()(registration)
@classmethod
def get_year(cls) -> int:
"""
Get year of the wei concerned by the type of the survey.
"""
raise NotImplementedError
@classmethod
def get_wei(cls) -> WEIClub:
"""
The WEI associated to this kind of survey.
"""
return WEIClub.objects.get(year=cls.get_year())
@classmethod
def get_survey_information_class(cls):
"""
The class of the data (extending WEISurveyInformation).
"""
raise NotImplementedError
def get_form_class(self) -> Form:
"""
The form class of the survey.
This is proper to the status of the survey: the form class can evolve according to the progress of the survey.
"""
raise NotImplementedError
def update_form(self, form) -> None:
"""
Once the form is instanciated, the information can be updated with the information of the registration
and the information of the survey.
This method is called once the form is created.
"""
pass
def form_valid(self, form) -> None:
"""
Called when the information of the form are validated.
This method should update the information of the survey.
"""
raise NotImplementedError
def is_complete(self) -> bool:
"""
Return True if the survey is complete.
If the survey is complete, then the button "Next" will display some text for the end of the survey.
If not, the survey is reloaded and continues.
"""
raise NotImplementedError
def save(self) -> None:
"""
Store the information of the survey into the database.
"""
self.information.save(self.registration)
# The information is forced-saved.
# We don't want that anyone can update manually the information, so since most users don't have the
# right to save the information of a registration, we force save.
self.registration._force_save = True
self.registration.save()
@classmethod
def get_algorithm_class(cls):
"""
Algorithm class associated to the survey.
The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member.
The association is not permanent: that's only a suggestion.
"""
raise NotImplementedError
def select_bus(self, bus) -> None:
"""
Set the suggestion into the data of the membership.
:param bus: The bus suggested.
"""
self.information.selected_bus_pk = bus.pk
self.information.selected_bus_name = bus.name
self.information.valid = True

View File

@ -0,0 +1,89 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from ...models import Bus
class WEISurveyForm2020(forms.Form):
"""
Survey form for the year 2020.
For now, that's only a Bus selector.
TODO: Do a better survey (later)
"""
bus = forms.ModelChoiceField(
Bus.objects,
)
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
self.fields["bus"].queryset = Bus.objects.filter(wei=registration.wei)
class WEISurveyInformation2020(WEISurveyInformation):
"""
We store the id of the selected bus. We store only the name, but is not used in the selection:
that's only for humans that try to read data.
"""
chosen_bus_pk = None
chosen_bus_name = None
class WEISurvey2020(WEISurvey):
"""
Survey for the year 2020.
"""
@classmethod
def get_year(cls):
return 2020
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2020
def get_form_class(self):
return WEISurveyForm2020
def update_form(self, form):
"""
Filter the bus selector with the buses of the WEI.
"""
form.set_registration(self.registration)
def form_valid(self, form):
bus = form.cleaned_data["bus"]
self.information.chosen_bus_pk = bus.pk
self.information.chosen_bus_name = bus.name
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2020
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
return self.information.chosen_bus_pk is not None
class WEISurveyAlgorithm2020(WEISurveyAlgorithm):
"""
The algorithm class for the year 2020.
For now, the algorithm is quite simple: the selected bus is the chosen bus.
TODO: Improve this algorithm.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2020
def run_algorithm(self):
for registration in self.get_registrations():
survey = self.get_survey_class()(registration)
survey.select_bus(Bus.objects.get(pk=survey.information.chosen_bus_pk))
survey.save()

View File

329
apps/wei/models.py Normal file
View File

@ -0,0 +1,329 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from datetime import date
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy as _
from member.models import Role, Club, Membership
from note.models import MembershipTransaction
class WEIClub(Club):
"""
The WEI is a club. Register to the WEI is equivalent than be member of the club.
"""
year = models.PositiveIntegerField(
unique=True,
default=date.today().year,
verbose_name=_("year"),
)
date_start = models.DateField(
verbose_name=_("date start"),
)
date_end = models.DateField(
verbose_name=_("date end"),
)
@property
def is_current_wei(self):
"""
We consider that this is the current WEI iff there is no future WEI planned.
"""
return not WEIClub.objects.filter(date_start__gt=self.date_start).exists()
def update_membership_dates(self):
"""
We can't join the WEI next years.
"""
return
class Meta:
verbose_name = _("WEI")
verbose_name_plural = _("WEI")
class Bus(models.Model):
"""
The best bus for the best WEI
"""
wei = models.ForeignKey(
WEIClub,
on_delete=models.PROTECT,
related_name="buses",
verbose_name=_("WEI"),
)
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
description = models.TextField(
blank=True,
default="",
verbose_name=_("description"),
)
information_json = models.TextField(
default="{}",
verbose_name=_("survey information"),
help_text=_("Information about the survey for new members, encoded in JSON"),
)
@property
def information(self):
"""
The information about the survey for new members are stored in a dictionary that can evolve following the years.
The dictionary is stored as a JSON string.
"""
return json.loads(self.information_json)
@information.setter
def information(self, information):
"""
Store information as a JSON string
"""
self.information_json = json.dumps(information)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Bus")
verbose_name_plural = _("Buses")
unique_together = ('wei', 'name',)
class BusTeam(models.Model):
"""
A bus has multiple teams
"""
bus = models.ForeignKey(
Bus,
on_delete=models.CASCADE,
related_name="teams",
verbose_name=_("bus"),
)
name = models.CharField(
max_length=255,
)
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
verbose_name=_("color"),
help_text=_("The color of the T-Shirt, stored with its number equivalent"),
)
description = models.TextField(
blank=True,
default="",
verbose_name=_("description"),
)
def __str__(self):
return self.name + " (" + str(self.bus) + ")"
class Meta:
unique_together = ('bus', 'name',)
verbose_name = _("Bus team")
verbose_name_plural = _("Bus teams")
class WEIRole(Role):
"""
A Role for the WEI can be bus chief, team chief, free electron, ...
"""
class Meta:
verbose_name = _("WEI Role")
verbose_name_plural = _("WEI Roles")
class WEIRegistration(models.Model):
"""
Store personal data that can be useful for the WEI.
"""
user = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="wei",
verbose_name=_("user"),
)
wei = models.ForeignKey(
WEIClub,
on_delete=models.PROTECT,
related_name="users",
verbose_name=_("WEI"),
)
soge_credit = models.BooleanField(
default=False,
verbose_name=_("Credit from Société générale"),
)
caution_check = models.BooleanField(
default=False,
verbose_name=_("Caution check given")
)
birth_date = models.DateField(
verbose_name=_("birth date"),
)
gender = models.CharField(
max_length=16,
choices=(
('male', _("Male")),
('female', _("Female")),
('nonbinary', _("Non binary")),
),
verbose_name=_("gender"),
)
health_issues = models.TextField(
blank=True,
default="",
verbose_name=_("health issues"),
)
emergency_contact_name = models.CharField(
max_length=255,
verbose_name=_("emergency contact name"),
)
emergency_contact_phone = models.CharField(
max_length=32,
verbose_name=_("emergency contact phone"),
)
ml_events_registration = models.BooleanField(
default=False,
verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"),
)
ml_sport_registration = models.BooleanField(
default=False,
verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"),
)
ml_art_registration = models.BooleanField(
default=False,
verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"),
)
first_year = models.BooleanField(
default=False,
verbose_name=_("first year"),
help_text=_("Tells if the user is new in the school.")
)
information_json = models.TextField(
default="{}",
verbose_name=_("registration information"),
help_text=_("Information about the registration (buses for old members, survey fot the new members), "
"encoded in JSON"),
)
@property
def information(self):
"""
The information about the registration (the survey for the new members, the bus for the older members, ...)
are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string.
"""
return json.loads(self.information_json)
@information.setter
def information(self, information):
"""
Store information as a JSON string
"""
self.information_json = json.dumps(information)
@property
def is_validated(self):
try:
return self.membership is not None
except AttributeError:
return False
def __str__(self):
return str(self.user)
class Meta:
unique_together = ('user', 'wei',)
verbose_name = _("WEI User")
verbose_name_plural = _("WEI Users")
class WEIMembership(Membership):
bus = models.ForeignKey(
Bus,
on_delete=models.PROTECT,
related_name="memberships",
null=True,
default=None,
verbose_name=_("bus"),
)
team = models.ForeignKey(
BusTeam,
on_delete=models.PROTECT,
related_name="memberships",
null=True,
blank=True,
default=None,
verbose_name=_("team"),
)
registration = models.OneToOneField(
WEIRegistration,
on_delete=models.PROTECT,
null=True,
blank=True,
default=None,
related_name="membership",
verbose_name=_("WEI registration"),
)
class Meta:
verbose_name = _("WEI membership")
verbose_name_plural = _("WEI memberships")
def make_transaction(self):
"""
Create Membership transaction associated to this membership.
"""
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
return
if self.fee:
transaction = MembershipTransaction(
membership=self,
source=self.user.note,
destination=self.club.note,
quantity=1,
amount=self.fee,
reason="Adhésion WEI " + self.club.name,
valid=not self.registration.soge_credit # Soge transactions are by default invalidated
)
transaction._force_save = True
transaction.save(force_insert=True)
if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS:
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
# to treasurers.
transaction.refresh_from_db()
from treasury.models import SogeCredit
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
soge_credit.refresh_from_db()
transaction.save()
soge_credit.transactions.add(transaction)
soge_credit.save()

201
apps/wei/tables.py Normal file
View File

@ -0,0 +1,201 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django_tables2 import A
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
class WEITable(tables.Table):
"""
List all WEI.
"""
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = WEIClub
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'year', 'date_start', 'date_end',)
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: reverse_lazy('wei:wei_detail', args=(record.pk,))
}
class WEIRegistrationTable(tables.Table):
"""
List all WEI registrations.
"""
user = tables.LinkColumn(
'member:user_detail',
args=[A('user.pk')],
)
edit = tables.LinkColumn(
'wei:wei_update_registration',
args=[A('pk')],
verbose_name=_("Edit"),
text=_("Edit"),
attrs={
'a': {
'class': 'btn btn-warning'
}
}
)
validate = tables.LinkColumn(
'wei:validate_registration',
args=[A('pk')],
verbose_name=_("Validate"),
text=_("Validate"),
attrs={
'a': {
'class': 'btn btn-success'
}
}
)
delete = tables.LinkColumn(
'wei:wei_delete_registration',
args=[A('pk')],
verbose_name=_("delete"),
text=_("Delete"),
attrs={
'a': {
'class': 'btn btn-danger'
}
},
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user.first_name', 'user.last_name', 'first_year',)
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk
}
class WEIMembershipTable(tables.Table):
user = tables.LinkColumn(
'wei:wei_update_registration',
args=[A('registration.pk')],
)
year = tables.Column(
accessor=A("pk"),
verbose_name=_("Year"),
)
bus = tables.LinkColumn(
'wei:manage_bus',
args=[A('bus.pk')],
)
team = tables.LinkColumn(
'wei:manage_bus_team',
args=[A('bus.pk')],
)
def render_year(self, record):
return str(record.user.profile.ens_year) + "A"
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = WEIMembership
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user.last_name', 'user.first_name', 'registration.gender', 'user.profile.department',
'year', 'bus', 'team', )
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),
}
class BusTable(tables.Table):
name = tables.LinkColumn(
'wei:manage_bus',
args=[A('pk')],
)
teams = tables.Column(
accessor=A("teams"),
verbose_name=_("Teams"),
attrs={
"td": {
"class": "text-truncate",
}
}
)
count = tables.Column(
verbose_name=_("Members count"),
)
def render_teams(self, value):
return ", ".join(team.name for team in value.all())
def render_count(self, value):
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Bus
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'teams', )
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),
}
class BusTeamTable(tables.Table):
name = tables.LinkColumn(
'wei:manage_bus_team',
args=[A('pk')],
)
color = tables.Column(
attrs={
"td": {
"style": lambda record: "background-color: #{:06X}; color: #{:06X};"
.format(record.color, 0xFFFFFF - record.color, )
}
}
)
def render_count(self, value):
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
count = tables.Column(
verbose_name=_("Members count"),
)
def render_color(self, value):
return "#{:06X}".format(value)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = BusTeam
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'color',)
row_attrs = {
'class': 'table-row',
'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, ))
}

43
apps/wei/urls.py Normal file
View File

@ -0,0 +1,43 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView,\
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\
WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIDeleteRegistrationView,\
WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
app_name = 'wei'
urlpatterns = [
path('detail/', CurrentWEIDetailView.as_view(), name="current_wei_detail"),
path('list/', WEIListView.as_view(), name="wei_list"),
path('create/', WEICreateView.as_view(), name="wei_create"),
path('detail/<int:pk>/', WEIDetailView.as_view(), name="wei_detail"),
path('update/<int:pk>/', WEIUpdateView.as_view(), name="wei_update"),
path('detail/<int:pk>/registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"),
path('detail/<int:pk>/memberships/', WEIMembershipsView.as_view(), name="wei_memberships"),
path('detail/<int:wei_pk>/memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"),
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/', MemberListRenderView.as_view(),
name="wei_memberships_bus_pdf"),
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
name="wei_memberships_team_pdf"),
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
path('add-bus-team/<int:pk>/', BusTeamCreateView.as_view(), name="add_team"),
path('manage-bus-team/<int:pk>/', BusTeamManageView.as_view(), name="manage_bus_team"),
path('update-bus-team/<int:pk>/', BusTeamUpdateView.as_view(), name="update_bus_team"),
path('register/<int:wei_pk>/1A/', WEIRegister1AView.as_view(), name="wei_register_1A"),
path('register/<int:wei_pk>/2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"),
path('register/<int:wei_pk>/1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"),
path('register/<int:wei_pk>/2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"),
path('edit-registration/<int:pk>/', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"),
path('delete-registration/<int:pk>/', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"),
path('validate/<int:pk>/', WEIValidateRegistrationView.as_view(), name="validate_registration"),
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
]

946
apps/wei/views.py Normal file
View File

@ -0,0 +1,946 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import shutil
import subprocess
from datetime import datetime, date
from tempfile import mkdtemp
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db.models import Q, Count
from django.db.models.functions.text import Lower
from django.forms import HiddenInput
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.views import View
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
from django.utils.translation import gettext_lazy as _
from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView
from member.models import Membership, Club
from note.models import Transaction, NoteClub, Alias
from note.tables import HistoryTable
from note_kfet.settings import BASE_DIR
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembershipForm, CurrentSurvey
from .tables import WEITable, WEIRegistrationTable, BusTable, BusTeamTable, WEIMembershipTable
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
wei = WEIClub.objects.filter(membership_start__lte=date.today()).order_by('date_start')
if wei.exists():
wei = wei.last()
return reverse_lazy('wei:wei_detail', args=(wei.pk,))
else:
return reverse_lazy('wei:wei_list')
class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing WEI
"""
model = WEIClub
table_class = WEITable
ordering = '-year'
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create WEI
"""
model = WEIClub
form_class = WEIForm
def form_valid(self, form):
form.instance.requires_membership = True
form.instance.parent_club = Club.objects.get(name="Kfet")
ret = super().form_valid(form)
NoteClub.objects.create(club=form.instance)
return ret
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View WEI information
"""
model = WEIClub
context_object_name = "club"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = context["club"]
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \
.order_by('-created_at', '-id')
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table
club_member = WEIMembership.objects.filter(
club=club,
date_end__gte=datetime.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
context['member_list'] = membership_table
pre_registrations = WEIRegistration.objects.filter(
PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter(
membership=None,
wei=club
)
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
context['pre_registrations'] = pre_registrations_table
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
if my_registration.exists():
my_registration = my_registration.get()
else:
my_registration = None
context["my_registration"] = my_registration
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships"))
bus_table = BusTable(data=buses, prefix="bus-")
context['buses'] = bus_table
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
if random_user is None:
# This case occurs when all users are registered to the WEI.
# Don't worry, Pikachu never went to the WEI.
# This bug can arrive only in dev mode.
context["can_add_first_year_member"] = True
context["can_add_any_member"] = True
else:
# Check if the user has the right to create a registration of a random first year member.
empty_fy_registration = WEIRegistration(
user=random_user,
first_year=True,
birth_date="1970-01-01",
gender="No",
emergency_contact_name="No",
emergency_contact_phone="No",
)
context["can_add_first_year_member"] = PermissionBackend \
.check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration)
# Check if the user has the right to create a registration of a random old member.
empty_old_registration = WEIRegistration(
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
first_year=False,
birth_date="1970-01-01",
gender="No",
emergency_contact_name="No",
emergency_contact_phone="No",
)
context["can_add_any_member"] = PermissionBackend \
.check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration)
empty_bus = Bus(
wei=club,
name="",
)
context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus)
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
return context
class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all WEI memberships
"""
model = WEIMembership
table_class = WEIMembershipTable
def dispatch(self, request, *args, **kwargs):
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(club=self.club)
pattern = self.request.GET.get("search", "")
if not pattern:
return qs.none()
qs = qs.filter(
Q(user__first_name__iregex=pattern)
| Q(user__last_name__iregex=pattern)
| Q(user__note__alias__name__iregex="^" + pattern)
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
| Q(bus__name__iregex=pattern)
| Q(team__name__iregex=pattern)
)
return qs[:20]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.club
context["title"] = _("Find WEI Membership")
return context
class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all non-validated WEI registrations.
"""
model = WEIRegistration
table_class = WEIRegistrationTable
def dispatch(self, request, *args, **kwargs):
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
pattern = self.request.GET.get("search", "")
if not pattern:
return qs.none()
qs = qs.filter(
Q(user__first_name__iregex=pattern)
| Q(user__last_name__iregex=pattern)
| Q(user__note__alias__name__iregex="^" + pattern)
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
)
return qs[:20]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.club
context["title"] = _("Find WEI Registration")
return context
class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the information of the WEI.
"""
model = WEIClub
context_object_name = "club"
form_class = WEIForm
def dispatch(self, request, *args, **kwargs):
wei = self.get_object()
today = date.today()
# We can't update a past WEI
# But we can update it while it is not officially opened
if today > wei.membership_end:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create Bus
"""
model = Bus
form_class = BusForm
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
today = date.today()
# We can't add a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["wei"].initial = WEIClub.objects.get(pk=self.kwargs["pk"])
return form
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update Bus
"""
model = Bus
form_class = BusForm
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()
# We can't update a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["wei"].disabled = True
return form
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Manage Bus
"""
model = Bus
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
bus = self.object
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
.filter(bus=bus).annotate(count=Count("memberships"))
teams_table = BusTeamTable(data=teams, prefix="team-")
context["teams"] = teams_table
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
self.request.user, WEIMembership, "view")).filter(bus=bus)
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
context["memberships"] = memberships_table
return context
class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create BusTeam
"""
model = BusTeam
form_class = BusTeamForm
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
today = date.today()
# We can't add a team once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
bus = Bus.objects.get(pk=self.kwargs["pk"])
context["club"] = bus.wei
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["bus"].initial = Bus.objects.get(pk=self.kwargs["pk"])
return form
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.pk})
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update Bus team
"""
model = BusTeam
form_class = BusTeamForm
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().bus.wei
today = date.today()
# We can't update a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.bus.wei
context["bus"] = self.object.bus
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["bus"].disabled = True
return form
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Manage Bus team
"""
model = BusTeam
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["bus"] = self.object.bus
context["club"] = self.object.bus.wei
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
self.request.user, WEIMembership, "view")).filter(team=self.object)
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
context["memberships"] = memberships_table
return context
class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Register a new user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistrationForm
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
today = date.today()
# We can't register someone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Register 1A")
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
if "myself" in self.request.path:
context["form"].fields["user"].disabled = True
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["user"].initial = self.request.user
del form.fields["first_year"]
del form.fields["caution_check"]
del form.fields["information_json"]
return form
def form_valid(self, form):
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
form.instance.first_year = True
if not form.instance.pk:
# Check if the user is not already registered to the WEI
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
form.add_error('user', _("This user is already registered to this WEI."))
return self.form_invalid(form)
# Check if the user can be in her/his first year (yeah, no cheat)
if WEIRegistration.objects.filter(user=form.instance.user).exists():
form.add_error('user', _("This user can't be in her/his first year since he/she has already"
" participed to a WEI."))
return self.form_invalid(form)
return super().form_valid(form)
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Register an old user to the WEI
"""
model = WEIRegistration
form_class = WEIRegistrationForm
def dispatch(self, request, *args, **kwargs):
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
today = date.today()
# We can't register someone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Register 2A+")
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
if "myself" in self.request.path:
context["form"].fields["user"].disabled = True
choose_bus_form = WEIChooseBusForm()
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
context['membership_form'] = choose_bus_form
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["user"].initial = self.request.user
if "myself" in self.request.path and self.request.user.profile.soge:
form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
del form.fields["caution_check"]
del form.fields["first_year"]
del form.fields["ml_events_registration"]
del form.fields["ml_art_registration"]
del form.fields["ml_sport_registration"]
del form.fields["information_json"]
return form
def form_valid(self, form):
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
form.instance.first_year = False
if not form.instance.pk:
# Check if the user is not already registered to the WEI
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
form.add_error('user', _("This user is already registered to this WEI."))
return self.form_invalid(form)
choose_bus_form = WEIChooseBusForm(self.request.POST)
if not choose_bus_form.is_valid():
return self.form_invalid(form)
information = form.instance.information
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information
form.instance.save()
return super().form_valid(form)
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update a registration for the WEI
"""
model = WEIRegistration
form_class = WEIRegistrationForm
def get_queryset(self, **kwargs):
return WEIRegistration.objects
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()
# We can't update a registration once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
if self.object.is_validated:
membership_form = WEIMembershipForm(instance=self.object.membership)
for field_name, field in membership_form.fields.items():
if not PermissionBackend.check_perm(
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
field.widget = HiddenInput()
context["membership_form"] = membership_form
elif not self.object.first_year and PermissionBackend.check_perm(
self.request.user, "wei.change_weiregistration_information_json", self.object):
choose_bus_form = WEIChooseBusForm(
dict(
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
)
)
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
context["membership_form"] = choose_bus_form
if not self.object.soge_credit and self.object.user.profile.soge:
form = context["form"]
form.fields["soge_credit"].disabled = True
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["user"].disabled = True
if not self.object.first_year:
del form.fields["information_json"]
return form
def form_valid(self, form):
# If the membership is already validated, then we update the bus and the team (and the roles)
if form.instance.is_validated:
membership_form = WEIMembershipForm(self.request.POST, instance=form.instance.membership)
if not membership_form.is_valid():
return self.form_invalid(form)
membership_form.save()
# If it is not validated and if this is an old member, then we update the choices
elif not form.instance.first_year and PermissionBackend.check_perm(
self.request.user, "wei.change_weiregistration_information_json", self.object):
choose_bus_form = WEIChooseBusForm(self.request.POST)
if not choose_bus_form.is_valid():
return self.form_invalid(form)
information = form.instance.information
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information
form.instance.save()
return super().form_valid(form)
def get_success_url(self):
self.object.refresh_from_db()
if self.object.first_year:
survey = CurrentSurvey(self.object)
if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
"""
Delete a non-validated WEI registration
"""
model = WEIRegistration
def dispatch(self, request, *args, **kwargs):
object = self.get_object()
wei = object.wei
today = date.today()
# We can't delete a registration of a past WEI
if today > wei.membership_end:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object):
raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
return context
def get_success_url(self):
return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,))
class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Validate WEI Registration
"""
model = WEIMembership
form_class = WEIMembershipForm
def dispatch(self, request, *args, **kwargs):
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
today = date.today()
# We can't validate anyone once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
context["registration"] = registration
survey = CurrentSurvey(registration)
if survey.information.valid:
context["suggested_bus"] = survey.information.get_selected_bus()
context["club"] = registration.wei
context["fee"] = registration.wei.membership_fee_paid if registration.user.profile.paid \
else registration.wei.membership_fee_unpaid
context["kfet_member"] = Membership.objects.filter(
club__name="Kfet",
user=registration.user,
date_start__lte=datetime.now().date(),
date_end__gte=datetime.now().date(),
).exists()
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
if registration.first_year:
# Use the results of the survey to fill initial data
# A first year has no other role than "1A"
del form.fields["roles"]
survey = CurrentSurvey(registration)
if survey.information.valid:
form.fields["bus"].initial = survey.information.get_selected_bus()
else:
# Use the choice of the member to fill initial data
information = registration.information
if "preferred_bus_pk" in information and len(information["preferred_bus_pk"]) == 1:
form["bus"].initial = Bus.objects.get(pk=information["preferred_bus_pk"][0])
if "preferred_team_pk" in information and len(information["preferred_team_pk"]) == 1:
form["team"].initial = Bus.objects.get(pk=information["preferred_team_pk"][0])
if "preferred_roles_pk" in information:
form["roles"].initial = WEIRole.objects.filter(
Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI")
).all()
return form
def form_valid(self, form):
"""
Create membership, check that all is good, make transactions
"""
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
club = registration.wei
user = registration.user
membership = form.instance
membership.user = user
membership.club = club
membership.date_start = min(date.today(), club.date_start)
membership.registration = registration
if user.profile.paid:
fee = club.membership_fee_paid
else:
fee = club.membership_fee_unpaid
if not registration.soge_credit and user.note.balance < fee:
# Users must have money before registering to the WEI.
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
form.add_error('bus',
_("This user don't have enough money to join this club, and can't have a negative balance."))
return super().form_invalid(form)
if not registration.caution_check and not registration.first_year:
form.add_error('bus', _("This user didn't give her/his caution check."))
return super().form_invalid(form)
if club.parent_club is not None:
if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
return super().form_invalid(form)
# Now, all is fine, the membership can be created.
if registration.first_year:
membership = form.instance
membership.save()
membership.refresh_from_db()
membership.roles.set(WEIRole.objects.filter(name="1A").all())
membership.save()
ret = super().form_valid(form)
membership.refresh_from_db()
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
return ret
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk})
class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
"""
Display the survey for the WEI for first year members.
Warning: this page is accessible for anyone that is connected, the view doesn't extend ProtectQuerySetMixin.
"""
model = WEIRegistration
template_name = "wei/survey.html"
survey = None
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
wei = obj.wei
today = date.today()
# We can't access to the WEI survey once the WEI is started and before the membership start date
if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
if not self.survey:
self.survey = CurrentSurvey(obj)
# If the survey is complete, then display the end page.
if self.survey.is_complete():
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
# Non first year members don't have a survey
if not obj.first_year:
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_form_class(self):
"""
Get the survey form. It may depend on the current state of the survey.
"""
return self.survey.get_form_class()
def get_form(self, form_class=None):
"""
Update the form with the data of the survey.
"""
form = super().get_form(form_class)
self.survey.update_form(form)
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
context["title"] = _("Survey WEI")
return context
def form_valid(self, form):
"""
Update the survey with the data of the form.
"""
self.survey.form_valid(form)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,))
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
template_name = "wei/survey_end.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
context["title"] = _("Survey WEI")
return context
class WEIClosedView(LoginRequiredMixin, TemplateView):
template_name = "wei/survey_closed.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
context["title"] = _("Survey WEI")
return context
class MemberListRenderView(LoginRequiredMixin, View):
"""
Render Invoice as a generated PDF with the given information and a LaTeX template
"""
def get_queryset(self, **kwargs):
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
Lower('bus__name'),
Lower('team__name'),
'user__profile__promotion',
Lower('user__last_name'),
Lower('user__first_name'),
'id',
)
if "bus_pk" in self.kwargs:
qs = qs.filter(bus__pk=self.kwargs["bus_pk"])
if "team_pk" in self.kwargs:
qs = qs.filter(team__pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
return qs.distinct()
def get(self, request, **kwargs):
qs = self.get_queryset()
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
bus = team = None
if "bus_pk" in self.kwargs:
bus = Bus.objects.get(pk=self.kwargs["bus_pk"])
if "team_pk" in self.kwargs:
team = BusTeam.objects.filter(pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
if team.exists():
team = team.get()
bus = team.bus
else:
team = dict(name="Staff")
# Fill the template with the information
tex = render_to_string("wei/weilist_sample.tex", dict(memberships=qs.all(), wei=wei, bus=bus, team=team))
try:
os.mkdir(BASE_DIR + "/tmp")
except FileExistsError:
pass
# We render the file in a temporary directory
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
try:
with open("{}/wei-list.tex".format(tmp_dir), "wb") as f:
f.write(tex.encode("UTF-8"))
del tex
error = subprocess.Popen(
["pdflatex", "{}/wei-list.tex".format(tmp_dir)],
cwd=tmp_dir,
stdin=open(os.devnull, "r"),
stderr=open(os.devnull, "wb"),
stdout=open(os.devnull, "wb"),
).wait()
if error:
raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")")
# Display the generated pdf as a HTTP Response
pdf = open("{}/wei-list.pdf".format(tmp_dir), 'rb').read()
response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=Liste%20des%20participants%20au%20WEI.pdf"
except IOError as e:
raise e
finally:
# Delete all temporary files
shutil.rmtree(tmp_dir)
return response

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -3,7 +3,7 @@
from json import dumps as json_dumps
from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput
from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput, Widget
class AmountInput(NumberInput):
@ -41,6 +41,29 @@ class Autocomplete(TextInput):
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:
https://github.com/monim67/django-bootstrap-datepicker-plus

View File

@ -39,41 +39,20 @@ else:
from .development import *
try:
#in secrets.py defines everything you want
# in secrets.py defines everything you want
from .secrets import *
INSTALLED_APPS += OPTIONAL_APPS
except ImportError:
pass
if "cas" in INSTALLED_APPS:
MIDDLEWARE += ['cas.middleware.CASMiddleware']
if "cas_server" in INSTALLED_APPS:
# CAS Settings
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
CAS_SHOW_SERVICE_MESSAGES = True
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:
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)

View File

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

View File

@ -62,10 +62,6 @@ CSRF_COOKIE_HTTPONLY = False
X_FRAME_OPTIONS = 'DENY'
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
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')]

View File

@ -51,6 +51,3 @@ CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False
X_FRAME_OPTIONS = 'DENY'
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.contrib import admin
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 member.views import CustomLoginView
@ -19,14 +20,16 @@ urlpatterns = [
path('registration/', include('registration.urls')),
path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),
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/', include('django.contrib.auth.urls')),
path('api/', include('api.urls')),
path('permission/', include('permission.urls')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
@ -44,3 +47,11 @@ if "debug_toolbar" in settings.INSTALLED_APPS:
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + 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

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";
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 = "";
objects.results.forEach(function (obj) {

View File

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

View File

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

View File

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

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"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<link rel="stylesheet"
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/all.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
@ -58,6 +58,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
cursor: pointer;
text-decoration: underline;
}
.tooltip.show {
opacity: 1; /* opaque tooltip */
}
.tooltip-inner {
background-color: #fff;
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
border: 1px solid rgba(0,0,0,.250);
color: #000;
margin: 0 .5rem .25rem .5rem;
padding: 0;
}
.bs-tooltip-bottom .arrow::before {
border-bottom-color: rgba(0,0,0,.250);
}
</style>
{% block extracss %}{% endblock %}
@ -76,39 +90,52 @@ SPDX-License-Identifier: GPL-3.0-or-later
<ul class="navbar-nav">
{% if "note.transactiontemplate"|not_empty_model_list %}
<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>
{% endif %}
{% if "note.transaction"|not_empty_model_list %}
<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>
{% endif %}
{% if "auth.user"|model_list|length >= 2 %}
<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>
{% endif %}
{% if "member.club"|not_empty_model_list %}
<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>
{% endif %}
{% if "member.change_profile_registration_valid"|has_perm:user %}
<li class="nav-item active">
<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>
</li>
{% endif %}
{% if "activity.activity"|not_empty_model_list %}
<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>
{% endif %}
{% if "treasury.invoice"|not_empty_model_change_list %}
{% if "treasury.invoice"|not_empty_model_list %}
<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>
{% endif %}
</ul>
@ -116,28 +143,28 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
<li class="dropdown">
<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>
</a>
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="navbarDropdownMenuLink">
<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 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>
</div>
</li>
{% else %}
<li class="nav-item active">
<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>
</li>
<li class="nav-item active">
<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>
</li>
{% 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 %}
<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 %}
{% if club.require_memberships %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>
{% if club.membership_start %}
<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>
<dd class="col-xl-6">{{ club.membership_end }}</dd>
{% if club.membership_end %}
<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>
<dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd>
{% if club.membership_duration %}
<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 %}
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
@ -39,23 +45,31 @@
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd>
{% endif %}
{% endif %}
{% if "note.view_note"|has_perm:club.note %}
<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-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
<dd class="col-xl-6 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd>
<dt class="col-xl-3">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></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>
</div>
<div class="card-footer text-center">
{% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a>
{% endif %}
{% if ".change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.path_info != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} </div>
{% if not club.weiclub %}
<div class="card-footer text-center">
{% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a>
{% endif %}
{% if ".change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.path_info != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %}
</div>
{% endif %}
</div>

View File

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

View File

@ -1,23 +1,27 @@
{% load render_table from django_tables2 %}
{% load i18n %}
<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-users"></i> {% trans "Member of the Club" %}
</a>
</div>
{% if member_list.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-users"></i> {% trans "Member of the Club" %}
</a>
</div>
{% 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>
<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">
{% render_table history_list %}
</div>
</div>
</div>
{% endif %}

View File

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

View File

@ -17,12 +17,14 @@
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.username }}</dd>
<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>
{% if object.pk == user.pk %}
<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>
{% endif %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.section }}</dd>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
{% block content %}
{% if validlink %}
{% 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 %}
{% else %}
{% trans "You must pay now your membership in the Kfet to complete your registration." %}

View File

@ -31,13 +31,6 @@
</dd>
{% 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>
<dd class="col-xl-6">{{ object.profile.section }}</dd>

View File

@ -16,13 +16,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% endblocktrans %}
</p>
{% 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 | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">

View File

@ -5,13 +5,18 @@
{% block title %}{% trans "Sign up" %}{% endblock %}
{% block content %}
<h2>{% trans "Sign up" %}</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
<h2>{% trans "Sign up" %}</h2>
<div class="alert alert-warning">
{% blocktrans %}If you already signed up, your registration is taken into account. The BDE must validate your account before your can log in. You have to go to the Kfet and pay the registration fee. You must also validate your email address by following the link you received.{% endblocktrans %}
</div>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
{% 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 }}

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