Merge branch 'master' into 'explicit>implicit'

# Conflicts:
#   apps/activity/views.py
This commit is contained in:
Pierre-antoine Comby 2020-04-09 22:31:54 +02:00
commit 99d3ba9c1a
54 changed files with 2700 additions and 810 deletions

View File

@ -0,0 +1,20 @@
[
{
"model": "activity.activitytype",
"pk": 1,
"fields": {
"name": "Pot",
"can_invite": true,
"guest_entry_fee": 500
}
},
{
"model": "activity.activitytype",
"pk": 2,
"fields": {
"name": "Soir\u00e9e de club",
"can_invite": false,
"guest_entry_fee": 0
}
}
]

View File

@ -136,6 +136,8 @@ class Entry(models.Model):
class Meta: class Meta:
unique_together = (('activity', 'note', 'guest', ), ) unique_together = (('activity', 'note', 'guest', ), )
verbose_name = _("entry")
verbose_name_plural = _("entries")
def save(self, *args,**kwargs): def save(self, *args,**kwargs):

View File

@ -139,6 +139,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
| Q(name__regex=pattern) | Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern)))) \ | Q(normalized_name__regex=Alias.normalize(pattern)))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\
.filter(note__noteuser__user__profile__registration_valid=True)\
.distinct("username")[:20] .distinct("username")[:20]
for note in note_qs: for note in note_qs:
note.type = "Adhérent" note.type = "Adhérent"
@ -154,4 +155,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
return context return context

View File

@ -75,3 +75,7 @@ class Changelog(models.Model):
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed.")) raise ValidationError(_("Logs cannot be destroyed."))
class Meta:
verbose_name = _("changelog")
verbose_name_plural = _("changelogs")

View File

@ -2,8 +2,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask from permission.models import PermissionMask
@ -18,17 +20,6 @@ class CustomAuthenticationForm(AuthenticationForm):
) )
class SignUpForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop("autofocus", None)
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
class Meta:
model = User
fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. A form for the extras field provided by the :model:`member.Profile` model.
@ -37,7 +28,7 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
exclude = ['user'] exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', )
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):
@ -59,6 +50,42 @@ class ClubForm(forms.ModelForm):
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
help_text=_("Check this case is the Société Générale paid the inscription."),
)
credit_type = forms.ModelChoiceField(
queryset=NoteSpecial.objects,
label=_("Credit type"),
empty_label=_("No credit"),
required=False,
help_text=_("You can credit the note of the user."),
)
credit_amount = forms.IntegerField(
label=_("Credit amount"),
required=False,
initial=0,
widget=AmountInput(),
)
last_name = forms.CharField(
label=_("Last name"),
required=False,
)
first_name = forms.CharField(
label=_("First name"),
required=False,
)
bank = forms.CharField(
label=_("Bank"),
required=False,
)
class Meta: class Meta:
model = Membership model = Membership
fields = ('user', 'roles', 'date_start') fields = ('user', 'roles', 'date_start')

View File

