mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-12-22 23:42:25 +00:00
Merge branch 'master' into 'tranfer_front'
# Conflicts: # apps/activity/views.py # apps/permission/backends.py # locale/de/LC_MESSAGES/django.po # locale/fr/LC_MESSAGES/django.po # static/js/base.js # templates/base.html # templates/member/user_list.html
This commit is contained in:
commit
395c52220f
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -46,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
|
||||
|
||||
|
@ -15,6 +15,7 @@ from note.api.urls import register_note_urls
|
||||
from treasury.api.urls import register_treasury_urls
|
||||
from logs.api.urls import register_logs_urls
|
||||
from permission.api.urls import register_permission_urls
|
||||
from wei.api.urls import register_wei_urls
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@ -78,6 +79,7 @@ register_note_urls(router, 'note')
|
||||
register_treasury_urls(router, 'treasury')
|
||||
register_permission_urls(router, 'permission')
|
||||
register_logs_urls(router, 'logs')
|
||||
register_wei_urls(router, 'wei')
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
|
@ -9,7 +9,7 @@ from note.models import NoteSpecial
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from permission.models import PermissionMask
|
||||
|
||||
from .models import Profile, Club, Membership
|
||||
from .models import Profile, Club, Membership, Role
|
||||
|
||||
|
||||
class CustomAuthenticationForm(AuthenticationForm):
|
||||
@ -25,10 +25,16 @@ class ProfileForm(forms.ModelForm):
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
"""
|
||||
|
||||
def save(self, commit=True):
|
||||
if not self.instance.section or (("department" in self.changed_data
|
||||
or "promotion" in self.changed_data) and "section" not in self.changed_data):
|
||||
self.instance.section = self.instance.section_generated
|
||||
return super().save(commit)
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', )
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', )
|
||||
|
||||
|
||||
class ClubForm(forms.ModelForm):
|
||||
@ -50,6 +56,8 @@ class ClubForm(forms.ModelForm):
|
||||
|
||||
|
||||
class MembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
|
||||
|
||||
soge = forms.BooleanField(
|
||||
label=_("Inscription paid by Société Générale"),
|
||||
required=False,
|
||||
|
@ -23,18 +23,20 @@ class Profile(models.Model):
|
||||
|
||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||
so this model add an user profile with additional information.
|
||||
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
phone_number = models.CharField(
|
||||
verbose_name=_('phone number'),
|
||||
max_length=50,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
section = models.CharField(
|
||||
verbose_name=_('section'),
|
||||
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
|
||||
@ -42,12 +44,44 @@ class Profile(models.Model):
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
department = models.CharField(
|
||||
max_length=8,
|
||||
verbose_name=_("department"),
|
||||
choices=[
|
||||
('A0', _("Informatics (A0)")),
|
||||
('A1', _("Mathematics (A1)")),
|
||||
('A2', _("Physics (A2)")),
|
||||
("A'2", _("Applied physics (A'2)")),
|
||||
('A''2', _("Chemistry (A''2)")),
|
||||
('A3', _("Biology (A3)")),
|
||||
('B1234', _("SAPHIRE (B1234)")),
|
||||
('B1', _("Mechanics (B1)")),
|
||||
('B2', _("Civil engineering (B2)")),
|
||||
('B3', _("Mechanical engineering (B3)")),
|
||||
('B4', _("EEA (B4)")),
|
||||
('C', _("Design (C)")),
|
||||
('D2', _("Economy-management (D2)")),
|
||||
('D3', _("Social sciences (D3)")),
|
||||
('E', _("English (E)")),
|
||||
('EXT', _("External (EXT)")),
|
||||
]
|
||||
)
|
||||
|
||||
promotion = models.PositiveIntegerField(
|
||||
null=True,
|
||||
default=datetime.date.today().year,
|
||||
verbose_name=_("promotion"),
|
||||
help_text=_("Year of entry to the school (None if not ENS student)"),
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
verbose_name=_('address'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
paid = models.BooleanField(
|
||||
verbose_name=_("paid"),
|
||||
help_text=_("Tells if the user receive a salary."),
|
||||
@ -64,11 +98,29 @@ class Profile(models.Model):
|
||||
default=False,
|
||||
)
|
||||
|
||||
soge = models.BooleanField(
|
||||
verbose_name=_("Société générale"),
|
||||
help_text=_("Has the user ever be paid by the Société générale?"),
|
||||
default=False,
|
||||
)
|
||||
@property
|
||||
def ens_year(self):
|
||||
"""
|
||||
Number of years since the 1st august of the entry year, rounded up.
|
||||
"""
|
||||
if self.promotion is None:
|
||||
return 0
|
||||
today = datetime.date.today()
|
||||
years = today.year - self.promotion
|
||||
if today.month >= 8:
|
||||
years += 1
|
||||
return years
|
||||
|
||||
@property
|
||||
def section_generated(self):
|
||||
return str(self.ens_year) + self.department
|
||||
|
||||
@property
|
||||
def soge(self):
|
||||
if "treasury" in settings.INSTALLED_APPS:
|
||||
from treasury.models import SogeCredit
|
||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
@ -85,7 +137,7 @@ class Profile(models.Model):
|
||||
'user': self.user,
|
||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||
'token': email_validation_token.make_token(self.user),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)).decode('UTF-8'),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
||||
})
|
||||
self.user.email_user(subject, message)
|
||||
|
||||
@ -171,6 +223,7 @@ class Club(models.Model):
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
self._force_save = True
|
||||
self.save(force_update=True)
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
@ -220,6 +273,7 @@ class Membership(models.Model):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
@ -308,7 +362,20 @@ class Membership(models.Model):
|
||||
reason="Adhésion " + self.club.name,
|
||||
)
|
||||
transaction._force_save = True
|
||||
transaction.save(force_insert=True)
|
||||
print(hasattr(self, '_soge'))
|
||||
if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS:
|
||||
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
|
||||
# to treasurers.
|
||||
transaction.valid = False
|
||||
from treasury.models import SogeCredit
|
||||
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
|
||||
soge_credit.refresh_from_db()
|
||||
transaction.save(force_insert=True)
|
||||
transaction.refresh_from_db()
|
||||
soge_credit.transactions.add(transaction)
|
||||
soge_credit.save()
|
||||
else:
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
@ -18,7 +18,7 @@ from django.views.generic.edit import FormMixin
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from note.forms import ImageForm
|
||||
from note.models import Alias, NoteUser, NoteSpecial
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from permission.backends import PermissionBackend
|
||||
@ -128,7 +128,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||
.order_by("-created_at", "-id")\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||
@ -165,7 +166,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(profile__section__iregex=pattern)
|
||||
| Q(profile__username__iregex="^" + pattern)
|
||||
| Q(username__iregex="^" + pattern)
|
||||
| Q(note__alias__name__iregex="^" + pattern)
|
||||
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
|
||||
)
|
||||
@ -314,7 +315,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
club.update_membership_dates()
|
||||
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||
.order_by('-created_at', '-id')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
context['history_list'] = history_table
|
||||
@ -365,6 +367,15 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form_class = ClubForm
|
||||
template_name = "member/club_form.html"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
|
||||
# Don't update a WEI club through this view
|
||||
if "wei" in settings.INSTALLED_APPS:
|
||||
qs = qs.filter(weiclub=None)
|
||||
|
||||
return qs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
@ -396,7 +407,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
if "club_pk" in self.kwargs:
|
||||
# We create a new membership.
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||
.get(pk=self.kwargs["club_pk"])
|
||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
|
||||
|
||||
@ -463,17 +474,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
bank = form.cleaned_data["bank"]
|
||||
soge = form.cleaned_data["soge"] and not user.profile.soge and club.name == "BDE"
|
||||
|
||||
# If Société générale pays, then we auto-fill some data
|
||||
# If Société générale pays, then we store that information but the payment must be controlled by treasurers
|
||||
# later. The membership transaction will be invalidated.
|
||||
if soge:
|
||||
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire")
|
||||
bde = club
|
||||
kfet = Club.objects.get(name="Kfet")
|
||||
if user.profile.paid:
|
||||
fee = bde.membership_fee_paid + kfet.membership_fee_paid
|
||||
else:
|
||||
fee = bde.membership_fee_unpaid + kfet.membership_fee_unpaid
|
||||
credit_amount = fee
|
||||
bank = "Société générale"
|
||||
credit_type = None
|
||||
form.instance._soge = True
|
||||
|
||||
if credit_type is None:
|
||||
credit_amount = 0
|
||||
@ -521,6 +526,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
|
||||
# Now, all is fine, the membership can be created.
|
||||
|
||||
if club.name == "BDE":
|
||||
# When we renew the BDE membership, we update the profile section.
|
||||
# We could automate that and remove the section field from the Profile model,
|
||||
# but with this way users can customize their section as they want.
|
||||
user.profile.section = user.profile.section_generated
|
||||
user.profile.save()
|
||||
|
||||
# Credit note before the membership is created.
|
||||
if credit_amount > 0:
|
||||
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
||||
@ -544,11 +556,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
valid=True,
|
||||
)
|
||||
|
||||
# If Société générale pays, then we store the information: the bank can't pay twice to a same person.
|
||||
if soge:
|
||||
user.profile.soge = True
|
||||
user.profile.save()
|
||||
ret = super().form_valid(form)
|
||||
|
||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||
# Kfet membership.
|
||||
if soge:
|
||||
kfet = Club.objects.get(name="Kfet")
|
||||
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||
|
||||
@ -560,20 +572,23 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
date_end__gte=datetime.today(),
|
||||
)
|
||||
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=kfet,
|
||||
user=user,
|
||||
fee=kfet_fee,
|
||||
date_start=old_membership.get().date_end + timedelta(days=1)
|
||||
if old_membership.exists() else form.instance.date_start,
|
||||
)
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
if old_membership.exists():
|
||||
membership.roles.set(old_membership.get().roles.all())
|
||||
else:
|
||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||
membership.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -31,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):
|
||||
"""
|
||||
@ -121,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):
|
||||
"""
|
||||
|
@ -37,21 +37,16 @@ class PermissionBackend(ModelBackend):
|
||||
# Unauthenticated users have no permissions
|
||||
return Permission.objects.none()
|
||||
|
||||
perms = 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", 42),
|
||||
)
|
||||
if settings.DATABASES[perms.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
|
||||
# We want one permission per club, and per permission type.
|
||||
# SQLite does not support this kind of filter, that's why we don't filter the permissions with this
|
||||
# kind of DB. This only increases performances (we can check multiple times the same permission)
|
||||
# but don't have any semantic influence.
|
||||
perms = perms.distinct('club', 'pk')
|
||||
return perms
|
||||
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
|
||||
def permissions(user, model, type):
|
||||
@ -63,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:
|
||||
@ -72,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,
|
||||
@ -83,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
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -120,7 +120,12 @@ class Permission(models.Model):
|
||||
('delete', 'delete')
|
||||
]
|
||||
|
||||
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
|
||||
model = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+',
|
||||
verbose_name=_("model"),
|
||||
)
|
||||
|
||||
# A json encoded Q object with the following grammar
|
||||
# query -> [] | {} (the empty query representing all objects)
|
||||
@ -142,18 +147,34 @@ class Permission(models.Model):
|
||||
# Examples:
|
||||
# Q(is_superuser=True) := {"is_superuser": true}
|
||||
# ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
|
||||
query = models.TextField()
|
||||
query = models.TextField(
|
||||
verbose_name=_("query"),
|
||||
)
|
||||
|
||||
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
|
||||
type = models.CharField(
|
||||
max_length=15,
|
||||
choices=PERMISSION_TYPES,
|
||||
verbose_name=_("type"),
|
||||
)
|
||||
|
||||
mask = models.ForeignKey(
|
||||
PermissionMask,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="permissions",
|
||||
verbose_name=_("mask"),
|
||||
)
|
||||
|
||||
field = models.CharField(max_length=255, blank=True)
|
||||
field = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("field"),
|
||||
)
|
||||
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('model', 'query', 'type', 'field')
|
||||
@ -277,24 +298,22 @@ class Permission(models.Model):
|
||||
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.field:
|
||||
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
||||
else:
|
||||
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
||||
return self.description
|
||||
|
||||
|
||||
class RolePermissions(models.Model):
|
||||
"""
|
||||
Permissions associated with a Role
|
||||
"""
|
||||
role = models.ForeignKey(
|
||||
role = models.OneToOneField(
|
||||
Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
related_name='permissions',
|
||||
verbose_name=_('role'),
|
||||
)
|
||||
permissions = models.ManyToManyField(
|
||||
Permission,
|
||||
verbose_name=_("permissions"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
@ -57,13 +58,19 @@ def pre_save_object(sender, instance, **kwargs):
|
||||
if old_value == new_value:
|
||||
continue
|
||||
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to change the field {field} on this instance of model"
|
||||
" {app_label}.{model_name}.")
|
||||
.format(field=field_name, app_label=app_label, model_name=model_name, )
|
||||
)
|
||||
else:
|
||||
# We check if the user has right to add the object
|
||||
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
|
||||
|
||||
if not has_perm:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to add this instance of model {app_label}.{model_name}.")
|
||||
.format(app_label=app_label, model_name=model_name, ))
|
||||
|
||||
|
||||
def pre_delete_object(instance, **kwargs):
|
||||
@ -88,4 +95,6 @@ def pre_delete_object(instance, **kwargs):
|
||||
|
||||
# We check if the user has rights to delete the object
|
||||
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
|
||||
.format(app_label=app_label, model_name=model_name))
|
||||
|
10
apps/permission/urls.py
Normal file
10
apps/permission/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
from permission.views import RightsView
|
||||
|
||||
app_name = 'permission'
|
||||
urlpatterns = [
|
||||
path('rights', RightsView.as_view(), name="rights"),
|
||||
]
|
@ -1,11 +1,60 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import date
|
||||
|
||||
from permission.backends import PermissionBackend
|
||||
from django.forms import HiddenInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import UpdateView, TemplateView
|
||||
from member.models import Role, Membership
|
||||
|
||||
from .backends import PermissionBackend
|
||||
|
||||
|
||||
class ProtectQuerysetMixin:
|
||||
"""
|
||||
This is a View class decorator and not a proper View class.
|
||||
Ensure that the user has the right to see or update objects.
|
||||
Display 404 error if the user can't see an object, remove the fields the user can't
|
||||
update on an update form (useful if the user can't change only specified fields).
|
||||
"""
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
|
||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
|
||||
if not isinstance(self, UpdateView):
|
||||
return form
|
||||
|
||||
# If we are in an UpdateView, we display only the fields the user has right to see.
|
||||
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
|
||||
# a custom request.
|
||||
# We could also delete the field, but some views might be affected.
|
||||
for key in form.base_fields:
|
||||
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object):
|
||||
form.fields[key].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class RightsView(TemplateView):
|
||||
template_name = "permission/all_rights.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = _("All rights")
|
||||
roles = Role.objects.all()
|
||||
context["roles"] = roles
|
||||
if self.request.user.is_authenticated:
|
||||
active_memberships = Membership.objects.filter(user=self.request.user,
|
||||
date_start__lte=date.today(),
|
||||
date_end__gte=date.today()).all()
|
||||
else:
|
||||
active_memberships = Membership.objects.none()
|
||||
|
||||
for role in roles:
|
||||
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
|
||||
|
||||
return context
|
||||
|
@ -27,6 +27,15 @@ class SignUpForm(UserCreationForm):
|
||||
fields = ('first_name', 'last_name', 'username', 'email', )
|
||||
|
||||
|
||||
class WEISignupForm(forms.Form):
|
||||
wei_registration = forms.BooleanField(
|
||||
label=_("Register to the WEI"),
|
||||
required=False,
|
||||
help_text=_("Check this case if you want to register to the WEI. If you hesitate, you will be able to register"
|
||||
" later, after validating your account in the Kfet."),
|
||||
)
|
||||
|
||||
|
||||
class ValidationForm(forms.Form):
|
||||
"""
|
||||
Validate the inscription of the new users and pay memberships.
|
||||
|
@ -11,12 +11,12 @@ from django.urls import reverse_lazy
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, TemplateView, DetailView, FormView
|
||||
from django.views.generic import CreateView, TemplateView, DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2 import SingleTableView
|
||||
from member.forms import ProfileForm
|
||||
from member.models import Membership, Club, Role
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
@ -32,13 +32,13 @@ class UserCreateView(CreateView):
|
||||
"""
|
||||
|
||||
form_class = SignUpForm
|
||||
success_url = reverse_lazy('registration:email_validation_sent')
|
||||
template_name = 'registration/signup.html'
|
||||
second_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["profile_form"] = self.second_form()
|
||||
del context["profile_form"].fields["section"]
|
||||
|
||||
return context
|
||||
|
||||
@ -67,6 +67,9 @@ class UserCreateView(CreateView):
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('registration:email_validation_sent')
|
||||
|
||||
|
||||
class UserValidateView(TemplateView):
|
||||
"""
|
||||
@ -112,7 +115,7 @@ class UserValidateView(TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['user'] = self.get_user(self.kwargs["uidb64"])
|
||||
context['user_object'] = self.get_user(self.kwargs["uidb64"])
|
||||
context['login_url'] = resolve_url(settings.LOGIN_URL)
|
||||
if self.validlink:
|
||||
context['validlink'] = True
|
||||
@ -263,17 +266,17 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
fee += kfet_fee
|
||||
|
||||
if soge:
|
||||
# Fill payment information if Société Générale pays the inscription
|
||||
credit_type = NoteSpecial.objects.get(special_type="Virement bancaire")
|
||||
credit_amount = fee
|
||||
bank = "Société générale"
|
||||
# If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
||||
# and credit the note later.
|
||||
credit_type = None
|
||||
|
||||
print("OK")
|
||||
if credit_type is None:
|
||||
credit_amount = 0
|
||||
|
||||
if join_Kfet and not join_BDE:
|
||||
form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club."))
|
||||
|
||||
if fee > credit_amount:
|
||||
if fee > credit_amount and not soge:
|
||||
# Check if the user credits enough money
|
||||
form.add_error('credit_type',
|
||||
_("The entered amount is not enough for the memberships, should be at least {}")
|
||||
@ -295,10 +298,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
ret = super().form_valid(form)
|
||||
user.is_active = user.profile.email_confirmed or user.is_superuser
|
||||
user.profile.registration_valid = True
|
||||
# Store if Société générale paid for next years
|
||||
user.profile.soge = soge
|
||||
user.save()
|
||||
user.profile.save()
|
||||
user.refresh_from_db()
|
||||
|
||||
if credit_type is not None and credit_amount > 0:
|
||||
# Credit the note
|
||||
@ -316,21 +318,29 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
|
||||
if join_BDE:
|
||||
# Create membership for the user to the BDE starting today
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=bde,
|
||||
user=user,
|
||||
fee=bde_fee,
|
||||
)
|
||||
if soge:
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||
membership.save()
|
||||
|
||||
if join_Kfet:
|
||||
# Create membership for the user to the Kfet starting today
|
||||
membership = Membership.objects.create(
|
||||
membership = Membership(
|
||||
club=kfet,
|
||||
user=user,
|
||||
fee=kfet_fee,
|
||||
)
|
||||
if soge:
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||
membership.save()
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit b9fdced3c2ce34168b8f0d6004a20a69ca16e0de
|
||||
Subproject commit f0aa426950b9b867bf99233795e260871be2cb99
|
@ -3,7 +3,7 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import RemittanceType, Remittance
|
||||
from .models import RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
@admin.register(RemittanceType)
|
||||
@ -25,3 +25,6 @@ class RemittanceAdmin(admin.ModelAdmin):
|
||||
if not obj:
|
||||
return True
|
||||
return not obj.closed and super().has_change_permission(request, obj)
|
||||
|
||||
|
||||
admin.site.register(SogeCredit)
|
||||
|
@ -4,7 +4,7 @@
|
||||
from rest_framework import serializers
|
||||
from note.api.serializers import SpecialTransactionSerializer
|
||||
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
@ -60,3 +60,14 @@ class RemittanceSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_transactions(self, obj):
|
||||
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions)
|
||||
|
||||
|
||||
class SogeCreditSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for SogeCredit types.
|
||||
The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SogeCredit
|
||||
fields = '__all__'
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet
|
||||
from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, SogeCreditViewSet
|
||||
|
||||
|
||||
def register_treasury_urls(router, path):
|
||||
@ -12,3 +12,4 @@ def register_treasury_urls(router, path):
|
||||
router.register(path + '/product', ProductViewSet)
|
||||
router.register(path + '/remittance_type', RemittanceTypeViewSet)
|
||||
router.register(path + '/remittance', RemittanceViewSet)
|
||||
router.register(path + '/soge_credit', SogeCreditViewSet)
|
||||
|
@ -5,8 +5,9 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer,\
|
||||
SogeCreditSerializer
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||
@ -39,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
||||
then render it on /api/treasury/remittance_type/
|
||||
"""
|
||||
queryset = RemittanceType.objects.all()
|
||||
queryset = RemittanceType.objects
|
||||
serializer_class = RemittanceTypeSerializer
|
||||
|
||||
|
||||
@ -49,5 +50,15 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/remittance/
|
||||
"""
|
||||
queryset = Remittance.objects.all()
|
||||
queryset = Remittance.objects
|
||||
serializer_class = RemittanceSerializer
|
||||
|
||||
|
||||
class SogeCreditViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/soge_credit/
|
||||
"""
|
||||
queryset = SogeCredit.objects
|
||||
serializer_class = SogeCreditSerializer
|
||||
|
@ -1,11 +1,13 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, SpecialTransaction
|
||||
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
@ -207,3 +209,101 @@ class SpecialTransactionProxy(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("special transaction proxy")
|
||||
verbose_name_plural = _("special transaction proxies")
|
||||
|
||||
|
||||
class SogeCredit(models.Model):
|
||||
"""
|
||||
Manage the credits from the Société générale.
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
transactions = models.ManyToManyField(
|
||||
MembershipTransaction,
|
||||
related_name="+",
|
||||
verbose_name=_("membership transactions"),
|
||||
)
|
||||
|
||||
credit_transaction = models.OneToOneField(
|
||||
SpecialTransaction,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("credit transaction"),
|
||||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self.credit_transaction is not None
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
return sum(transaction.total for transaction in self.transactions.all())
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Invalidating a Société générale delete the transaction of the bank if it was already created.
|
||||
Treasurers must know what they do, With Great Power Comes Great Responsibility...
|
||||
"""
|
||||
if self.valid:
|
||||
self.credit_transaction.valid = False
|
||||
self.credit_transaction._force_save = True
|
||||
self.credit_transaction.save()
|
||||
self.credit_transaction._force_delete = True
|
||||
self.credit_transaction.delete()
|
||||
self.credit_transaction = None
|
||||
for transaction in self.transactions.all():
|
||||
transaction.valid = False
|
||||
transaction._force_save = True
|
||||
transaction.save()
|
||||
|
||||
def validate(self, force=False):
|
||||
if self.valid and not force:
|
||||
# The credit is already done
|
||||
return
|
||||
|
||||
# First invalidate all transaction and delete the credit if already did (and force mode)
|
||||
self.invalidate()
|
||||
self.credit_transaction = SpecialTransaction.objects.create(
|
||||
source=NoteSpecial.objects.get(special_type="Virement bancaire"),
|
||||
destination=self.user.note,
|
||||
quantity=1,
|
||||
amount=self.amount,
|
||||
reason="Crédit société générale",
|
||||
last_name=self.user.last_name,
|
||||
first_name=self.user.first_name,
|
||||
bank="Société générale",
|
||||
)
|
||||
self.save()
|
||||
|
||||
for transaction in self.transactions.all():
|
||||
transaction.valid = True
|
||||
transaction._force_save = True
|
||||
transaction.created_at = datetime.now()
|
||||
transaction.save()
|
||||
|
||||
def delete(self, **kwargs):
|
||||
"""
|
||||
Deleting a SogeCredit is equivalent to say that the Société générale didn't pay.
|
||||
Treasurers must know what they do, this is difficult to undo this operation.
|
||||
With Great Power Comes Great Responsibility...
|
||||
"""
|
||||
|
||||
total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid)
|
||||
if self.user.note.balance < total_fee:
|
||||
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. "
|
||||
"Please ask her/him to credit the note before invalidating this credit."))
|
||||
|
||||
self.invalidate()
|
||||
for transaction in self.transactions.all():
|
||||
transaction._force_save = True
|
||||
transaction.valid = True
|
||||
transaction.created_at = datetime.now()
|
||||
transaction.save()
|
||||
super().delete(**kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Credit from the Société générale")
|
||||
verbose_name_plural = _("Credits from the Société générale")
|
||||
|
@ -7,7 +7,7 @@ from django_tables2 import A
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
|
||||
from .models import Invoice, Remittance
|
||||
from .models import Invoice, Remittance, SogeCredit
|
||||
|
||||
|
||||
class InvoiceTable(tables.Table):
|
||||
@ -101,3 +101,28 @@ class SpecialTransactionTable(tables.Table):
|
||||
model = SpecialTransaction
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
|
||||
|
||||
|
||||
class SogeCreditTable(tables.Table):
|
||||
user = tables.LinkColumn(
|
||||
'treasury:manage_soge_credit',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
amount = tables.Column(
|
||||
verbose_name=_("Amount"),
|
||||
)
|
||||
|
||||
valid = tables.Column(
|
||||
verbose_name=_("Valid"),
|
||||
)
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
def render_valid(self, value):
|
||||
return _("Yes") if value else _("No")
|
||||
|
||||
class Meta:
|
||||
model = SogeCredit
|
||||
fields = ('user', 'amount', 'valid', )
|
||||
|
@ -4,7 +4,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\
|
||||
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView
|
||||
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView,\
|
||||
SogeCreditListView, SogeCreditManageView
|
||||
|
||||
app_name = 'treasury'
|
||||
urlpatterns = [
|
||||
@ -21,4 +22,7 @@ urlpatterns = [
|
||||
path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'),
|
||||
path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(),
|
||||
name='unlink_transaction'),
|
||||
|
||||
path('soge-credits/list/', SogeCreditListView.as_view(), name='soge_credits'),
|
||||
path('soge-credits/manage/<int:pk>/', SogeCreditManageView.as_view(), name='manage_soge_credit'),
|
||||
]
|
||||
|
@ -10,21 +10,23 @@ from crispy_forms.helper import FormHelper
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms import Form
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.views.generic.base import View, TemplateView
|
||||
from django.views.generic.edit import BaseFormView
|
||||
from django_tables2 import SingleTableView
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from note.models import SpecialTransaction, NoteSpecial, Alias
|
||||
from note_kfet.settings.base import BASE_DIR
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
|
||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
|
||||
|
||||
|
||||
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
@ -180,7 +182,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
||||
# Display the generated pdf as a HTTP Response
|
||||
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk)
|
||||
response['Content-Disposition'] = "inline;filename=Facture%20n°{:d}.pdf".format(pk)
|
||||
except IOError as e:
|
||||
raise e
|
||||
finally:
|
||||
@ -307,3 +309,61 @@ class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
|
||||
transaction.save()
|
||||
|
||||
return redirect('treasury:remittance_list')
|
||||
|
||||
|
||||
class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
|
||||
"""
|
||||
List all Société Générale credits
|
||||
"""
|
||||
model = SogeCredit
|
||||
table_class = SogeCreditTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Filter the table with the given parameter.
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
if "valid" in self.request.GET:
|
||||
q = Q(credit_transaction=None)
|
||||
if not self.request.GET["valid"]:
|
||||
q = ~q
|
||||
qs = qs.filter(q)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
|
||||
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
|
||||
"""
|
||||
Manage credits from the Société générale.
|
||||
"""
|
||||
model = SogeCredit
|
||||
form_class = Form
|
||||
|
||||
def form_valid(self, form):
|
||||
if "validate" in form.data:
|
||||
self.get_object().validate(True)
|
||||
elif "delete" in form.data:
|
||||
self.get_object().delete()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
if "validate" in self.request.POST:
|
||||
return reverse_lazy('treasury:manage_soge_credit', args=(self.get_object().pk,))
|
||||
return reverse_lazy('treasury:soge_credits')
|
||||
|
4
apps/wei/__init__.py
Normal file
4
apps/wei/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'wei.apps.WeiConfig'
|
13
apps/wei/admin.py
Normal file
13
apps/wei/admin.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
|
||||
|
||||
admin.site.register(WEIClub)
|
||||
admin.site.register(WEIRegistration)
|
||||
admin.site.register(WEIMembership)
|
||||
admin.site.register(WEIRole)
|
||||
admin.site.register(Bus)
|
||||
admin.site.register(BusTeam)
|
0
apps/wei/api/__init__.py
Normal file
0
apps/wei/api/__init__.py
Normal file
72
apps/wei/api/serializers.py
Normal file
72
apps/wei/api/serializers.py
Normal file
@ -0,0 +1,72 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
|
||||
|
||||
|
||||
class WEIClubSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Clubs.
|
||||
The djangorestframework plugin will analyse the model `WEIClub` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIClub
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BusSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Bus.
|
||||
The djangorestframework plugin will analyse the model `Bus` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Bus
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BusTeamSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Bus teams.
|
||||
The djangorestframework plugin will analyse the model `BusTeam` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = BusTeam
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIRoleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI roles.
|
||||
The djangorestframework plugin will analyse the model `WEIRole` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIRole
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIRegistrationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI registrations.
|
||||
The djangorestframework plugin will analyse the model `WEIRegistration` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIRegistration
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WEIMembershipSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for WEI memberships.
|
||||
The djangorestframework plugin will analyse the model `WEIMembership` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WEIMembership
|
||||
fields = '__all__'
|
17
apps/wei/api/urls.py
Normal file
17
apps/wei/api/urls.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import WEIClubViewSet, BusViewSet, BusTeamViewSet, WEIRoleViewSet, WEIRegistrationViewSet, \
|
||||
WEIMembershipViewSet
|
||||
|
||||
|
||||
def register_wei_urls(router, path):
|
||||
"""
|
||||
Configure router for Member REST API.
|
||||
"""
|
||||
router.register(path + '/club', WEIClubViewSet)
|
||||
router.register(path + '/bus', BusViewSet)
|
||||
router.register(path + '/team', BusTeamViewSet)
|
||||
router.register(path + '/role', WEIRoleViewSet)
|
||||
router.register(path + '/registration', WEIRegistrationViewSet)
|
||||
router.register(path + '/membership', WEIMembershipViewSet)
|
86
apps/wei/api/views.py
Normal file
86
apps/wei/api/views.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
|
||||
WEIRegistrationSerializer, WEIMembershipSerializer
|
||||
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
|
||||
|
||||
|
||||
class WEIClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/club/
|
||||
"""
|
||||
queryset = WEIClub.objects.all()
|
||||
serializer_class = WEIClubSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'year', ]
|
||||
|
||||
|
||||
class BusViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/bus/
|
||||
"""
|
||||
queryset = Bus.objects
|
||||
serializer_class = BusSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'wei', ]
|
||||
|
||||
|
||||
class BusTeamViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/team/
|
||||
"""
|
||||
queryset = BusTeam.objects
|
||||
serializer_class = BusTeamSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$name', ]
|
||||
filterset_fields = ['name', 'bus', 'bus__wei', ]
|
||||
|
||||
|
||||
class WEIRoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/role/
|
||||
"""
|
||||
queryset = WEIRole.objects
|
||||
serializer_class = WEIRoleSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class WEIRegistrationViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/registration/
|
||||
"""
|
||||
queryset = WEIRegistration.objects
|
||||
serializer_class = WEIRegistrationSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$user__username', ]
|
||||
filterset_fields = ['user', 'wei', ]
|
||||
|
||||
|
||||
class WEIMembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/wei/membership/
|
||||
"""
|
||||
queryset = WEIMembership.objects
|
||||
serializer_class = WEIMembershipSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
search_fields = ['$user__username', '$bus__name', '$team__name', ]
|
||||
filterset_fields = ['user', 'club', 'bus', 'team', ]
|
10
apps/wei/apps.py
Normal file
10
apps/wei/apps.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class WeiConfig(AppConfig):
|
||||
name = 'wei'
|
||||
verbose_name = _('WEI')
|
10
apps/wei/forms/__init__.py
Normal file
10
apps/wei/forms/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .registration import WEIForm, WEIRegistrationForm, WEIMembershipForm, BusForm, BusTeamForm
|
||||
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
|
||||
|
||||
__all__ = [
|
||||
'WEIForm', 'WEIRegistrationForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
124
apps/wei/forms/registration.py
Normal file
124
apps/wei/forms/registration.py
Normal file
@ -0,0 +1,124 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
||||
|
||||
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
||||
|
||||
|
||||
class WEIForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WEIClub
|
||||
exclude = ('parent_club', 'require_memberships', 'membership_duration', )
|
||||
widgets = {
|
||||
"membership_fee_paid": AmountInput(),
|
||||
"membership_fee_unpaid": AmountInput(),
|
||||
"membership_start": DatePickerInput(),
|
||||
"membership_end": DatePickerInput(),
|
||||
"date_start": DatePickerInput(),
|
||||
"date_end": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class WEIRegistrationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WEIRegistration
|
||||
exclude = ('wei', )
|
||||
widgets = {
|
||||
"user": Autocomplete(
|
||||
User,
|
||||
attrs={
|
||||
'api_url': '/api/user/',
|
||||
'name_field': 'username',
|
||||
'placeholder': 'Nom ...',
|
||||
},
|
||||
),
|
||||
"birth_date": DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class WEIChooseBusForm(forms.Form):
|
||||
bus = forms.ModelMultipleChoiceField(
|
||||
queryset=Bus.objects,
|
||||
label=_("bus"),
|
||||
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
|
||||
+ " in particular if you are a free eletron."),
|
||||
)
|
||||
|
||||
team = forms.ModelMultipleChoiceField(
|
||||
queryset=BusTeam.objects,
|
||||
label=_("Team"),
|
||||
required=False,
|
||||
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
|
||||
)
|
||||
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=WEIRole.objects.filter(~Q(name="1A")),
|
||||
label=_("WEI Roles"),
|
||||
help_text=_("Select the roles that you are interested in."),
|
||||
)
|
||||
|
||||
|
||||
class WEIMembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects, label=_("WEI Roles"))
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]:
|
||||
self.add_error('bus', _("This team doesn't belong to the given bus."))
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = WEIMembership
|
||||
fields = ('roles', 'bus', 'team',)
|
||||
widgets = {
|
||||
"bus": Autocomplete(
|
||||
Bus,
|
||||
attrs={
|
||||
'api_url': '/api/wei/bus/',
|
||||
'placeholder': 'Bus ...',
|
||||
}
|
||||
),
|
||||
"team": Autocomplete(
|
||||
BusTeam,
|
||||
attrs={
|
||||
'api_url': '/api/wei/team/',
|
||||
'placeholder': 'Équipe ...',
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bus
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
"wei": Autocomplete(
|
||||
WEIClub,
|
||||
attrs={
|
||||
'api_url': '/api/wei/club/',
|
||||
'placeholder': 'WEI ...',
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BusTeamForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BusTeam
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
"bus": Autocomplete(
|
||||
Bus,
|
||||
attrs={
|
||||
'api_url': '/api/wei/bus/',
|
||||
'placeholder': 'Bus ...',
|
||||
},
|
||||
),
|
||||
"color": ColorWidget(),
|
||||
}
|
12
apps/wei/forms/surveys/__init__.py
Normal file
12
apps/wei/forms/surveys/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from .wei2020 import WEISurvey2020
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
||||
|
||||
CurrentSurvey = WEISurvey2020
|
189
apps/wei/forms/surveys/base.py
Normal file
189
apps/wei/forms/surveys/base.py
Normal file
@ -0,0 +1,189 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Form
|
||||
|
||||
from ...models import WEIClub, WEIRegistration, Bus
|
||||
|
||||
|
||||
class WEISurveyInformation:
|
||||
"""
|
||||
Abstract data of the survey.
|
||||
"""
|
||||
valid = False
|
||||
selected_bus_pk = None
|
||||
selected_bus_name = None
|
||||
|
||||
def __init__(self, registration):
|
||||
self.__dict__.update(registration.information)
|
||||
|
||||
def get_selected_bus(self) -> Optional[Bus]:
|
||||
"""
|
||||
If the algorithm ran, return the prefered bus according to the survey.
|
||||
In the other case, return None.
|
||||
"""
|
||||
if not self.valid:
|
||||
return None
|
||||
return Bus.objects.get(pk=self.selected_bus_pk)
|
||||
|
||||
def save(self, registration) -> None:
|
||||
"""
|
||||
Store the data of the survey into the database, with the information of the registration.
|
||||
"""
|
||||
registration.information = self.__dict__
|
||||
|
||||
|
||||
class WEIBusInformation:
|
||||
"""
|
||||
Abstract data of the bus.
|
||||
"""
|
||||
|
||||
def __init__(self, bus: Bus):
|
||||
self.__dict__.update(bus.information)
|
||||
self.bus = bus
|
||||
|
||||
|
||||
class WEISurveyAlgorithm:
|
||||
"""
|
||||
Abstract algorithm that attributes a bus to each new member.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_survey_class(cls):
|
||||
"""
|
||||
The class of the survey associated with this algorithm.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_bus_information_class(cls):
|
||||
"""
|
||||
The class of the information associated to a bus extending WEIBusInformation.
|
||||
Default: WEIBusInformation (contains nothing)
|
||||
"""
|
||||
return WEIBusInformation
|
||||
|
||||
@classmethod
|
||||
def get_registrations(cls) -> QuerySet:
|
||||
"""
|
||||
Queryset of all first year registrations
|
||||
"""
|
||||
return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True)
|
||||
|
||||
@classmethod
|
||||
def get_buses(cls) -> QuerySet:
|
||||
"""
|
||||
Queryset of all buses of the associated wei.
|
||||
"""
|
||||
return Bus.objects.filter(wei__year=cls.get_survey_class().get_year())
|
||||
|
||||
@classmethod
|
||||
def get_bus_information(cls, bus):
|
||||
"""
|
||||
Return the WEIBusInformation object containing the data stored in a given bus.
|
||||
"""
|
||||
return cls.get_bus_information_class()(bus)
|
||||
|
||||
def run_algorithm(self) -> None:
|
||||
"""
|
||||
Once this method implemented, run the algorithm that attributes a bus to each first year member.
|
||||
This method can be run in command line through ``python manage.py wei_algorithm``
|
||||
See ``wei.management.commmands.wei_algorithm``
|
||||
This method must call Survey.select_bus for each survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WEISurvey:
|
||||
"""
|
||||
Survey associated to a first year WEI registration.
|
||||
The data is stored into WEISurveyInformation, this class acts as a manager.
|
||||
This is an abstract class: this has to be extended each year to implement custom methods.
|
||||
"""
|
||||
|
||||
def __init__(self, registration: WEIRegistration):
|
||||
self.registration = registration
|
||||
self.information = self.get_survey_information_class()(registration)
|
||||
|
||||
@classmethod
|
||||
def get_year(cls) -> int:
|
||||
"""
|
||||
Get year of the wei concerned by the type of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_wei(cls) -> WEIClub:
|
||||
"""
|
||||
The WEI associated to this kind of survey.
|
||||
"""
|
||||
return WEIClub.objects.get(year=cls.get_year())
|
||||
|
||||
@classmethod
|
||||
def get_survey_information_class(cls):
|
||||
"""
|
||||
The class of the data (extending WEISurveyInformation).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
"""
|
||||
The form class of the survey.
|
||||
This is proper to the status of the survey: the form class can evolve according to the progress of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_form(self, form) -> None:
|
||||
"""
|
||||
Once the form is instanciated, the information can be updated with the information of the registration
|
||||
and the information of the survey.
|
||||
This method is called once the form is created.
|
||||
"""
|
||||
pass
|
||||
|
||||
def form_valid(self, form) -> None:
|
||||
"""
|
||||
Called when the information of the form are validated.
|
||||
This method should update the information of the survey.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""
|
||||
Return True if the survey is complete.
|
||||
If the survey is complete, then the button "Next" will display some text for the end of the survey.
|
||||
If not, the survey is reloaded and continues.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Store the information of the survey into the database.
|
||||
"""
|
||||
self.information.save(self.registration)
|
||||
# The information is forced-saved.
|
||||
# We don't want that anyone can update manually the information, so since most users don't have the
|
||||
# right to save the information of a registration, we force save.
|
||||
self.registration._force_save = True
|
||||
self.registration.save()
|
||||
|
||||
@classmethod
|
||||
def get_algorithm_class(cls):
|
||||
"""
|
||||
Algorithm class associated to the survey.
|
||||
The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member.
|
||||
The association is not permanent: that's only a suggestion.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def select_bus(self, bus) -> None:
|
||||
"""
|
||||
Set the suggestion into the data of the membership.
|
||||
:param bus: The bus suggested.
|
||||
"""
|
||||
self.information.selected_bus_pk = bus.pk
|
||||
self.information.selected_bus_name = bus.name
|
||||
self.information.valid = True
|
89
apps/wei/forms/surveys/wei2020.py
Normal file
89
apps/wei/forms/surveys/wei2020.py
Normal file
@ -0,0 +1,89 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import forms
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from ...models import Bus
|
||||
|
||||
|
||||
class WEISurveyForm2020(forms.Form):
|
||||
"""
|
||||
Survey form for the year 2020.
|
||||
For now, that's only a Bus selector.
|
||||
TODO: Do a better survey (later)
|
||||
"""
|
||||
bus = forms.ModelChoiceField(
|
||||
Bus.objects,
|
||||
)
|
||||
|
||||
def set_registration(self, registration):
|
||||
"""
|
||||
Filter the bus selector with the buses of the current WEI.
|
||||
"""
|
||||
self.fields["bus"].queryset = Bus.objects.filter(wei=registration.wei)
|
||||
|
||||
|
||||
class WEISurveyInformation2020(WEISurveyInformation):
|
||||
"""
|
||||
We store the id of the selected bus. We store only the name, but is not used in the selection:
|
||||
that's only for humans that try to read data.
|
||||
"""
|
||||
chosen_bus_pk = None
|
||||
chosen_bus_name = None
|
||||
|
||||
|
||||
class WEISurvey2020(WEISurvey):
|
||||
"""
|
||||
Survey for the year 2020.
|
||||
"""
|
||||
@classmethod
|
||||
def get_year(cls):
|
||||
return 2020
|
||||
|
||||
@classmethod
|
||||
def get_survey_information_class(cls):
|
||||
return WEISurveyInformation2020
|
||||
|
||||
def get_form_class(self):
|
||||
return WEISurveyForm2020
|
||||
|
||||
def update_form(self, form):
|
||||
"""
|
||||
Filter the bus selector with the buses of the WEI.
|
||||
"""
|
||||
form.set_registration(self.registration)
|
||||
|
||||
def form_valid(self, form):
|
||||
bus = form.cleaned_data["bus"]
|
||||
self.information.chosen_bus_pk = bus.pk
|
||||
self.information.chosen_bus_name = bus.name
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def get_algorithm_class(cls):
|
||||
return WEISurveyAlgorithm2020
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""
|
||||
The survey is complete once the bus is chosen.
|
||||
"""
|
||||
return self.information.chosen_bus_pk is not None
|
||||
|
||||
|
||||
class WEISurveyAlgorithm2020(WEISurveyAlgorithm):
|
||||
"""
|
||||
The algorithm class for the year 2020.
|
||||
For now, the algorithm is quite simple: the selected bus is the chosen bus.
|
||||
TODO: Improve this algorithm.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_survey_class(cls):
|
||||
return WEISurvey2020
|
||||
|
||||
def run_algorithm(self):
|
||||
for registration in self.get_registrations():
|
||||
survey = self.get_survey_class()(registration)
|
||||
survey.select_bus(Bus.objects.get(pk=survey.information.chosen_bus_pk))
|
||||
survey.save()
|
0
apps/wei/migrations/__init__.py
Normal file
0
apps/wei/migrations/__init__.py
Normal file
329
apps/wei/models.py
Normal file
329
apps/wei/models.py
Normal file
@ -0,0 +1,329 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Role, Club, Membership
|
||||
from note.models import MembershipTransaction
|
||||
|
||||
|
||||
class WEIClub(Club):
|
||||
"""
|
||||
The WEI is a club. Register to the WEI is equivalent than be member of the club.
|
||||
"""
|
||||
year = models.PositiveIntegerField(
|
||||
unique=True,
|
||||
default=date.today().year,
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
date_start = models.DateField(
|
||||
verbose_name=_("date start"),
|
||||
)
|
||||
|
||||
date_end = models.DateField(
|
||||
verbose_name=_("date end"),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_current_wei(self):
|
||||
"""
|
||||
We consider that this is the current WEI iff there is no future WEI planned.
|
||||
"""
|
||||
return not WEIClub.objects.filter(date_start__gt=self.date_start).exists()
|
||||
|
||||
def update_membership_dates(self):
|
||||
"""
|
||||
We can't join the WEI next years.
|
||||
"""
|
||||
return
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI")
|
||||
verbose_name_plural = _("WEI")
|
||||
|
||||
|
||||
class Bus(models.Model):
|
||||
"""
|
||||
The best bus for the best WEI
|
||||
"""
|
||||
wei = models.ForeignKey(
|
||||
WEIClub,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="buses",
|
||||
verbose_name=_("WEI"),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
information_json = models.TextField(
|
||||
default="{}",
|
||||
verbose_name=_("survey information"),
|
||||
help_text=_("Information about the survey for new members, encoded in JSON"),
|
||||
)
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information about the survey for new members are stored in a dictionary that can evolve following the years.
|
||||
The dictionary is stored as a JSON string.
|
||||
"""
|
||||
return json.loads(self.information_json)
|
||||
|
||||
@information.setter
|
||||
def information(self, information):
|
||||
"""
|
||||
Store information as a JSON string
|
||||
"""
|
||||
self.information_json = json.dumps(information)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Bus")
|
||||
verbose_name_plural = _("Buses")
|
||||
unique_together = ('wei', 'name',)
|
||||
|
||||
|
||||
class BusTeam(models.Model):
|
||||
"""
|
||||
A bus has multiple teams
|
||||
"""
|
||||
bus = models.ForeignKey(
|
||||
Bus,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="teams",
|
||||
verbose_name=_("bus"),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
||||
verbose_name=_("color"),
|
||||
help_text=_("The color of the T-Shirt, stored with its number equivalent"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name + " (" + str(self.bus) + ")"
|
||||
|
||||
class Meta:
|
||||
unique_together = ('bus', 'name',)
|
||||
verbose_name = _("Bus team")
|
||||
verbose_name_plural = _("Bus teams")
|
||||
|
||||
|
||||
class WEIRole(Role):
|
||||
"""
|
||||
A Role for the WEI can be bus chief, team chief, free electron, ...
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI Role")
|
||||
verbose_name_plural = _("WEI Roles")
|
||||
|
||||
|
||||
class WEIRegistration(models.Model):
|
||||
"""
|
||||
Store personal data that can be useful for the WEI.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="wei",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
wei = models.ForeignKey(
|
||||
WEIClub,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="users",
|
||||
verbose_name=_("WEI"),
|
||||
)
|
||||
|
||||
soge_credit = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Credit from Société générale"),
|
||||
)
|
||||
|
||||
caution_check = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Caution check given")
|
||||
)
|
||||
|
||||
birth_date = models.DateField(
|
||||
verbose_name=_("birth date"),
|
||||
)
|
||||
|
||||
gender = models.CharField(
|
||||
max_length=16,
|
||||
choices=(
|
||||
('male', _("Male")),
|
||||
('female', _("Female")),
|
||||
('nonbinary', _("Non binary")),
|
||||
),
|
||||
verbose_name=_("gender"),
|
||||
)
|
||||
|
||||
health_issues = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("health issues"),
|
||||
)
|
||||
|
||||
emergency_contact_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("emergency contact name"),
|
||||
)
|
||||
|
||||
emergency_contact_phone = models.CharField(
|
||||
max_length=32,
|
||||
verbose_name=_("emergency contact phone"),
|
||||
)
|
||||
|
||||
ml_events_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
ml_sport_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
ml_art_registration = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"),
|
||||
)
|
||||
|
||||
first_year = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("first year"),
|
||||
help_text=_("Tells if the user is new in the school.")
|
||||
)
|
||||
|
||||
information_json = models.TextField(
|
||||
default="{}",
|
||||
verbose_name=_("registration information"),
|
||||
help_text=_("Information about the registration (buses for old members, survey fot the new members), "
|
||||
"encoded in JSON"),
|
||||
)
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information about the registration (the survey for the new members, the bus for the older members, ...)
|
||||
are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string.
|
||||
"""
|
||||
return json.loads(self.information_json)
|
||||
|
||||
@information.setter
|
||||
def information(self, information):
|
||||
"""
|
||||
Store information as a JSON string
|
||||
"""
|
||||
self.information_json = json.dumps(information)
|
||||
|
||||
@property
|
||||
def is_validated(self):
|
||||
try:
|
||||
return self.membership is not None
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'wei',)
|
||||
verbose_name = _("WEI User")
|
||||
verbose_name_plural = _("WEI Users")
|
||||
|
||||
|
||||
class WEIMembership(Membership):
|
||||
bus = models.ForeignKey(
|
||||
Bus,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("bus"),
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
BusTeam,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="memberships",
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
registration = models.OneToOneField(
|
||||
WEIRegistration,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
related_name="membership",
|
||||
verbose_name=_("WEI registration"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI membership")
|
||||
verbose_name_plural = _("WEI memberships")
|
||||
|
||||
def make_transaction(self):
|
||||
"""
|
||||
Create Membership transaction associated to this membership.
|
||||
"""
|
||||
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
|
||||
return
|
||||
|
||||
if self.fee:
|
||||
transaction = MembershipTransaction(
|
||||
membership=self,
|
||||
source=self.user.note,
|
||||
destination=self.club.note,
|
||||
quantity=1,
|
||||
amount=self.fee,
|
||||
reason="Adhésion WEI " + self.club.name,
|
||||
valid=not self.registration.soge_credit # Soge transactions are by default invalidated
|
||||
)
|
||||
transaction._force_save = True
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS:
|
||||
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
|
||||
# to treasurers.
|
||||
transaction.refresh_from_db()
|
||||
from treasury.models import SogeCredit
|
||||
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
|
||||
soge_credit.refresh_from_db()
|
||||
transaction.save()
|
||||
soge_credit.transactions.add(transaction)
|
||||
soge_credit.save()
|
201
apps/wei/tables.py
Normal file
201
apps/wei/tables.py
Normal file
@ -0,0 +1,201 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2 import A
|
||||
|
||||
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
|
||||
|
||||
|
||||
class WEITable(tables.Table):
|
||||
"""
|
||||
List all WEI.
|
||||
"""
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIClub
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'year', 'date_start', 'date_end',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: reverse_lazy('wei:wei_detail', args=(record.pk,))
|
||||
}
|
||||
|
||||
|
||||
class WEIRegistrationTable(tables.Table):
|
||||
"""
|
||||
List all WEI registrations.
|
||||
"""
|
||||
user = tables.LinkColumn(
|
||||
'member:user_detail',
|
||||
args=[A('user.pk')],
|
||||
)
|
||||
|
||||
edit = tables.LinkColumn(
|
||||
'wei:wei_update_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("Edit"),
|
||||
text=_("Edit"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-warning'
|
||||
}
|
||||
}
|
||||
)
|
||||
validate = tables.LinkColumn(
|
||||
'wei:validate_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("Validate"),
|
||||
text=_("Validate"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-success'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
delete = tables.LinkColumn(
|
||||
'wei:wei_delete_registration',
|
||||
args=[A('pk')],
|
||||
verbose_name=_("delete"),
|
||||
text=_("Delete"),
|
||||
attrs={
|
||||
'a': {
|
||||
'class': 'btn btn-danger'
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIRegistration
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'user.first_name', 'user.last_name', 'first_year',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
|
||||
class WEIMembershipTable(tables.Table):
|
||||
user = tables.LinkColumn(
|
||||
'wei:wei_update_registration',
|
||||
args=[A('registration.pk')],
|
||||
)
|
||||
|
||||
year = tables.Column(
|
||||
accessor=A("pk"),
|
||||
verbose_name=_("Year"),
|
||||
)
|
||||
|
||||
bus = tables.LinkColumn(
|
||||
'wei:manage_bus',
|
||||
args=[A('bus.pk')],
|
||||
)
|
||||
|
||||
team = tables.LinkColumn(
|
||||
'wei:manage_bus_team',
|
||||
args=[A('bus.pk')],
|
||||
)
|
||||
|
||||
def render_year(self, record):
|
||||
return str(record.user.profile.ens_year) + "A"
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = WEIMembership
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'user.last_name', 'user.first_name', 'registration.gender', 'user.profile.department',
|
||||
'year', 'bus', 'team', )
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
}
|
||||
|
||||
|
||||
class BusTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'wei:manage_bus',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
teams = tables.Column(
|
||||
accessor=A("teams"),
|
||||
verbose_name=_("Teams"),
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-truncate",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
count = tables.Column(
|
||||
verbose_name=_("Members count"),
|
||||
)
|
||||
|
||||
def render_teams(self, value):
|
||||
return ", ".join(team.name for team in value.all())
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = Bus
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'teams', )
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
}
|
||||
|
||||
|
||||
class BusTeamTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'wei:manage_bus_team',
|
||||
args=[A('pk')],
|
||||
)
|
||||
|
||||
color = tables.Column(
|
||||
attrs={
|
||||
"td": {
|
||||
"style": lambda record: "background-color: #{:06X}; color: #{:06X};"
|
||||
.format(record.color, 0xFFFFFF - record.color, )
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
|
||||
count = tables.Column(
|
||||
verbose_name=_("Members count"),
|
||||
)
|
||||
|
||||
def render_color(self, value):
|
||||
return "#{:06X}".format(value)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = BusTeam
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'color',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, ))
|
||||
}
|
43
apps/wei/urls.py
Normal file
43
apps/wei/urls.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\
|
||||
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView,\
|
||||
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\
|
||||
WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIDeleteRegistrationView,\
|
||||
WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
|
||||
|
||||
|
||||
app_name = 'wei'
|
||||
urlpatterns = [
|
||||
path('detail/', CurrentWEIDetailView.as_view(), name="current_wei_detail"),
|
||||
path('list/', WEIListView.as_view(), name="wei_list"),
|
||||
path('create/', WEICreateView.as_view(), name="wei_create"),
|
||||
path('detail/<int:pk>/', WEIDetailView.as_view(), name="wei_detail"),
|
||||
path('update/<int:pk>/', WEIUpdateView.as_view(), name="wei_update"),
|
||||
path('detail/<int:pk>/registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"),
|
||||
path('detail/<int:pk>/memberships/', WEIMembershipsView.as_view(), name="wei_memberships"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/', MemberListRenderView.as_view(),
|
||||
name="wei_memberships_bus_pdf"),
|
||||
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
|
||||
name="wei_memberships_team_pdf"),
|
||||
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
|
||||
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
|
||||
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
|
||||
path('add-bus-team/<int:pk>/', BusTeamCreateView.as_view(), name="add_team"),
|
||||
path('manage-bus-team/<int:pk>/', BusTeamManageView.as_view(), name="manage_bus_team"),
|
||||
path('update-bus-team/<int:pk>/', BusTeamUpdateView.as_view(), name="update_bus_team"),
|
||||
path('register/<int:wei_pk>/1A/', WEIRegister1AView.as_view(), name="wei_register_1A"),
|
||||
path('register/<int:wei_pk>/2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"),
|
||||
path('register/<int:wei_pk>/1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"),
|
||||
path('register/<int:wei_pk>/2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"),
|
||||
path('edit-registration/<int:pk>/', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"),
|
||||
path('delete-registration/<int:pk>/', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"),
|
||||
path('validate/<int:pk>/', WEIValidateRegistrationView.as_view(), name="validate_registration"),
|
||||
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
|
||||
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
|
||||
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
|
||||
]
|
946
apps/wei/views.py
Normal file
946
apps/wei/views.py
Normal file
@ -0,0 +1,946 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime, date
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models.functions.text import Lower
|
||||
from django.forms import HiddenInput
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import BaseFormView, DeleteView
|
||||
from django_tables2 import SingleTableView
|
||||
from member.models import Membership, Club
|
||||
from note.models import Transaction, NoteClub, Alias
|
||||
from note.tables import HistoryTable
|
||||
from note_kfet.settings import BASE_DIR
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms.registration import WEIChooseBusForm
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
|
||||
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembershipForm, CurrentSurvey
|
||||
from .tables import WEITable, WEIRegistrationTable, BusTable, BusTeamTable, WEIMembershipTable
|
||||
|
||||
|
||||
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
wei = WEIClub.objects.filter(membership_start__lte=date.today()).order_by('date_start')
|
||||
if wei.exists():
|
||||
wei = wei.last()
|
||||
return reverse_lazy('wei:wei_detail', args=(wei.pk,))
|
||||
else:
|
||||
return reverse_lazy('wei:wei_list')
|
||||
|
||||
|
||||
class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List existing WEI
|
||||
"""
|
||||
model = WEIClub
|
||||
table_class = WEITable
|
||||
ordering = '-year'
|
||||
|
||||
|
||||
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create WEI
|
||||
"""
|
||||
model = WEIClub
|
||||
form_class = WEIForm
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.requires_membership = True
|
||||
form.instance.parent_club = Club.objects.get(name="Kfet")
|
||||
ret = super().form_valid(form)
|
||||
NoteClub.objects.create(club=form.instance)
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View WEI information
|
||||
"""
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
club = context["club"]
|
||||
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \
|
||||
.order_by('-created_at', '-id')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
context['history_list'] = history_table
|
||||
|
||||
club_member = WEIMembership.objects.filter(
|
||||
club=club,
|
||||
date_end__gte=datetime.today(),
|
||||
).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
||||
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
|
||||
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||
context['member_list'] = membership_table
|
||||
|
||||
pre_registrations = WEIRegistration.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter(
|
||||
membership=None,
|
||||
wei=club
|
||||
)
|
||||
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
|
||||
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||
context['pre_registrations'] = pre_registrations_table
|
||||
|
||||
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
||||
if my_registration.exists():
|
||||
my_registration = my_registration.get()
|
||||
else:
|
||||
my_registration = None
|
||||
context["my_registration"] = my_registration
|
||||
|
||||
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
|
||||
.filter(wei=self.object).annotate(count=Count("memberships"))
|
||||
bus_table = BusTable(data=buses, prefix="bus-")
|
||||
context['buses'] = bus_table
|
||||
|
||||
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
|
||||
|
||||
if random_user is None:
|
||||
# This case occurs when all users are registered to the WEI.
|
||||
# Don't worry, Pikachu never went to the WEI.
|
||||
# This bug can arrive only in dev mode.
|
||||
context["can_add_first_year_member"] = True
|
||||
context["can_add_any_member"] = True
|
||||
else:
|
||||
# Check if the user has the right to create a registration of a random first year member.
|
||||
empty_fy_registration = WEIRegistration(
|
||||
user=random_user,
|
||||
first_year=True,
|
||||
birth_date="1970-01-01",
|
||||
gender="No",
|
||||
emergency_contact_name="No",
|
||||
emergency_contact_phone="No",
|
||||
)
|
||||
context["can_add_first_year_member"] = PermissionBackend \
|
||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration)
|
||||
|
||||
# Check if the user has the right to create a registration of a random old member.
|
||||
empty_old_registration = WEIRegistration(
|
||||
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
|
||||
first_year=False,
|
||||
birth_date="1970-01-01",
|
||||
gender="No",
|
||||
emergency_contact_name="No",
|
||||
emergency_contact_phone="No",
|
||||
)
|
||||
context["can_add_any_member"] = PermissionBackend \
|
||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration)
|
||||
|
||||
empty_bus = Bus(
|
||||
wei=club,
|
||||
name="",
|
||||
)
|
||||
context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus)
|
||||
|
||||
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List all WEI memberships
|
||||
"""
|
||||
model = WEIMembership
|
||||
table_class = WEIMembershipTable
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(club=self.club)
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
| Q(bus__name__iregex=pattern)
|
||||
| Q(team__name__iregex=pattern)
|
||||
)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.club
|
||||
context["title"] = _("Find WEI Membership")
|
||||
return context
|
||||
|
||||
|
||||
class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List all non-validated WEI registrations.
|
||||
"""
|
||||
model = WEIRegistration
|
||||
table_class = WEIRegistrationTable
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
)
|
||||
|
||||
return qs[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.club
|
||||
context["title"] = _("Find WEI Registration")
|
||||
return context
|
||||
|
||||
|
||||
class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update the information of the WEI.
|
||||
"""
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
form_class = WEIForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object()
|
||||
today = date.today()
|
||||
# We can't update a past WEI
|
||||
# But we can update it while it is not officially opened
|
||||
if today > wei.membership_end:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create Bus
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
today = date.today()
|
||||
# We can't add a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["wei"].initial = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update Bus
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().wei
|
||||
today = date.today()
|
||||
# We can't update a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["wei"].disabled = True
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Manage Bus
|
||||
"""
|
||||
model = Bus
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
|
||||
bus = self.object
|
||||
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
||||
.filter(bus=bus).annotate(count=Count("memberships"))
|
||||
teams_table = BusTeamTable(data=teams, prefix="team-")
|
||||
context["teams"] = teams_table
|
||||
|
||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request.user, WEIMembership, "view")).filter(bus=bus)
|
||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||
context["memberships"] = memberships_table
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create BusTeam
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
|
||||
today = date.today()
|
||||
# We can't add a team once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
bus = Bus.objects.get(pk=self.kwargs["pk"])
|
||||
context["club"] = bus.wei
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["bus"].initial = Bus.objects.get(pk=self.kwargs["pk"])
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.pk})
|
||||
|
||||
|
||||
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update Bus team
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().bus.wei
|
||||
today = date.today()
|
||||
# We can't update a bus once the WEI is started
|
||||
if today >= wei.date_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.bus.wei
|
||||
context["bus"] = self.object.bus
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["bus"].disabled = True
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Manage Bus team
|
||||
"""
|
||||
model = BusTeam
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["bus"] = self.object.bus
|
||||
context["club"] = self.object.bus.wei
|
||||
|
||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request.user, WEIMembership, "view")).filter(team=self.object)
|
||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||
context["memberships"] = memberships_table
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register a new user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
today = date.today()
|
||||
# We can't register someone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Register 1A")
|
||||
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
if "myself" in self.request.path:
|
||||
context["form"].fields["user"].disabled = True
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].initial = self.request.user
|
||||
del form.fields["first_year"]
|
||||
del form.fields["caution_check"]
|
||||
del form.fields["information_json"]
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
form.instance.first_year = True
|
||||
|
||||
if not form.instance.pk:
|
||||
# Check if the user is not already registered to the WEI
|
||||
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user is already registered to this WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Check if the user can be in her/his first year (yeah, no cheat)
|
||||
if WEIRegistration.objects.filter(user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user can't be in her/his first year since he/she has already"
|
||||
" participed to a WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register an old user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
today = date.today()
|
||||
# We can't register someone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _("Register 2A+")
|
||||
context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
|
||||
if "myself" in self.request.path:
|
||||
context["form"].fields["user"].disabled = True
|
||||
|
||||
choose_bus_form = WEIChooseBusForm()
|
||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
||||
context['membership_form'] = choose_bus_form
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].initial = self.request.user
|
||||
if "myself" in self.request.path and self.request.user.profile.soge:
|
||||
form.fields["soge_credit"].disabled = True
|
||||
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
|
||||
|
||||
del form.fields["caution_check"]
|
||||
del form.fields["first_year"]
|
||||
del form.fields["ml_events_registration"]
|
||||
del form.fields["ml_art_registration"]
|
||||
del form.fields["ml_sport_registration"]
|
||||
del form.fields["information_json"]
|
||||
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
form.instance.first_year = False
|
||||
|
||||
if not form.instance.pk:
|
||||
# Check if the user is not already registered to the WEI
|
||||
if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists():
|
||||
form.add_error('user', _("This user is already registered to this WEI."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
||||
if not choose_bus_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
information = form.instance.information
|
||||
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
|
||||
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
form.instance.information = information
|
||||
form.instance.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update a registration for the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return WEIRegistration.objects
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().wei
|
||||
today = date.today()
|
||||
# We can't update a registration once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
|
||||
if self.object.is_validated:
|
||||
membership_form = WEIMembershipForm(instance=self.object.membership)
|
||||
for field_name, field in membership_form.fields.items():
|
||||
if not PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
|
||||
field.widget = HiddenInput()
|
||||
context["membership_form"] = membership_form
|
||||
elif not self.object.first_year and PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
||||
choose_bus_form = WEIChooseBusForm(
|
||||
dict(
|
||||
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
||||
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
|
||||
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
|
||||
)
|
||||
)
|
||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
||||
context["membership_form"] = choose_bus_form
|
||||
|
||||
if not self.object.soge_credit and self.object.user.profile.soge:
|
||||
form = context["form"]
|
||||
form.fields["soge_credit"].disabled = True
|
||||
form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.")
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].disabled = True
|
||||
if not self.object.first_year:
|
||||
del form.fields["information_json"]
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
# If the membership is already validated, then we update the bus and the team (and the roles)
|
||||
if form.instance.is_validated:
|
||||
membership_form = WEIMembershipForm(self.request.POST, instance=form.instance.membership)
|
||||
if not membership_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
membership_form.save()
|
||||
# If it is not validated and if this is an old member, then we update the choices
|
||||
elif not form.instance.first_year and PermissionBackend.check_perm(
|
||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
||||
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
||||
if not choose_bus_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
information = form.instance.information
|
||||
information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]]
|
||||
information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]]
|
||||
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
|
||||
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
form.instance.information = information
|
||||
form.instance.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
if self.object.first_year:
|
||||
survey = CurrentSurvey(self.object)
|
||||
if not survey.is_complete():
|
||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
|
||||
|
||||
|
||||
class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||
"""
|
||||
Delete a non-validated WEI registration
|
||||
"""
|
||||
model = WEIRegistration
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
wei = object.wei
|
||||
today = date.today()
|
||||
# We can't delete a registration of a past WEI
|
||||
if today > wei.membership_end:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
|
||||
if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object):
|
||||
raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,))
|
||||
|
||||
|
||||
class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Validate WEI Registration
|
||||
"""
|
||||
model = WEIMembership
|
||||
form_class = WEIMembershipForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
today = date.today()
|
||||
# We can't validate anyone once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
context["registration"] = registration
|
||||
survey = CurrentSurvey(registration)
|
||||
if survey.information.valid:
|
||||
context["suggested_bus"] = survey.information.get_selected_bus()
|
||||
context["club"] = registration.wei
|
||||
context["fee"] = registration.wei.membership_fee_paid if registration.user.profile.paid \
|
||||
else registration.wei.membership_fee_unpaid
|
||||
context["kfet_member"] = Membership.objects.filter(
|
||||
club__name="Kfet",
|
||||
user=registration.user,
|
||||
date_start__lte=datetime.now().date(),
|
||||
date_end__gte=datetime.now().date(),
|
||||
).exists()
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
|
||||
if registration.first_year:
|
||||
# Use the results of the survey to fill initial data
|
||||
# A first year has no other role than "1A"
|
||||
del form.fields["roles"]
|
||||
survey = CurrentSurvey(registration)
|
||||
if survey.information.valid:
|
||||
form.fields["bus"].initial = survey.information.get_selected_bus()
|
||||
else:
|
||||
# Use the choice of the member to fill initial data
|
||||
information = registration.information
|
||||
if "preferred_bus_pk" in information and len(information["preferred_bus_pk"]) == 1:
|
||||
form["bus"].initial = Bus.objects.get(pk=information["preferred_bus_pk"][0])
|
||||
if "preferred_team_pk" in information and len(information["preferred_team_pk"]) == 1:
|
||||
form["team"].initial = Bus.objects.get(pk=information["preferred_team_pk"][0])
|
||||
if "preferred_roles_pk" in information:
|
||||
form["roles"].initial = WEIRole.objects.filter(
|
||||
Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI")
|
||||
).all()
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Create membership, check that all is good, make transactions
|
||||
"""
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
club = registration.wei
|
||||
user = registration.user
|
||||
|
||||
membership = form.instance
|
||||
membership.user = user
|
||||
membership.club = club
|
||||
membership.date_start = min(date.today(), club.date_start)
|
||||
membership.registration = registration
|
||||
|
||||
if user.profile.paid:
|
||||
fee = club.membership_fee_paid
|
||||
else:
|
||||
fee = club.membership_fee_unpaid
|
||||
|
||||
if not registration.soge_credit and user.note.balance < fee:
|
||||
# Users must have money before registering to the WEI.
|
||||
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
|
||||
form.add_error('bus',
|
||||
_("This user don't have enough money to join this club, and can't have a negative balance."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if not registration.caution_check and not registration.first_year:
|
||||
form.add_error('bus', _("This user didn't give her/his caution check."))
|
||||
return super().form_invalid(form)
|
||||
|
||||
if club.parent_club is not None:
|
||||
if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
|
||||
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
||||
return super().form_invalid(form)
|
||||
|
||||
# Now, all is fine, the membership can be created.
|
||||
|
||||
if registration.first_year:
|
||||
membership = form.instance
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
membership.roles.set(WEIRole.objects.filter(name="1A").all())
|
||||
membership.save()
|
||||
|
||||
ret = super().form_valid(form)
|
||||
|
||||
membership.refresh_from_db()
|
||||
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
|
||||
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk})
|
||||
|
||||
|
||||
class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||
"""
|
||||
Display the survey for the WEI for first year members.
|
||||
Warning: this page is accessible for anyone that is connected, the view doesn't extend ProtectQuerySetMixin.
|
||||
"""
|
||||
model = WEIRegistration
|
||||
template_name = "wei/survey.html"
|
||||
survey = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
|
||||
wei = obj.wei
|
||||
today = date.today()
|
||||
# We can't access to the WEI survey once the WEI is started and before the membership start date
|
||||
if today >= wei.date_start or today < wei.membership_start:
|
||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||
|
||||
if not self.survey:
|
||||
self.survey = CurrentSurvey(obj)
|
||||
# If the survey is complete, then display the end page.
|
||||
if self.survey.is_complete():
|
||||
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
|
||||
# Non first year members don't have a survey
|
||||
if not obj.first_year:
|
||||
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_class(self):
|
||||
"""
|
||||
Get the survey form. It may depend on the current state of the survey.
|
||||
"""
|
||||
return self.survey.get_form_class()
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""
|
||||
Update the form with the data of the survey.
|
||||
"""
|
||||
form = super().get_form(form_class)
|
||||
self.survey.update_form(form)
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Update the survey with the data of the form.
|
||||
"""
|
||||
self.survey.form_valid(form)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,))
|
||||
|
||||
|
||||
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_end.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
class WEIClosedView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_closed.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
class MemberListRenderView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Render Invoice as a generated PDF with the given information and a LaTeX template
|
||||
"""
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
||||
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
|
||||
Lower('bus__name'),
|
||||
Lower('team__name'),
|
||||
'user__profile__promotion',
|
||||
Lower('user__last_name'),
|
||||
Lower('user__first_name'),
|
||||
'id',
|
||||
)
|
||||
|
||||
if "bus_pk" in self.kwargs:
|
||||
qs = qs.filter(bus__pk=self.kwargs["bus_pk"])
|
||||
|
||||
if "team_pk" in self.kwargs:
|
||||
qs = qs.filter(team__pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
qs = self.get_queryset()
|
||||
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
bus = team = None
|
||||
if "bus_pk" in self.kwargs:
|
||||
bus = Bus.objects.get(pk=self.kwargs["bus_pk"])
|
||||
if "team_pk" in self.kwargs:
|
||||
team = BusTeam.objects.filter(pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None)
|
||||
if team.exists():
|
||||
team = team.get()
|
||||
bus = team.bus
|
||||
else:
|
||||
team = dict(name="Staff")
|
||||
|
||||
# Fill the template with the information
|
||||
tex = render_to_string("wei/weilist_sample.tex", dict(memberships=qs.all(), wei=wei, bus=bus, team=team))
|
||||
|
||||
try:
|
||||
os.mkdir(BASE_DIR + "/tmp")
|
||||
except FileExistsError:
|
||||
pass
|
||||
# We render the file in a temporary directory
|
||||
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
|
||||
|
||||
try:
|
||||
with open("{}/wei-list.tex".format(tmp_dir), "wb") as f:
|
||||
f.write(tex.encode("UTF-8"))
|
||||
del tex
|
||||
|
||||
error = subprocess.Popen(
|
||||
["pdflatex", "{}/wei-list.tex".format(tmp_dir)],
|
||||
cwd=tmp_dir,
|
||||
stdin=open(os.devnull, "r"),
|
||||
stderr=open(os.devnull, "wb"),
|
||||
stdout=open(os.devnull, "wb"),
|
||||
).wait()
|
||||
|
||||
if error:
|
||||
raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")")
|
||||
|
||||
# Display the generated pdf as a HTTP Response
|
||||
pdf = open("{}/wei-list.pdf".format(tmp_dir), 'rb').read()
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response['Content-Disposition'] = "inline;filename=Liste%20des%20participants%20au%20WEI.pdf"
|
||||
except IOError as e:
|
||||
raise e
|
||||
finally:
|
||||
# Delete all temporary files
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
return response
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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',)
|
||||
|
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
||||
'permission',
|
||||
'registration',
|
||||
'treasury',
|
||||
'wei',
|
||||
]
|
||||
LOGIN_REDIRECT_URL = '/note/transfer/'
|
||||
|
||||
|
@ -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')]
|
||||
|
@ -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/"
|
||||
|
@ -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
|
||||
|
@ -1,2 +1 @@
|
||||
django-cas-client==1.5.3
|
||||
django-cas-server==1.1.0
|
||||
|
12
static/colorfield/colorfield.js
Normal file
12
static/colorfield/colorfield.js
Normal 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));
|
||||
}
|
||||
};
|
1855
static/colorfield/jscolor/jscolor.js
Executable file
1855
static/colorfield/jscolor/jscolor.js
Executable file
File diff suppressed because it is too large
Load Diff
1
static/colorfield/jscolor/jscolor.min.js
vendored
Executable file
1
static/colorfield/jscolor/jscolor.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -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) {
|
||||
|
@ -335,14 +335,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
|
||||
})
|
||||
});
|
||||
|
||||
});// end getJSON alias
|
||||
});
|
||||
}// end function autocomplete
|
||||
|
||||
// When a validate button is clicked, we switch the validation status
|
||||
function de_validate(id, validated) {
|
||||
let invalidity_reason = $("#invalidity_reason_" + id).val();
|
||||
$("#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 insufficient rights, an error message will appear
|
||||
|
8
templates/400.html
Normal file
8
templates/400.html
Normal 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
13
templates/403.html
Normal 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
13
templates/404.html
Normal 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
8
templates/500.html
Normal 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 %}
|
@ -31,9 +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.7.0/css/font-awesome.min.css"
|
||||
crossorigin="anonymous">
|
||||
<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"
|
||||
@ -91,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>
|
||||
@ -131,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 %}
|
||||
|
@ -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">×</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 %}
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
|
||||
<cas:authenticationFailure code="{{code}}">{{msg}}</cas:authenticationFailure>
|
||||
</cas:serviceResponse>
|
@ -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 %}
|
8
templates/colorfield/color.html
Executable file
8
templates/colorfield/color.html
Executable 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 %} />
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
|
||||
|
||||
@ -12,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>
|
||||
|
52
templates/permission/all_rights.html
Normal file
52
templates/permission/all_rights.html
Normal 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 %}
|
@ -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." %}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
2
templates/scripts/mail-error500.html
Normal file
2
templates/scripts/mail-error500.html
Normal 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 }}
|
7
templates/scripts/mail-error500.txt
Normal file
7
templates/scripts/mail-error500.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Une erreur est survenue dans la Note Kfet. Les détails sont ci-dessous.
|
||||
|
||||
Cordialement,
|
||||
|
||||
L'équipe de la Note Kfet.
|
||||
|
||||
{{ error }}
|
@ -12,6 +12,9 @@
|
||||
<a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Remittance" %}s
|
||||
</a>
|
||||
<a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Société générale credits" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,6 +12,9 @@
|
||||
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||
{% trans "Remittance" %}s
|
||||
</a>
|
||||
<a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Société générale credits" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
67
templates/treasury/sogecredit_detail.html
Normal file
67
templates/treasury/sogecredit_detail.html
Normal file
@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load pretty_money %}
|
||||
{% load perms %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Credit from the Société générale" %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt>
|
||||
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd>
|
||||
|
||||
{% if "note.view_note_balance"|has_perm:object.user.note %}
|
||||
<dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ object.user.note.balance|pretty_money }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'transactions'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
{% for transaction in object.transactions.all %}
|
||||
{{ transaction.membership.club }} ({{ transaction.amount|pretty_money }})<br>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'total amount'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ object.amount|pretty_money }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
{% trans 'Warning: Validating this credit implies that all membership transactions will be validated.' %}
|
||||
{% trans 'If you delete this credit, there all membership transactions will be also validated, but no credit will be operated.' %}
|
||||
{% trans "If this credit is validated, then the user won't be able to ask for a credit from the Société générale." %}
|
||||
{% trans 'If you think there is an error, please contact the "respos info".' %}
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center" id="buttons_footer">
|
||||
{% if object.valid %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "This credit is already validated." %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if object.user.note.balance < object.amount %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Warning: if you don't validate this credit, the note of the user doesn't have enough money to pay its memberships." %}
|
||||
{% trans "Please ask the user to credit its note before deleting this credit." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="btn-group btn-block">
|
||||
<button name="validate" class="btn btn-success">{% trans "Validate" %}</button>
|
||||
{% if object.user.note.balance >= object.amount %}
|
||||
<button name="delete" class="btn btn-danger">{% trans "Delete" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="{% url 'treasury:soge_credits' %}"><button class="btn btn-primary btn-block">{% trans "Return to credit list" %}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
66
templates/treasury/sogecredit_list.html
Normal file
66
templates/treasury/sogecredit_list.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
||||
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Invoice" %}s
|
||||
</a>
|
||||
<a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Remittance" %}s
|
||||
</a>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||
{% trans "Société générale credits" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
|
||||
<div class="form-check">
|
||||
<label for="invalid_only" class="form-check-label">
|
||||
<input id="invalid_only" name="invalid_only" type="checkbox" class="checkboxinput form-check-input">
|
||||
{% trans "Filter with unvalidated credits only" %}
|
||||
</label>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="credits_table">
|
||||
{% if table.data %}
|
||||
{% render_table table %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no matched user that have asked for a Société générale credit." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
let old_pattern = null;
|
||||
let searchbar_obj = $("#searchbar");
|
||||
let invalid_only_obj = $("#invalid_only");
|
||||
|
||||
function reloadTable() {
|
||||
let pattern = searchbar_obj.val();
|
||||
|
||||
if (pattern === old_pattern || pattern === "")
|
||||
return;
|
||||
|
||||
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
||||
|
||||
$(".table-row").click(function() {
|
||||
window.document.location = $(this).data("href");
|
||||
});
|
||||
}
|
||||
|
||||
searchbar_obj.keyup(reloadTable);
|
||||
invalid_only_obj.change(reloadTable);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
9
templates/wei/bus_detail.html
Normal file
9
templates/wei/bus_detail.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
{% include "wei/bus_tables.html" %}
|
||||
{% endblock %}
|
15
templates/wei/bus_form.html
Normal file
15
templates/wei/bus_form.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
50
templates/wei/bus_tables.html
Normal file
50
templates/wei/bus_tables.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ object.name }}</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
{{ object.description }}
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}">{% trans "Edit" %}</a>
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}">{% trans "Add team" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if teams.data %}
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="btn btn-link stretched-link font-weight-bold">
|
||||
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table teams %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if memberships.data %}
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="btn btn-link stretched-link font-weight-bold">
|
||||
<i class="fa fa-bus"></i> {% trans "Members" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table memberships %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
|
||||
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
|
||||
</a>
|
||||
{% endif %}
|
9
templates/wei/busteam_detail.html
Normal file
9
templates/wei/busteam_detail.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
{% include "wei/busteam_tables.html" %}
|
||||
{% endblock %}
|
15
templates/wei/busteam_form.html
Normal file
15
templates/wei/busteam_form.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
54
templates/wei/busteam_tables.html
Normal file
54
templates/wei/busteam_tables.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ bus.name }}</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
{{ bus.description }}
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}">{% trans "Edit" %}</a>
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}">{% trans "Add team" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center" style="background-color: #{{ object.color|stringformat:"06X" }}; color: #{{ -16777215|add:object.color|stringformat:"06X"|slice:"1:" }};">
|
||||
<h4>{{ object.name }}</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
{{ object.description }}
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_team' pk=object.pk %}">{% trans "Edit" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if memberships.data or True %}
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="btn btn-link stretched-link font-weight-bold">
|
||||
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table memberships %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}" data-turbolinks="false">
|
||||
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
|
||||
</a>
|
||||
{% endif %}
|
29
templates/wei/survey.html
Normal file
29
templates/wei/survey.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Survey WEI" %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-xl-6">{% trans 'user'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ object.user }}</dd>
|
||||
</dl>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="card-footer text-center">
|
||||
<input class="btn btn-success" type="submit" value="{% trans "Next" %}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
23
templates/wei/survey_closed.html
Normal file
23
templates/wei/survey_closed.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "member/noteowner_detail.html" %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block profile_info %}
|
||||
{% include "wei/weiclub_info.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block profile_content %}
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Survey WEI" %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{% trans "The inscription for this WEI are now closed." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_detail' pk=club.pk %}">{% trans "Return to WEI detail" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user