@ -2,13 +2,18 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime import datetime
import os
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.template import loader
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from registration.tokens import email_validation_token
from note.models import MembershipTransaction from note.models import MembershipTransaction
@ -45,6 +50,23 @@ class Profile(models.Model):
) )
paid = models.BooleanField( paid = models.BooleanField(
verbose_name=_("paid"), verbose_name=_("paid"),
help_text=_("Tells if the user receive a salary."),
default=False,
)
email_confirmed = models.BooleanField(
verbose_name=_("email confirmed"),
default=False,
)
registration_valid = models.BooleanField(
verbose_name=_("registration valid"),
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, default=False,
) )
@ -56,6 +78,17 @@ class Profile(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('user_detail', args=(self.pk,)) return reverse('user_detail', args=(self.pk,))
def send_email_validation_link(self):
subject = "Activate your Note Kfet account"
message = loader.render_to_string('registration/mails/email_validation_email.html',
{
'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'),
})
self.user.email_user(subject, message)
class Club(models.Model): class Club(models.Model):
""" """
@ -202,6 +235,7 @@ class Membership(models.Model):
) )
date_start = models.DateField( date_start = models.DateField(
default=datetime.date.today,
verbose_name=_('membership starts on'), verbose_name=_('membership starts on'),
) )
@ -215,12 +249,18 @@ class Membership(models.Model):
) )
def valid(self): def valid(self):
"""
A membership is valid if today is between the start and the end date.
"""
if self.date_end is not None: if self.date_end is not None:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal() return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
else: else:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
"""
if self.club.parent_club is not None: if self.club.parent_club is not None:
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists(): if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name) raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
@ -252,6 +292,9 @@ class Membership(models.Model):
self.make_transaction() self.make_transaction()
def make_transaction(self): def make_transaction(self):
"""
Create Membership transaction associated to this membership.
"""
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists(): if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
return return

View File

@ -10,7 +10,7 @@ def save_user_profile(instance, created, raw, **_kwargs):
# When provisionning data, do not try to autocreate # When provisionning data, do not try to autocreate
return return
if created: if created and instance.is_active:
from .models import Profile from .models import Profile
Profile.objects.get_or_create(user=instance) Profile.objects.get_or_create(user=instance)
instance.profile.save() instance.profile.save()

View File

@ -15,6 +15,9 @@ from .models import Club, Membership
class ClubTable(tables.Table): class ClubTable(tables.Table):
"""
List all clubs.
"""
class Meta: class Meta:
attrs = { attrs = {
'class': 'table table-condensed table-striped table-hover' 'class': 'table table-condensed table-striped table-hover'
@ -30,6 +33,9 @@ class ClubTable(tables.Table):
class UserTable(tables.Table): class UserTable(tables.Table):
"""
List all users.
"""
section = tables.Column(accessor='profile.section') section = tables.Column(accessor='profile.section')
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance")) balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
@ -51,6 +57,9 @@ class UserTable(tables.Table):
class MembershipTable(tables.Table): class MembershipTable(tables.Table):
"""
List all memberships.
"""
roles = tables.Column( roles = tables.Column(
attrs={ attrs={
"td": { "td": {
@ -59,7 +68,17 @@ class MembershipTable(tables.Table):
} }
) )
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
return s
def render_club(self, value): def render_club(self, value):
# If the user has the right, link the displayed club with the page of its detail.
s = value.name s = value.name
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value): if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>", s = format_html("<a href={url}>{name}</a>",
@ -94,6 +113,7 @@ class MembershipTable(tables.Table):
return t return t
def render_roles(self, record): def render_roles(self, record):
# If the user has the right to manage the roles, display the link to manage them
roles = record.roles.all() roles = record.roles.all()
s = ", ".join(str(role) for role in roles) s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record): if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):

View File

@ -7,14 +7,12 @@ from . import views
app_name = 'member' app_name = 'member'
urlpatterns = [ urlpatterns = [
path('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/', views.ClubListView.as_view(), name="club_list"), path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/<int:club_pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"), path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
path('club/renew_membership/<int:pk>/', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"), path('club/renew_membership/<int:pk>/', views.ClubAddMemberView.as_view(), name="club_renew_membership"),
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"), path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"), path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),

View File

@ -9,30 +9,30 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.forms import HiddenInput
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic.base import View
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from note.forms import ImageForm from note.forms import ImageForm
from note.models import Alias, NoteUser from note.models import Alias, NoteUser, NoteSpecial
from note.models.transactions import Transaction from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable from note.tables import HistoryTable, AliasTable
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
from .models import Club, Membership from .models import Club, Membership, Role
from .tables import ClubTable, UserTable, MembershipTable from .tables import ClubTable, UserTable, MembershipTable
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
"""
Login view, where the user can select its permission mask.
"""
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
def form_valid(self, form): def form_valid(self, form):
@ -40,33 +40,10 @@ class CustomLoginView(LoginView):
return super().form_valid(form) return super().form_valid(form)
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profile
"""
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'member/signup.html'
second_form = ProfileForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form()
return context
def form_valid(self, form):
profile_form = ProfileForm(self.request.POST)
if form.is_valid() and profile_form.is_valid():
user = form.save(commit=False)
user.profile = profile_form.save(commit=False)
user.save()
user.profile.save()
return super().form_valid(form)
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the user information.
"""
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html' template_name = 'member/profile_update.html'
@ -75,14 +52,20 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form = context['form']
form.fields['username'].widget.attrs.pop("autofocus", None)
form.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
form.fields['first_name'].required = True
form.fields['last_name'].required = True
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
context['profile_form'] = self.profile_form(instance=context['user_object'].profile) context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
context['title'] = _("Update Profile") context['title'] = _("Update Profile")
return context return context
def get_form(self, form_class=None): def form_valid(self, form):
form = super().get_form(form_class)
if 'username' not in form.data:
return form
new_username = form.data['username'] new_username = form.data['username']
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant # Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
note = NoteUser.objects.filter( note = NoteUser.objects.filter(
@ -90,9 +73,8 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
if note.exists() and note.get().user != self.object: if note.exists() and note.get().user != self.object:
form.add_error('username', form.add_error('username',
_("An alias with a similar name already exists.")) _("An alias with a similar name already exists."))
return form return super().form_invalid(form)
def form_valid(self, form):
profile_form = ProfileForm( profile_form = ProfileForm(
data=self.request.POST, data=self.request.POST,
instance=self.object.profile, instance=self.object.profile,
@ -108,19 +90,24 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
if similar.exists(): if similar.exists():
similar.delete() similar.delete()
olduser = User.objects.get(pk=form.instance.pk)
user = form.save(commit=False) user = form.save(commit=False)
profile = profile_form.save(commit=False) profile = profile_form.save(commit=False)
profile.user = user profile.user = user
profile.save() profile.save()
user.save() user.save()
if olduser.email != user.email:
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
user.profile.email_confirmed = False
user.profile.send_email_validation_link()
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
if kwargs: url = 'member:user_detail' if self.object.profile.registration_valid else 'registration:future_user_detail'
return reverse_lazy('member:user_detail', return reverse_lazy(url, args=(self.object.id,))
kwargs={'pk': kwargs['id']})
else:
return reverse_lazy('member:user_detail', args=(self.object.id,))
class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
@ -131,29 +118,43 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = "user_object" context_object_name = "user_object"
template_name = "member/profile_detail.html" template_name = "member/profile_detail.html"
def get_queryset(self, **kwargs):
"""
We can't display information of a not registered user.
"""
return super().get_queryset().filter(profile__registration_valid=True)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['user_object'] user = context['user_object']
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
context['history_list'] = HistoryTable(history_list) history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\ club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
context['club_list'] = MembershipTable(data=club_list) membership_table = MembershipTable(data=club_list, prefix='membership-')
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table
return context return context
class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
Affiche la liste des utilisateurs, avec une fonction de recherche statique Display user list, with a search bar
""" """
model = User model = User
table_class = UserTable table_class = UserTable
template_name = 'member/user_list.html' template_name = 'member/user_list.html'
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset() """
Filter the user list with the given pattern.
"""
qs = super().get_queryset().filter(profile__registration_valid=True)
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -164,6 +165,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
Q(first_name__iregex=pattern) Q(first_name__iregex=pattern)
| Q(last_name__iregex=pattern) | Q(last_name__iregex=pattern)
| Q(profile__section__iregex=pattern) | Q(profile__section__iregex=pattern)
| Q(profile__username__iregex="^" + pattern)
| Q(note__alias__name__iregex="^" + pattern) | Q(note__alias__name__iregex="^" + pattern)
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
) )
@ -181,6 +183,9 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user aliases.
"""
model = User model = User
template_name = 'member/profile_alias.html' template_name = 'member/profile_alias.html'
context_object_name = 'user_object' context_object_name = 'user_object'
@ -193,6 +198,9 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
"""
Update profile picture of the user note.
"""
form_class = ImageForm form_class = ImageForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -292,6 +300,9 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Display details of a club
"""
model = Club model = Club
context_object_name = "club" context_object_name = "club"
@ -304,14 +315,19 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id') .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
context['history_list'] = HistoryTable(club_transactions) 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 = Membership.objects.filter( club_member = Membership.objects.filter(
club=club, club=club,
date_end__gte=datetime.today(), date_end__gte=datetime.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
context['member_list'] = MembershipTable(data=club_member) membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
context['member_list'] = membership_table
# Check if the user has the right to create a membership, to display the button.
empty_membership = Membership( empty_membership = Membership(
club=club, club=club,
user=User.objects.first(), user=User.objects.first(),
@ -326,6 +342,9 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Manage aliases of a club.
"""
model = Club model = Club
template_name = 'member/club_alias.html' template_name = 'member/club_alias.html'
context_object_name = 'club' context_object_name = 'club'
@ -338,6 +357,9 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the information of a club.
"""
model = Club model = Club
context_object_name = "club" context_object_name = "club"
form_class = ClubForm form_class = ClubForm
@ -348,6 +370,9 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
class ClubPictureUpdateView(PictureUpdateView): class ClubPictureUpdateView(PictureUpdateView):
"""
Update the profile picture of a club.
"""
model = Club model = Club
template_name = 'member/club_picture_update.html' template_name = 'member/club_picture_update.html'
context_object_name = 'club' context_object_name = 'club'
@ -357,29 +382,107 @@ class ClubPictureUpdateView(PictureUpdateView):
class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Add a membership to a club.
"""
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipForm
template_name = 'member/add_members.html' template_name = 'member/add_members.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["pk"])
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form = context['form']
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"])
form.fields['credit_amount'].initial = club.membership_fee_paid
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
if club.name != "BDE":
del form.fields['soge']
else:
fee = 0
bde = Club.objects.get(name="BDE")
fee += bde.membership_fee_paid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid
context["total_fee"] = "{:.02f}".format(fee / 100, )
else:
# This is a renewal. Fields can be pre-completed.
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
user = old_membership.user
form.fields['user'].initial = user
form.fields['user'].disabled = True
form.fields['roles'].initial = old_membership.roles.all()
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
else club.membership_fee_unpaid
form.fields['last_name'].initial = user.last_name
form.fields['first_name'].initial = user.first_name
# If this is a renewal of a BDE membership, Société générale can pays, if it is not yet done
if club.name != "BDE" or user.profile.soge:
del form.fields['soge']
else:
fee = 0
bde = Club.objects.get(name="BDE")
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
context["total_fee"] = "{:.02f}".format(fee / 100, )
context['club'] = club context['club'] = club
return context return context
def form_valid(self, form): def form_valid(self, form):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ """
.get(pk=self.kwargs["pk"]) Create membership, check that all is good, make transactions
user = self.request.user """
# Get the club that is concerned by the membership
if "club_pk" in self.kwargs:
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
else:
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
user = old_membership.user
form.instance.club = club form.instance.club = club
# Get form data
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
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 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"
if credit_type is None:
credit_amount = 0
if user.profile.paid: if user.profile.paid:
fee = club.membership_fee_paid fee = club.membership_fee_paid
else: else:
fee = club.membership_fee_unpaid fee = club.membership_fee_unpaid
if user.note.balance < fee and not Membership.objects.filter( if user.note.balance + credit_amount < fee and not Membership.objects.filter(
club__name="Kfet", club__name="Kfet",
user=user, user=user,
date_start__lte=datetime.now().date(), date_start__lte=datetime.now().date(),
@ -390,6 +493,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
form.add_error('user', form.add_error('user',
_("This user don't have enough money to join this club, and can't have a negative balance.")) _("This user don't have enough money to join this club, and can't have a negative balance."))
return super().form_invalid(form)
if club.parent_club is not None: if club.parent_club is not None:
if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists(): if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
@ -405,16 +509,70 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
form.add_error('user', _('User is already a member of the club')) form.add_error('user', _('User is already a member of the club'))
return super().form_invalid(form) return super().form_invalid(form)
if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start: if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start)) .format(form.instance.club.membership_start))
return super().form_invalid(form) return super().form_invalid(form)
if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end: if club.membership_end and form.instance.date_start > club.membership_end:
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
.format(form.instance.club.membership_start)) .format(form.instance.club.membership_start))
return super().form_invalid(form) return super().form_invalid(form)
# Now, all is fine, the membership can be created.
# 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"):
if not last_name:
form.add_error('last_name', _("This field is required."))
if not first_name:
form.add_error('first_name', _("This field is required."))
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
SpecialTransaction.objects.create(
source=credit_type,
destination=user.note,
quantity=1,
amount=credit_amount,
reason="Crédit " + credit_type.special_type + " (Adhésion " + club.name + ")",
last_name=last_name,
first_name=first_name,
bank=bank,
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()
kfet = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
# Get current membership, to get the end date
old_membership = Membership.objects.filter(
club__name="Kfet",
user=user,
date_start__lte=datetime.today(),
date_end__gte=datetime.today(),
)
membership = Membership.objects.create(
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,
)
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 super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -422,6 +580,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Manage the roles of a user in a club
"""
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipForm
template_name = 'member/add_members.html' template_name = 'member/add_members.html'
@ -430,49 +591,19 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = self.object.club club = self.object.club
context['club'] = club context['club'] = club
form = context['form']
form.fields['user'].disabled = True
form.fields['date_start'].widget = HiddenInput()
return context return context
def form_valid(self, form): def get_form(self, form_class=None):
if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start: form = super().get_form(form_class)
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") # We don't create a full membership, we only update one field
.format(form.instance.club.membership_start)) form.fields['user'].disabled = True
return super().form_invalid(form) del form.fields['date_start']
del form.fields['credit_type']
if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end: del form.fields['credit_amount']
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") del form.fields['last_name']
.format(form.instance.club.membership_start)) del form.fields['first_name']
return super().form_invalid(form) del form.fields['bank']
return form
return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
class ClubRenewMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, View):
def get(self, *args, **kwargs):
user = self.request.user
membership = Membership.objects.filter(PermissionBackend.filter_queryset(user, Membership, "change"))\
.filter(pk=self.kwargs["pk"]).get()
if Membership.objects.filter(
club=membership.club,
user=membership.user,
date_start__gte=membership.club.membership_start,
date_end__lte=membership.club.membership_end,
).exists():
raise ValidationError(_("This membership is already renewed"))
new_membership = Membership.objects.create(
user=user,
club=membership.club,
date_start=membership.date_end + timedelta(days=1),
)
new_membership.roles.set(membership.roles.all())
new_membership.save()
return redirect(reverse_lazy('member:club_detail', kwargs={'pk': membership.club.pk}))

View File

@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction RecurrentTransaction, MembershipTransaction, SpecialTransaction
class AliasInlines(admin.TabularInline): class AliasInlines(admin.TabularInline):
@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
""" """
Admin customisation for Transaction Admin customisation for Transaction
""" """
child_models = (RecurrentTransaction, MembershipTransaction) child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction)
list_display = ('created_at', 'poly_source', 'poly_destination', list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'valid') 'quantity', 'amount', 'valid')
list_filter = ('valid',) list_filter = ('valid',)
@ -141,7 +141,14 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
@admin.register(MembershipTransaction) @admin.register(MembershipTransaction)
class MembershipTransactionAdmin(PolymorphicChildModelAdmin): class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
""" """
Admin customisation for Transaction Admin customisation for MembershipTransaction
"""
@admin.register(SpecialTransaction)
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for SpecialTransaction
""" """

View File

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

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -29,7 +30,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
table_class = HistoryTable table_class = HistoryTable
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:50] return super().get_queryset(**kwargs).order_by("-id").all()[:20]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
@ -44,12 +45,19 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
.order_by("special_type").all() .order_by("special_type").all()
# Add a shortcut for entry page for open activities
if "activity" in settings.INSTALLED_APPS:
from activity.models import Activity
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
return context return context
class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
""" """
Create TransactionTemplate Create Transaction template
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
@ -58,7 +66,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
List TransactionsTemplates List Transaction templates
""" """
model = TransactionTemplate model = TransactionTemplate
table_class = ButtonTable table_class = ButtonTable
@ -66,6 +74,7 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
Update Transaction template
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
@ -84,7 +93,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
table_class = HistoryTable table_class = HistoryTable
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:50] return super().get_queryset(**kwargs).order_by("-id").all()[:20]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,10 @@ class PermissionMask(models.Model):
def __str__(self): def __str__(self):
return self.description return self.description
class Meta:
verbose_name = _("permission mask")
verbose_name_plural = _("permission masks")
class Permission(models.Model): class Permission(models.Model):
@ -153,6 +157,8 @@ class Permission(models.Model):
class Meta: class Meta:
unique_together = ('model', 'query', 'type', 'field') unique_together = ('model', 'query', 'type', 'field')
verbose_name = _("permission")
verbose_name_plural = _("permissions")
def clean(self): def clean(self):
self.query = json.dumps(json.loads(self.query)) self.query = json.dumps(json.loads(self.query))
@ -293,3 +299,7 @@ class RolePermissions(models.Model):
def __str__(self): def __str__(self):
return str(self.role) return str(self.role)
class Meta:
verbose_name = _("role permissions")
verbose_name_plural = _("role permissions")

View File

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

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

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

View File

@ -0,0 +1,80 @@
# 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.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
from note_kfet.inputs import AmountInput
class SignUpForm(UserCreationForm):
"""
Pre-register users with all information
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop("autofocus", None)
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['first_name'].required = True
self.fields['last_name'].required = True
self.fields['email'].required = True
self.fields['email'].help_text = _("This address must be valid.")
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email', )
class ValidationForm(forms.Form):
"""
Validate the inscription of the new users and pay memberships.
"""
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
help_text=_("Check this case is the Société Générale paid the inscription."),
)
credit_type = forms.ModelChoiceField(
queryset=NoteSpecial.objects,
label=_("Credit type"),
empty_label=_("No credit"),
required=False,
)
credit_amount = forms.IntegerField(
label=_("Credit amount"),
required=False,
initial=0,
widget=AmountInput(),
)
last_name = forms.CharField(
label=_("Last name"),
required=False,
)
first_name = forms.CharField(
label=_("First name"),
required=False,
)
bank = forms.CharField(
label=_("Bank"),
required=False,
)
join_BDE = forms.BooleanField(
label=_("Join BDE Club"),
required=False,
initial=True,
)
# The user can join the Kfet club at the inscription
join_Kfet = forms.BooleanField(
label=_("Join Kfet Club"),
required=False,
initial=True,
)

View File

View File

@ -0,0 +1,26 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.contrib.auth.models import User
class FutureUserTable(tables.Table):
"""
Display the list of pre-registered users
"""
phone_number = tables.Column(accessor='profile.phone_number')
section = tables.Column(accessor='profile.section')
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email', )
model = User
row_attrs = {
'class': 'table-row',
'data-href': lambda record: record.pk
}

View File

@ -0,0 +1,30 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py
from django.contrib.auth.tokens import PasswordResetTokenGenerator
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
"""
Create a unique token generator to confirm email addresses.
"""
def _make_hash_value(self, user, timestamp):
"""
Hash the user's primary key and some user state that's sure to change
after an account validation to produce a token that invalidated when
it's used:
1. The user.profile.email_confirmed field will change upon an account
validation.
2. The last_login field will usually be updated very shortly after
an account validation.
Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually
invalidates the token.
"""
# Truncate microseconds so that tokens are consistent even if the
# database doesn't support microseconds.
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
email_validation_token = AccountActivationTokenGenerator()

18
apps/registration/urls.py Normal file
View File

@ -0,0 +1,18 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from . import views
app_name = 'registration'
urlpatterns = [
path('signup/', views.UserCreateView.as_view(), name="signup"),
path('validate_email/sent/', views.UserValidationEmailSentView.as_view(), name='email_validation_sent'),
path('validate_email/resend/<int:pk>/', views.UserResendValidationEmailView.as_view(),
name='email_validation_resend'),
path('validate_email/<uidb64>/<token>/', views.UserValidateView.as_view(), name='email_validation'),
path('validate_user/', views.FutureUserListView.as_view(), name="future_user_list"),
path('validate_user/<int:pk>/', views.FutureUserDetailView.as_view(), name="future_user_detail"),
path('validate_user/<int:pk>/invalidate/', views.FutureUserInvalidateView.as_view(), name="future_user_invalidate"),
]

358
apps/registration/views.py Normal file
View File

@ -0,0 +1,358 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.shortcuts import resolve_url, redirect
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.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.templatetags.pretty_money import pretty_money
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import SignUpForm, ValidationForm
from .tables import FutureUserTable
from .tokens import email_validation_token
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profil
"""
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()
return context
def form_valid(self, form):
"""
If the form is valid, then the user is created with is_active set to False
so that the user cannot log in until the email has been validated.
The user must also wait that someone validate her/his account.
"""
profile_form = ProfileForm(data=self.request.POST)
if not profile_form.is_valid():
return self.form_invalid(form)
# Save the user and the profile
user = form.save(commit=False)
user.is_active = False
profile_form.instance.user = user
profile = profile_form.save(commit=False)
user.profile = profile
user.save()
user.refresh_from_db()
profile.user = user
profile.save()
user.profile.send_email_validation_link()
return super().form_valid(form)
class UserValidateView(TemplateView):
"""
A view to validate the email address.
"""
title = _("Email validation")
template_name = 'registration/email_validation_complete.html'
def get(self, *args, **kwargs):
"""
With a given token and user id (in params), validate the email address.
"""
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
user = self.get_user(kwargs['uidb64'])
token = kwargs['token']
# Validate the token
if user is not None and email_validation_token.check_token(user, token):
self.validlink = True
# The user must wait that someone validates the account before the user can be active and login.
user.is_active = user.profile.registration_valid or user.is_superuser
user.profile.email_confirmed = True
user.save()
user.profile.save()
return super().dispatch(*args, **kwargs)
else:
# Display the "Email validation unsuccessful" page.
return self.render_to_response(self.get_context_data())
def get_user(self, uidb64):
"""
Get user from the base64-encoded string.
"""
try:
# urlsafe_base64_decode() decodes to bytestring
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
user = None
return user
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.get_user(self.kwargs["uidb64"])
context['login_url'] = resolve_url(settings.LOGIN_URL)
if self.validlink:
context['validlink'] = True
else:
context.update({
'title': _('Email validation unsuccessful'),
'validlink': False,
})
return context
class UserValidationEmailSentView(TemplateView):
"""
Display the information that the validation link has been sent.
"""
template_name = 'registration/email_validation_email_sent.html'
title = _('Email validation email sent')
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
"""
Rensend the email validation link.
"""
model = User
def get(self, request, *args, **kwargs):
user = self.get_object()
user.profile.send_email_validation_link()
url = 'member:user_detail' if user.profile.registration_valid else 'registration:future_user_detail'
return redirect(url, user.id)
class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Display pre-registered users, with a search bar
"""
model = User
table_class = FutureUserTable
template_name = 'registration/future_user_list.html'
def get_queryset(self, **kwargs):
"""
Filter the table with the given parameter.
:param kwargs:
:return:
"""
qs = super().get_queryset().filter(profile__registration_valid=False)
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if not pattern:
return qs.none()
qs = qs.filter(
Q(first_name__iregex=pattern)
| Q(last_name__iregex=pattern)
| Q(profile__section__iregex=pattern)
| Q(username__iregex="^" + pattern)
)
else:
qs = qs.none()
return qs[:20]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Unregistered users")
return context
class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
"""
Display information about a pre-registered user, in order to complete the registration.
"""
model = User
form_class = ValidationForm
context_object_name = "user_object"
template_name = "registration/future_profile_detail.html"
def post(self, request, *args, **kwargs):
form = self.get_form()
self.object = self.get_object()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_queryset(self, **kwargs):
"""
We only display information of a not registered user.
"""
return super().get_queryset().filter(profile__registration_valid=False)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
user = self.get_object()
fee = 0
bde = Club.objects.get(name="BDE")
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
return ctx
def get_form(self, form_class=None):
form = super().get_form(form_class)
user = self.get_object()
form.fields["last_name"].initial = user.last_name
form.fields["first_name"].initial = user.first_name
return form
def form_valid(self, form):
user = self.get_object()
# Get form data
soge = form.cleaned_data["soge"]
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
join_BDE = form.cleaned_data["join_BDE"]
join_Kfet = form.cleaned_data["join_Kfet"]
if soge:
# If Société Générale pays the inscription, the user joins the two clubs
join_BDE = True
join_Kfet = True
if not join_BDE:
form.add_error('join_BDE', _("You must join the BDE."))
return super().form_invalid(form)
fee = 0
bde = Club.objects.get(name="BDE")
bde_fee = bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
if join_BDE:
fee += bde_fee
kfet = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
if join_Kfet:
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"
print("OK")
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:
# 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 {}")
.format(pretty_money(fee)))
return self.form_invalid(form)
if credit_type is not None and credit_amount > 0:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
if not first_name:
form.add_error('first_name', _("This field is required."))
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
# Save the user and finally validate the registration
# Saving the user creates the associated note
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()
if credit_type is not None and credit_amount > 0:
# Credit the note
SpecialTransaction.objects.create(
source=credit_type,
destination=user.note,
quantity=1,
amount=credit_amount,
reason="Crédit " + ("Société générale" if soge else credit_type.special_type) + " (Inscription)",
last_name=last_name,
first_name=first_name,
bank=bank,
valid=True,
)
if join_BDE:
# Create membership for the user to the BDE starting today
membership = Membership.objects.create(
club=bde,
user=user,
fee=bde_fee,
)
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(
club=kfet,
user=user,
fee=kfet_fee,
)
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save()
return ret
def get_success_url(self):
return reverse_lazy('member:user_detail', args=(self.get_object().pk, ))
class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
"""
Delete a pre-registered user.
"""
def get(self, request, *args, **kwargs):
"""
Delete the pre-registered user which id is given in the URL.
"""
user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"])
user.delete()
return redirect('registration:future_user_list')

View File

@ -53,7 +53,7 @@ ProductFormSet = forms.inlineformset_factory(
class ProductFormSetHelper(FormHelper): class ProductFormSetHelper(FormHelper):
""" """
Specify some template informations for the product form. Specify some template information for the product form.
""" """
def __init__(self, form=None): def __init__(self, form=None):

View File

@ -59,6 +59,10 @@ class Invoice(models.Model):
verbose_name=_("Acquitted"), verbose_name=_("Acquitted"),
) )
class Meta:
verbose_name = _("invoice")
verbose_name_plural = _("invoices")
class Product(models.Model): class Product(models.Model):
""" """
@ -95,6 +99,10 @@ class Product(models.Model):
def total_euros(self): def total_euros(self):
return self.total / 100 return self.total / 100
class Meta:
verbose_name = _("product")
verbose_name_plural = _("products")
class RemittanceType(models.Model): class RemittanceType(models.Model):
""" """
@ -109,6 +117,10 @@ class RemittanceType(models.Model):
def __str__(self): def __str__(self):
return str(self.note) return str(self.note)
class Meta:
verbose_name = _("remittance type")
verbose_name_plural = _("remittance types")
class Remittance(models.Model): class Remittance(models.Model):
""" """
@ -136,6 +148,10 @@ class Remittance(models.Model):
verbose_name=_("Closed"), verbose_name=_("Closed"),
) )
class Meta:
verbose_name = _("remittance")
verbose_name_plural = _("remittances")
@property @property
def transactions(self): def transactions(self):
""" """
@ -187,3 +203,7 @@ class SpecialTransactionProxy(models.Model):
null=True, null=True,
verbose_name=_("Remittance"), verbose_name=_("Remittance"),
) )
class Meta:
verbose_name = _("special transaction proxy")
verbose_name_plural = _("special transaction proxies")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ class AmountInput(NumberInput):
template_name = "note/amount_input.html" template_name = "note/amount_input.html"
def format_value(self, value): def format_value(self, value):
return None if value is None or value == "" else "{:.02f}".format(value / 100, ) return None if value is None or value == "" else "{:.02f}".format(int(value) / 100, )
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
val = super().value_from_datadict(data, files, name) val = super().value_from_datadict(data, files, name)

View File

@ -54,13 +54,14 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
# Note apps # Note apps
'api',
'activity', 'activity',
'logs',
'member', 'member',
'note', 'note',
'treasury',
'permission', 'permission',
'api', 'registration',
'logs', 'treasury',
] ]
LOGIN_REDIRECT_URL = '/note/transfer/' LOGIN_REDIRECT_URL = '/note/transfer/'

View File

@ -16,6 +16,7 @@ urlpatterns = [
# Include project routers # Include project routers
path('note/', include('note.urls')), path('note/', include('note.urls')),
path('accounts/', include('member.urls')), path('accounts/', include('member.urls')),
path('registration/', include('registration.urls')),
path('activity/', include('activity.urls')), path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')), path('treasury/', include('treasury.urls')),
@ -37,14 +38,7 @@ if "cas_server" in settings.INSTALLED_APPS:
# Include CAS Server routers # Include CAS Server routers
path('cas/', include('cas_server.urls', namespace="cas_server")), path('cas/', include('cas_server.urls', namespace="cas_server")),
] ]
if "cas" in settings.INSTALLED_APPS:
from cas import views as cas_views
urlpatterns += [
# Include CAS Client routers
path('accounts/login/cas/', cas_views.login, name='cas_login'),
path('accounts/logout/cas/', cas_views.logout, name='cas_logout'),
]
if "debug_toolbar" in settings.INSTALLED_APPS: if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns = [

View File

@ -24,6 +24,9 @@ $(document).ready(function () {
$("#" + prefix + "_" + obj.id).click(function() { $("#" + prefix + "_" + obj.id).click(function() {
target.val(obj[name_field]); target.val(obj[name_field]);
$("#" + prefix + "_pk").val(obj.id); $("#" + prefix + "_pk").val(obj.id);
if (typeof autocompleted != 'undefined')
autocompleted(obj, prefix)
}); });
if (input === obj[name_field]) if (input === obj[name_field])

View File

@ -19,23 +19,32 @@ function pretty_money(value) {
* Add a message on the top of the page. * Add a message on the top of the page.
* @param msg The message to display * @param msg The message to display
* @param alert_type The type of the alert. Choices: info, success, warning, danger * @param alert_type The type of the alert. Choices: info, success, warning, danger
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/ */
function addMsg(msg, alert_type) { function addMsg(msg, alert_type, timeout=-1) {
let msgDiv = $("#messages"); let msgDiv = $("#messages");
let html = msgDiv.html(); let html = msgDiv.html();
let id = Math.floor(10000 * Math.random() + 1);
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" + html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" +
"<button class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>" "<button id=\"close-message-" + id + "\" class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>"
+ msg + "</div>\n"; + msg + "</div>\n";
msgDiv.html(html); msgDiv.html(html);
if (timeout > 0) {
setTimeout(function () {
$("#close-message-" + id).click();
}, timeout);
}
} }
/** /**
* add Muliple error message from err_obj * add Muliple error message from err_obj
* @param errs_obj [{error_code:erro_message}] * @param errs_obj [{error_code:erro_message}]
* @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored.
*/ */
function errMsg(errs_obj){ function errMsg(errs_obj, timeout=-1) {
for (const err_msg of Object.values(errs_obj)) { for (const err_msg of Object.values(errs_obj)) {
addMsg(err_msg,'danger'); addMsg(err_msg,'danger', timeout);
} }
} }

View File

@ -61,16 +61,24 @@ $(document).ready(function() {
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading. // Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
$("#type_gift").prop('checked', 'true'); let type_gift = $("#type_gift"); // Default mode
type_gift.removeAttr('checked');
$("#type_transfer").removeAttr('checked'); $("#type_transfer").removeAttr('checked');
$("#type_credit").removeAttr('checked'); $("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked'); $("#type_debit").removeAttr('checked');
$("label[for='type_gift']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
if (location.hash)
$("#type_" + location.hash.substr(1)).click();
else
type_gift.click();
location.hash = "";
}); });
$("#transfer").click(function() { $("#btn_transfer").click(function() {
if ($("#type_gift").is(':checked')) { if ($("#type_gift").is(':checked')) {
dests_notes_display.forEach(function (dest) { dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/", $.post("/api/note/transaction/transaction/",

View File

@ -118,7 +118,6 @@
}); });
$("#validate_activity").click(function () { $("#validate_activity").click(function () {
console.log(42);
$.ajax({ $.ajax({
url: "/api/activity/activity/{{ activity.pk }}/", url: "/api/activity/activity/{{ activity.pk }}/",
type: "PATCH", type: "PATCH",

View File

@ -6,6 +6,26 @@
{% load perms %} {% load perms %}
{% block content %} {% 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 "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
{% trans "Transfer" %}
</a>
{% if "note.notespecial"|not_empty_model_list %}
<a href="{% url "note:transfer" %}#credit" class="btn btn-sm btn-outline-primary">
{% trans "Credit" %}
</a>
{% endif %}
{% for a in activities_open %}
<a href="{% url "activity:activity_entry" pk=a.pk %}" class="btn btn-sm btn-outline-primary{% if a.pk == activity.pk %} active{% endif %}">
{% trans "Entries" %} {{ a.name }}
</a>
{% endfor %}
</div>
</div>
</div>
<a href="{% url "activity:activity_detail" pk=activity.pk %}"> <a href="{% url "activity:activity_detail" pk=activity.pk %}">
<button class="btn btn-light">{% trans "Return to activity page" %}</button> <button class="btn btn-light">{% trans "Return to activity page" %}</button>
</a> </a>
@ -56,10 +76,10 @@
note: id, note: id,
guest: null guest: null
}).done(function () { }).done(function () {
addMsg("Entrée effectuée !", "success"); addMsg("Entrée effectuée !", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function(xhr) { }).fail(function(xhr) {
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON, 4000);
}); });
} }
else { else {
@ -84,10 +104,10 @@
note: target.attr("data-inviter"), note: target.attr("data-inviter"),
guest: id guest: id
}).done(function () { }).done(function () {
addMsg("Entrée effectuée !", "success"); addMsg("Entrée effectuée !", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function (xhr) { }).fail(function (xhr) {
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON, 4000);
}); });
}; };
@ -111,7 +131,7 @@
makeTransaction(); makeTransaction();
reset(); reset();
}).fail(function (xhr) { }).fail(function (xhr) {
errMsg(xhr.responseJSON); errMsg(xhr.responseJSON, 4000);
}); });
}; };
}; };

View File

@ -94,6 +94,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<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="fa fa-users"></i> {% trans 'Clubs' %}</a>
</li> </li>
{% endif %} {% 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" %}
</a>
</li>
{% endif %}
{% if "activity.activity"|not_empty_model_list %} {% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
@ -124,7 +131,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</li> </li>
{% else %} {% else %}
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:signup' %}"> <a class="nav-link" href="{% url 'registration:signup' %}">
<i class="fa fa-user-plus"></i> S'inscrire <i class="fa fa-user-plus"></i> S'inscrire
</a> </a>
</li> </li>
@ -138,6 +145,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</nav> </nav>
<div class="container-fluid my-3" style="max-width: 1600px;"> <div class="container-fluid my-3" style="max-width: 1600px;">
{% if user.is_authenticated and not user.profile.email_confirmed %}
<div class="alert alert-warning">
{% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %}
</div>
{% endif %}
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div> <div id="messages"></div>
{% block content %} {% block content %}

View File

@ -16,6 +16,40 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
</script> function autocompleted(user) {
$("#id_last_name").val(user.last_name);
$("#id_first_name").val(user.first_name);
$.getJSON("/api/members/profile/" + user.id + "/", function(profile) {
let fee = profile.paid ? {{ club.membership_fee_paid }} : {{ club.membership_fee_unpaid }};
$("#id_credit_amount").val((fee / 100).toFixed(2));
});
}
soge_field = $("#id_soge");
function fillFields() {
let checked = soge_field.is(':checked');
if (!checked) {
$("input").attr('disabled', false);
$("#id_user").attr('disabled', true);
$("select").attr('disabled', false);
return;
}
let credit_type = $("#id_credit_type");
credit_type.attr('disabled', true);
credit_type.val(4);
let credit_amount = $("#id_credit_amount");
credit_amount.attr('disabled', true);
credit_amount.val('{{ total_fee }}');
let bank = $("#id_bank");
bank.attr('disabled', true);
bank.val('Société générale');
}
soge_field.change(fillFields);
</script>
{% endblock %} {% endblock %}

View File

@ -10,9 +10,11 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
function refreshHistory() { function refreshHistory() {
$("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list"); $("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos"); $("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos");
} }
window.history.replaceState({}, document.title, location.pathname);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -49,13 +49,13 @@
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if can_add_members %} {% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a>
{% endif %} {% endif %}
{% if ".change_"|has_perm:club %} {% 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> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% endif %} {% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.get_full_path != club_detail_url %} {%if request.path_info != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} </div> {% endif %} </div>
</div> </div>

View File

@ -36,7 +36,6 @@ function getInfo() {
if (asked.length >= 1) { if (asked.length >= 1) {
$.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){ $.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){
let selected_id = buttons.results.map((a => "#row-"+a.id)); let selected_id = buttons.results.map((a => "#row-"+a.id));
console.log(selected_id.join());
$(".table-row,"+selected_id.join()).show(); $(".table-row,"+selected_id.join()).show();
$(".table-row").not(selected_id.join()).hide(); $(".table-row").not(selected_id.join()).hide();

View File

@ -1,31 +1,23 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %} {% load i18n %}
<div class="accordion shadow" id="accordionProfile"> <div class="card">
<div class="card"> <div class="card-header position-relative" id="clubListHeading">
<div class="card-header position-relative" id="clubListHeading"> <a class="btn btn-link stretched-link font-weight-bold">
<a class="btn btn-link stretched-link font-weight-bold" <i class="fa fa-users"></i> {% trans "Member of the Club" %}
data-toggle="collapse" data-target="#clubListCollapse" </a>
aria-expanded="true" aria-controls="clubListCollapse">
<i class="fa fa-users"></i> {% trans "Member of the Club" %}
</a>
</div>
<div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile">
{% render_table member_list %}
</div>
</div>
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="false" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
<div id="history_list">
{% render_table history_list %}
</div>
</div>
</div> </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>
<div id="history_list">
{% render_table history_list %}
</div>
</div> </div>

View File

@ -7,3 +7,14 @@
{% block profile_content %} {% block profile_content %}
{% include "member/profile_tables.html" %} {% include "member/profile_tables.html" %}
{% endblock %} {% endblock %}
{% 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");
}
window.history.replaceState({}, document.title, location.pathname);
</script>
{% endblock %}

View File

@ -44,7 +44,7 @@
<div class="card-footer text-center"> <div class="card-footer text-center">
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a> <a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
{% url 'member:user_detail' object.pk as user_profile_url %} {% url 'member:user_detail' object.pk as user_profile_url %}
{%if request.get_full_path != user_profile_url %} {%if request.path_info != user_profile_url %}
<a class="btn btn-primary btn-sm" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-primary btn-sm" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,31 +1,34 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %} {% load i18n %}
<div class="accordion shadow" id="accordionProfile"> {% load perms %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold"
data-toggle="collapse" data-target="#clubListCollapse"
aria-expanded="true" aria-controls="clubListCollapse">
<i class="fa fa-users"></i> {% trans "View my memberships" %}
</a>
</div>
<div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile">
{% render_table club_list %}
</div>
</div>
<div class="card"> {% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %}
<div class="card-header position-relative" id="historyListHeading"> <div class="alert alert-warning">
<a class="btn btn-link stretched-link collapsed font-weight-bold" {% trans "This user doesn't have confirmed his/her e-mail address." %}
data-toggle="collapse" data-target="#historyListCollapse" <a href="{% url "registration:email_validation_resend" pk=object.pk %}">{% trans "Click here to resend a validation link." %}</a>
aria-expanded="false" aria-controls="historyListCollapse"> </div>
<i class="fa fa-euro"></i> {% trans "Transaction history" %} {% endif %}
</a>
</div> <div class="card">
<div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> <div class="card-header position-relative" id="clubListHeading">
<div id="history_list"> <a class="btn btn-link stretched-link font-weight-bold">
{% render_table history_list %} <i class="fa fa-users"></i> {% trans "View my memberships" %}
</div> </a>
</div> </div>
{% render_table club_list %}
</div>
<hr>
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="true" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div> </div>
</div> </div>

View File

@ -7,7 +7,13 @@
<hr> <hr>
<div id="user_table"> <div id="user_table">
{% render_table table %} {% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no pending user with this pattern." %}
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -28,6 +28,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% trans "Debit" %} {% trans "Debit" %}
</label> </label>
{% endif %} {% endif %}
{% for activity in activities_open %}
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
{% trans "Entries" %} {{ activity.name }}
</a>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
@ -137,7 +142,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="form-row"> <div class="form-row">
<div class="col-md-12"> <div class="col-md-12">
<button id="transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button> <button id="btn_transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button>
</div> </div>
</div> </div>

View File

@ -37,7 +37,6 @@ function getInfo() {
if (asked.length >= 1) { if (asked.length >= 1) {
$.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){ $.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){
let selected_id = buttons.results.map((a => "#row-"+a.id)); let selected_id = buttons.results.map((a => "#row-"+a.id));
console.log(selected_id.join());
$(".table-row,"+selected_id.join()).show(); $(".table-row,"+selected_id.join()).show();
$(".table-row").not(selected_id.join()).hide(); $(".table-row").not(selected_id.join()).hide();

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if validlink %}
{% trans "Your email have successfully been validated." %}
{% if user.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." %}
{% endif %}
{% else %}
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<h2>Account Activation</h2>
An email has been sent. Please click on the link to activate your account.
{% endblock %}

View File

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load perms %}
{% block content %}
<div class="row mt-4">
<div class="col-md-3 mb-4">
<div class="card bg-light shadow">
<div class="card-header text-center" >
<h4> {% trans "Account #" %} {{ object.pk }}</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.username }}</dd>
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ object.email }}">{{ object.email }}</a></dd>
{% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %}
<dd class="col-xl-12">
<div class="alert alert-warning">
{% trans "This user doesn't have confirmed his/her e-mail address." %}
<a href="{% url "registration:email_validation_resend" pk=object.pk %}">{% trans "Click here to resend a validation link." %}</a>
</div>
</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>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.phone_number }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.paid|yesno }}</dd>
</dl>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
<a class="btn btn-danger btn-sm" href="{% url 'registration:future_user_invalidate' object.pk %}">{% trans 'Delete registration' %}</a>
</div>
</div>
</div>
<div class="col-md-9">
<div class="card bg-light shadow">
<form method="post">
<div class="card-header text-center" >
<h4> {% trans "Validate account" %}</h4>
</div>
<div class="card-body" id="profile_infos">
{% csrf_token %}
{{ form|crispy }}
</div>
<div class="card-footer text-center">
<button class="btn btn-success btn-sm">{% trans 'Validate registration' %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
soge_field = $("#id_soge");
function fillFields() {
let checked = soge_field.is(':checked');
if (!checked) {
$("input").attr('disabled', false);
$("select").attr('disabled', false);
return;
}
let credit_type = $("#id_credit_type");
credit_type.attr('disabled', true);
credit_type.val(4);
let credit_amount = $("#id_credit_amount");
credit_amount.attr('disabled', true);
credit_amount.val('{{ total_fee }}');
let bank = $("#id_bank");
bank.attr('disabled', true);
bank.val('Société générale');
let join_BDE = $("#id_join_BDE");
join_BDE.attr('disabled', true);
join_BDE.attr('checked', 'checked');
let join_Kfet = $("#id_join_Kfet");
join_Kfet.attr('disabled', true);
join_Kfet.attr('checked', 'checked');
}
soge_field.change(fillFields);
{% if object.profile.soge %}
soge_field.attr('checked', true);
fillFields();
{% endif %}
</script>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<a href="{% url 'registration:signup' %}"><button class="btn btn-primary btn-block">{% trans "New user" %}</button></a>
<hr>
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
<hr>
<div id="user_table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no pending user with this pattern." %}
</div>
{% endif %}
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#searchbar");
function reloadTable() {
let pattern = searchbar_obj.val();
if (pattern === old_pattern || pattern === "")
return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
searchbar_obj.keyup(reloadTable);
function init() {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
init();
});
</script>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% load i18n %}
{% trans "Hi" %} {{ user.username }},
{% trans "You recently registered on the Note Kfet. Please click on the link below to confirm your registration." %}
https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %}
{% trans "Thanks" %},
{% trans "The Note Kfet team." %}

View File

@ -34,7 +34,7 @@ commands =
[flake8] [flake8]
# Ignore too many errors, should be reduced in the future # Ignore too many errors, should be reduced in the future
ignore = D203, W503, E203, I100, I101 ignore = D203, W503, E203, I100, I101, C901
exclude = exclude =
.tox, .tox,
.git, .git,