mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 23:54:30 +01:00 
			
		
		
		
	Merge branch 'master' into 'fix_distinct'
# Conflicts: # apps/activity/views.py
This commit is contained in:
		
							
								
								
									
										20
									
								
								apps/activity/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/activity/fixtures/initial.json
									
									
									
									
									
										Normal 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 | ||||
|     } | ||||
|   } | ||||
| ] | ||||
| @@ -130,6 +130,8 @@ class Entry(models.Model): | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = (('activity', 'note', 'guest', ), ) | ||||
|         verbose_name = _("entry") | ||||
|         verbose_name_plural = _("entries") | ||||
|  | ||||
|     def save(self, force_insert=False, force_update=False, using=None, | ||||
|              update_fields=None): | ||||
|   | ||||
| @@ -154,4 +154,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): | ||||
|         ctx["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk | ||||
|         ctx["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk | ||||
|  | ||||
|         ctx["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 ctx | ||||
|   | ||||
| @@ -75,3 +75,7 @@ class Changelog(models.Model): | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         raise ValidationError(_("Logs cannot be destroyed.")) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("changelog") | ||||
|         verbose_name_plural = _("changelogs") | ||||
|   | ||||
| @@ -2,8 +2,10 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| 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.utils.translation import gettext_lazy as _ | ||||
| from note.models import NoteSpecial | ||||
| from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput | ||||
| 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): | ||||
|     """ | ||||
|     A form for the extras field provided by the :model:`member.Profile` model. | ||||
| @@ -37,7 +28,7 @@ class ProfileForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = Profile | ||||
|         fields = '__all__' | ||||
|         exclude = ['user'] | ||||
|         exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', ) | ||||
|  | ||||
|  | ||||
| class ClubForm(forms.ModelForm): | ||||
| @@ -59,6 +50,42 @@ class ClubForm(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: | ||||
|         model = Membership | ||||
|         fields = ('user', 'roles', 'date_start') | ||||
|   | ||||
| @@ -2,13 +2,18 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import datetime | ||||
| import os | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.template import loader | ||||
| 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 registration.tokens import email_validation_token | ||||
| from note.models import MembershipTransaction | ||||
|  | ||||
|  | ||||
| @@ -45,6 +50,23 @@ class Profile(models.Model): | ||||
|     ) | ||||
|     paid = models.BooleanField( | ||||
|         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, | ||||
|     ) | ||||
|  | ||||
| @@ -56,6 +78,17 @@ class Profile(models.Model): | ||||
|     def get_absolute_url(self): | ||||
|         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): | ||||
|     """ | ||||
| @@ -202,6 +235,7 @@ class Membership(models.Model): | ||||
|     ) | ||||
|  | ||||
|     date_start = models.DateField( | ||||
|         default=datetime.date.today, | ||||
|         verbose_name=_('membership starts on'), | ||||
|     ) | ||||
|  | ||||
| @@ -215,12 +249,18 @@ class Membership(models.Model): | ||||
|     ) | ||||
|  | ||||
|     def valid(self): | ||||
|         """ | ||||
|         A membership is valid if today is between the start and the end date. | ||||
|         """ | ||||
|         if self.date_end is not None: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal() | ||||
|         else: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() | ||||
|  | ||||
|     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 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) | ||||
| @@ -252,6 +292,9 @@ class Membership(models.Model): | ||||
|         self.make_transaction() | ||||
|  | ||||
|     def make_transaction(self): | ||||
|         """ | ||||
|         Create Membership transaction associated to this membership. | ||||
|         """ | ||||
|         if not self.fee or MembershipTransaction.objects.filter(membership=self).exists(): | ||||
|             return | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ def save_user_profile(instance, created, raw, **_kwargs): | ||||
|         # When provisionning data, do not try to autocreate | ||||
|         return | ||||
|  | ||||
|     if created: | ||||
|     if created and instance.is_active: | ||||
|         from .models import Profile | ||||
|         Profile.objects.get_or_create(user=instance) | ||||
|     instance.profile.save() | ||||
|         instance.profile.save() | ||||
|   | ||||
| @@ -15,6 +15,9 @@ from .models import Club, Membership | ||||
|  | ||||
|  | ||||
| class ClubTable(tables.Table): | ||||
|     """ | ||||
|     List all clubs. | ||||
|     """ | ||||
|     class Meta: | ||||
|         attrs = { | ||||
|             'class': 'table table-condensed table-striped table-hover' | ||||
| @@ -30,6 +33,9 @@ class ClubTable(tables.Table): | ||||
|  | ||||
|  | ||||
| class UserTable(tables.Table): | ||||
|     """ | ||||
|     List all users. | ||||
|     """ | ||||
|     section = tables.Column(accessor='profile.section') | ||||
|  | ||||
|     balance = tables.Column(accessor='note.balance', verbose_name=_("Balance")) | ||||
| @@ -51,6 +57,9 @@ class UserTable(tables.Table): | ||||
|  | ||||
|  | ||||
| class MembershipTable(tables.Table): | ||||
|     """ | ||||
|     List all memberships. | ||||
|     """ | ||||
|     roles = tables.Column( | ||||
|         attrs={ | ||||
|             "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): | ||||
|         # If the user has the right, link the displayed club with the page of its detail. | ||||
|         s = value.name | ||||
|         if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value): | ||||
|             s = format_html("<a href={url}>{name}</a>", | ||||
| @@ -94,6 +113,7 @@ class MembershipTable(tables.Table): | ||||
|         return t | ||||
|  | ||||
|     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() | ||||
|         s = ", ".join(str(role) for role in roles) | ||||
|         if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record): | ||||
|   | ||||
| @@ -7,14 +7,12 @@ from . import views | ||||
|  | ||||
| app_name = 'member' | ||||
| urlpatterns = [ | ||||
|     path('signup/', views.UserCreateView.as_view(), name="signup"), | ||||
|  | ||||
|     path('club/', views.ClubListView.as_view(), name="club_list"), | ||||
|     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>/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/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_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), | ||||
|     path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"), | ||||
|   | ||||
| @@ -9,30 +9,30 @@ from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.auth.views import LoginView | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db.models import Q | ||||
| from django.forms import HiddenInput | ||||
| from django.shortcuts import redirect | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| 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_tables2.views import SingleTableView | ||||
| from rest_framework.authtoken.models import Token | ||||
| from note.forms import ImageForm | ||||
| from note.models import Alias, NoteUser | ||||
| from note.models.transactions import Transaction | ||||
| from note.models import Alias, NoteUser, NoteSpecial | ||||
| from note.models.transactions import Transaction, SpecialTransaction | ||||
| from note.tables import HistoryTable, AliasTable | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
|  | ||||
| from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm | ||||
| from .models import Club, Membership | ||||
| from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm | ||||
| from .models import Club, Membership, Role | ||||
| from .tables import ClubTable, UserTable, MembershipTable | ||||
|  | ||||
|  | ||||
| class CustomLoginView(LoginView): | ||||
|     """ | ||||
|     Login view, where the user can select its permission mask. | ||||
|     """ | ||||
|     form_class = CustomAuthenticationForm | ||||
|  | ||||
|     def form_valid(self, form): | ||||
| @@ -40,33 +40,10 @@ class CustomLoginView(LoginView): | ||||
|         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): | ||||
|     """ | ||||
|     Update the user information. | ||||
|     """ | ||||
|     model = User | ||||
|     fields = ['first_name', 'last_name', 'username', 'email'] | ||||
|     template_name = 'member/profile_update.html' | ||||
| @@ -75,14 +52,20 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|  | ||||
|     def get_context_data(self, **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['title'] = _("Update Profile") | ||||
|         return context | ||||
|  | ||||
|     def get_form(self, form_class=None): | ||||
|         form = super().get_form(form_class) | ||||
|         if 'username' not in form.data: | ||||
|             return form | ||||
|     def form_valid(self, form): | ||||
|         new_username = form.data['username'] | ||||
|         # Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant | ||||
|         note = NoteUser.objects.filter( | ||||
| @@ -90,9 +73,8 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|         if note.exists() and note.get().user != self.object: | ||||
|             form.add_error('username', | ||||
|                            _("An alias with a similar name already exists.")) | ||||
|         return form | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         profile_form = ProfileForm( | ||||
|             data=self.request.POST, | ||||
|             instance=self.object.profile, | ||||
| @@ -108,19 +90,24 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|                 if similar.exists(): | ||||
|                     similar.delete() | ||||
|  | ||||
|             olduser = User.objects.get(pk=form.instance.pk) | ||||
|  | ||||
|             user = form.save(commit=False) | ||||
|             profile = profile_form.save(commit=False) | ||||
|             profile.user = user | ||||
|             profile.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) | ||||
|  | ||||
|     def get_success_url(self, **kwargs): | ||||
|         if kwargs: | ||||
|             return reverse_lazy('member:user_detail', | ||||
|                                 kwargs={'pk': kwargs['id']}) | ||||
|         else: | ||||
|             return reverse_lazy('member:user_detail', args=(self.object.id,)) | ||||
|         url = 'member:user_detail' if self.object.profile.registration_valid else 'registration:future_user_detail' | ||||
|         return reverse_lazy(url, args=(self.object.id,)) | ||||
|  | ||||
|  | ||||
| class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
| @@ -131,29 +118,43 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     context_object_name = "user_object" | ||||
|     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): | ||||
|         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")\ | ||||
|             .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())\ | ||||
|             .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 | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     table_class = UserTable | ||||
|     template_name = 'member/user_list.html' | ||||
|  | ||||
|     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: | ||||
|             pattern = self.request.GET["search"] | ||||
|  | ||||
| @@ -164,6 +165,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(note__alias__name__iregex="^" + pattern) | ||||
|                 | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) | ||||
|             ) | ||||
| @@ -181,6 +183,9 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|  | ||||
|  | ||||
| class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     """ | ||||
|     View and manage user aliases. | ||||
|     """ | ||||
|     model = User | ||||
|     template_name = 'member/profile_alias.html' | ||||
|     context_object_name = 'user_object' | ||||
| @@ -193,6 +198,9 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): | ||||
|     """ | ||||
|     Update profile picture of the user note. | ||||
|     """ | ||||
|     form_class = ImageForm | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
| @@ -292,6 +300,9 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|  | ||||
|  | ||||
| class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     """ | ||||
|     Display details of a club | ||||
|     """ | ||||
|     model = 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))\ | ||||
|             .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=club, | ||||
|             date_end__gte=datetime.today(), | ||||
|         ).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( | ||||
|             club=club, | ||||
|             user=User.objects.first(), | ||||
| @@ -326,6 +342,9 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     """ | ||||
|     Manage aliases of a club. | ||||
|     """ | ||||
|     model = Club | ||||
|     template_name = 'member/club_alias.html' | ||||
|     context_object_name = 'club' | ||||
| @@ -338,6 +357,9 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|  | ||||
|  | ||||
| class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Update the information of a club. | ||||
|     """ | ||||
|     model = Club | ||||
|     context_object_name = "club" | ||||
|     form_class = ClubForm | ||||
| @@ -348,6 +370,9 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|  | ||||
|  | ||||
| class ClubPictureUpdateView(PictureUpdateView): | ||||
|     """ | ||||
|     Update the profile picture of a club. | ||||
|     """ | ||||
|     model = Club | ||||
|     template_name = 'member/club_picture_update.html' | ||||
|     context_object_name = 'club' | ||||
| @@ -357,29 +382,107 @@ class ClubPictureUpdateView(PictureUpdateView): | ||||
|  | ||||
|  | ||||
| class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Add a membership to a club. | ||||
|     """ | ||||
|     model = Membership | ||||
|     form_class = MembershipForm | ||||
|     template_name = 'member/add_members.html' | ||||
|  | ||||
|     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) | ||||
|         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 | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         user = self.request.user | ||||
|         """ | ||||
|         Create membership, check that all is good, make transactions | ||||
|         """ | ||||
|         # 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 | ||||
|  | ||||
|         # 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: | ||||
|             fee = club.membership_fee_paid | ||||
|         else: | ||||
|             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", | ||||
|                 user=user, | ||||
|                 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 | ||||
|             form.add_error('user', | ||||
|                            _("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 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')) | ||||
|             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}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             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}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             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) | ||||
|  | ||||
|     def get_success_url(self): | ||||
| @@ -422,6 +580,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|  | ||||
|  | ||||
| class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Manage the roles of a user in a club | ||||
|     """ | ||||
|     model = Membership | ||||
|     form_class = MembershipForm | ||||
|     template_name = 'member/add_members.html' | ||||
| @@ -430,49 +591,19 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         club = self.object.club | ||||
|         context['club'] = club | ||||
|         form = context['form'] | ||||
|         form.fields['user'].disabled = True | ||||
|         form.fields['date_start'].widget = HiddenInput() | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start: | ||||
|             form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end: | ||||
|             form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         return super().form_valid(form) | ||||
|     def get_form(self, form_class=None): | ||||
|         form = super().get_form(form_class) | ||||
|         # We don't create a full membership, we only update one field | ||||
|         form.fields['user'].disabled = True | ||||
|         del form.fields['date_start'] | ||||
|         del form.fields['credit_type'] | ||||
|         del form.fields['credit_amount'] | ||||
|         del form.fields['last_name'] | ||||
|         del form.fields['first_name'] | ||||
|         del form.fields['bank'] | ||||
|         return form | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         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})) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ | ||||
|  | ||||
| from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser | ||||
| from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ | ||||
|     RecurrentTransaction, MembershipTransaction | ||||
|     RecurrentTransaction, MembershipTransaction, SpecialTransaction | ||||
|  | ||||
|  | ||||
| class AliasInlines(admin.TabularInline): | ||||
| @@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for Transaction | ||||
|     """ | ||||
|     child_models = (RecurrentTransaction, MembershipTransaction) | ||||
|     child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction) | ||||
|     list_display = ('created_at', 'poly_source', 'poly_destination', | ||||
|                     'quantity', 'amount', 'valid') | ||||
|     list_filter = ('valid',) | ||||
| @@ -141,7 +141,14 @@ class TransactionAdmin(PolymorphicParentModelAdmin): | ||||
| @admin.register(MembershipTransaction) | ||||
| class MembershipTransactionAdmin(PolymorphicChildModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for Transaction | ||||
|     Admin customisation for MembershipTransaction | ||||
|     """ | ||||
|  | ||||
|  | ||||
| @admin.register(SpecialTransaction) | ||||
| class SpecialTransactionAdmin(PolymorphicChildModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for SpecialTransaction | ||||
|     """ | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| # 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 | ||||
|     """ | ||||
| @@ -10,10 +10,11 @@ def save_user_note(instance, created, raw, **_kwargs): | ||||
|         # When provisionning data, do not try to autocreate | ||||
|         return | ||||
|  | ||||
|     if created: | ||||
|         from .models import NoteUser | ||||
|         NoteUser.objects.create(user=instance) | ||||
|     instance.note.save() | ||||
|     if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active: | ||||
|         # 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): | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # 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.contenttypes.models import ContentType | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| @@ -29,7 +30,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl | ||||
|     table_class = HistoryTable | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
| @@ -44,12 +45,19 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\ | ||||
|             .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 | ||||
|  | ||||
|  | ||||
| class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Create TransactionTemplate | ||||
|     Create Transaction template | ||||
|     """ | ||||
|     model = TransactionTemplate | ||||
|     form_class = TransactionTemplateForm | ||||
| @@ -58,7 +66,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr | ||||
|  | ||||
| class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     List TransactionsTemplates | ||||
|     List Transaction templates | ||||
|     """ | ||||
|     model = TransactionTemplate | ||||
|     table_class = ButtonTable | ||||
| @@ -66,6 +74,7 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing | ||||
|  | ||||
| class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Update Transaction template | ||||
|     """ | ||||
|     model = TransactionTemplate | ||||
|     form_class = TransactionTemplateForm | ||||
| @@ -84,7 +93,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     table_class = HistoryTable | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -106,6 +106,10 @@ class PermissionMask(models.Model): | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("permission mask") | ||||
|         verbose_name_plural = _("permission masks") | ||||
|  | ||||
|  | ||||
| class Permission(models.Model): | ||||
|  | ||||
| @@ -153,6 +157,8 @@ class Permission(models.Model): | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = ('model', 'query', 'type', 'field') | ||||
|         verbose_name = _("permission") | ||||
|         verbose_name_plural = _("permissions") | ||||
|  | ||||
|     def clean(self): | ||||
|         self.query = json.dumps(json.loads(self.query)) | ||||
| @@ -293,3 +299,7 @@ class RolePermissions(models.Model): | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.role) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("role permissions") | ||||
|         verbose_name_plural = _("role permissions") | ||||
|   | ||||
							
								
								
									
										4
									
								
								apps/registration/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/registration/__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 = 'registration.apps.RegistrationConfig' | ||||
							
								
								
									
										10
									
								
								apps/registration/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								apps/registration/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 RegistrationConfig(AppConfig): | ||||
|     name = 'registration' | ||||
|     verbose_name = _('registration') | ||||
							
								
								
									
										80
									
								
								apps/registration/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								apps/registration/forms.py
									
									
									
									
									
										Normal 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, | ||||
|     ) | ||||
							
								
								
									
										0
									
								
								apps/registration/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/registration/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										26
									
								
								apps/registration/tables.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								apps/registration/tables.py
									
									
									
									
									
										Normal 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 | ||||
|         } | ||||
							
								
								
									
										30
									
								
								apps/registration/tokens.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/registration/tokens.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										18
									
								
								apps/registration/urls.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										358
									
								
								apps/registration/views.py
									
									
									
									
									
										Normal 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') | ||||
| @@ -53,7 +53,7 @@ ProductFormSet = forms.inlineformset_factory( | ||||
|  | ||||
| class ProductFormSetHelper(FormHelper): | ||||
|     """ | ||||
|     Specify some template informations for the product form. | ||||
|     Specify some template information for the product form. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, form=None): | ||||
|   | ||||
| @@ -59,6 +59,10 @@ class Invoice(models.Model): | ||||
|         verbose_name=_("Acquitted"), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("invoice") | ||||
|         verbose_name_plural = _("invoices") | ||||
|  | ||||
|  | ||||
| class Product(models.Model): | ||||
|     """ | ||||
| @@ -95,6 +99,10 @@ class Product(models.Model): | ||||
|     def total_euros(self): | ||||
|         return self.total / 100 | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("product") | ||||
|         verbose_name_plural = _("products") | ||||
|  | ||||
|  | ||||
| class RemittanceType(models.Model): | ||||
|     """ | ||||
| @@ -109,6 +117,10 @@ class RemittanceType(models.Model): | ||||
|     def __str__(self): | ||||
|         return str(self.note) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("remittance type") | ||||
|         verbose_name_plural = _("remittance types") | ||||
|  | ||||
|  | ||||
| class Remittance(models.Model): | ||||
|     """ | ||||
| @@ -136,6 +148,10 @@ class Remittance(models.Model): | ||||
|         verbose_name=_("Closed"), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("remittance") | ||||
|         verbose_name_plural = _("remittances") | ||||
|  | ||||
|     @property | ||||
|     def transactions(self): | ||||
|         """ | ||||
| @@ -187,3 +203,7 @@ class SpecialTransactionProxy(models.Model): | ||||
|         null=True, | ||||
|         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
											
										
									
								
							| @@ -13,7 +13,7 @@ class AmountInput(NumberInput): | ||||
|     template_name = "note/amount_input.html" | ||||
|  | ||||
|     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): | ||||
|         val = super().value_from_datadict(data, files, name) | ||||
|   | ||||
| @@ -54,13 +54,14 @@ INSTALLED_APPS = [ | ||||
|     'rest_framework.authtoken', | ||||
|  | ||||
|     # Note apps | ||||
|     'api', | ||||
|     'activity', | ||||
|     'logs', | ||||
|     'member', | ||||
|     'note', | ||||
|     'treasury', | ||||
|     'permission', | ||||
|     'api', | ||||
|     'logs', | ||||
|     'registration', | ||||
|     'treasury', | ||||
| ] | ||||
| LOGIN_REDIRECT_URL = '/note/transfer/' | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,7 @@ urlpatterns = [ | ||||
|     # Include project routers | ||||
|     path('note/', include('note.urls')), | ||||
|     path('accounts/', include('member.urls')), | ||||
|     path('registration/', include('registration.urls')), | ||||
|     path('activity/', include('activity.urls')), | ||||
|     path('treasury/', include('treasury.urls')), | ||||
|  | ||||
| @@ -37,14 +38,7 @@ if "cas_server" in settings.INSTALLED_APPS: | ||||
|         # Include CAS Server routers | ||||
|         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: | ||||
|     import debug_toolbar | ||||
|     urlpatterns = [ | ||||
|   | ||||
| @@ -24,6 +24,9 @@ $(document).ready(function () { | ||||
|                 $("#" + prefix + "_" + obj.id).click(function() { | ||||
|                     target.val(obj[name_field]); | ||||
|                     $("#" + prefix + "_pk").val(obj.id); | ||||
|  | ||||
|                     if (typeof autocompleted != 'undefined') | ||||
|                         autocompleted(obj, prefix) | ||||
|                 }); | ||||
|  | ||||
|                 if (input === obj[name_field]) | ||||
|   | ||||
| @@ -19,23 +19,32 @@ function pretty_money(value) { | ||||
|  * Add a message on the top of the page. | ||||
|  * @param msg The message to display | ||||
|  * @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 html = msgDiv.html(); | ||||
|     let id = Math.floor(10000 * Math.random() + 1); | ||||
|     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"; | ||||
|     msgDiv.html(html); | ||||
|  | ||||
|     if (timeout > 0) { | ||||
|         setTimeout(function () { | ||||
|             $("#close-message-" + id).click(); | ||||
|         }, timeout); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * add Muliple error message from err_obj | ||||
|  * @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)) { | ||||
|               addMsg(err_msg,'danger'); | ||||
|               addMsg(err_msg,'danger', timeout); | ||||
|           } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -61,16 +61,24 @@ $(document).ready(function() { | ||||
|  | ||||
|  | ||||
|     // 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_credit").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_credit']").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')) { | ||||
|         dests_notes_display.forEach(function (dest) { | ||||
|             $.post("/api/note/transaction/transaction/", | ||||
|   | ||||
| @@ -118,7 +118,6 @@ | ||||
|     }); | ||||
|  | ||||
|     $("#validate_activity").click(function () { | ||||
|         console.log(42); | ||||
|         $.ajax({ | ||||
|             url: "/api/activity/activity/{{ activity.pk }}/", | ||||
|             type: "PATCH", | ||||
|   | ||||
| @@ -6,6 +6,26 @@ | ||||
| {% load perms %} | ||||
|  | ||||
| {% 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 %}"> | ||||
|         <button class="btn btn-light">{% trans "Return to activity page" %}</button> | ||||
|     </a> | ||||
| @@ -56,10 +76,10 @@ | ||||
|                         note: id, | ||||
|                         guest: null | ||||
|                     }).done(function () { | ||||
|                         addMsg("Entrée effectuée !", "success"); | ||||
|                         addMsg("Entrée effectuée !", "success", 4000); | ||||
|                         reloadTable(true); | ||||
|                     }).fail(function(xhr) { | ||||
|                         errMsg(xhr.responseJSON); | ||||
|                         errMsg(xhr.responseJSON, 4000); | ||||
|                     }); | ||||
|                 } | ||||
|                 else { | ||||
| @@ -84,10 +104,10 @@ | ||||
|                             note: target.attr("data-inviter"), | ||||
|                             guest: id | ||||
|                         }).done(function () { | ||||
|                             addMsg("Entrée effectuée !", "success"); | ||||
|                             addMsg("Entrée effectuée !", "success", 4000); | ||||
|                             reloadTable(true); | ||||
|                         }).fail(function (xhr) { | ||||
|                             errMsg(xhr.responseJSON); | ||||
|                             errMsg(xhr.responseJSON, 4000); | ||||
|                         }); | ||||
|                     }; | ||||
|  | ||||
| @@ -111,7 +131,7 @@ | ||||
|                                 makeTransaction(); | ||||
|                                 reset(); | ||||
|                             }).fail(function (xhr) { | ||||
|                                 errMsg(xhr.responseJSON); | ||||
|                                 errMsg(xhr.responseJSON, 4000); | ||||
|                             }); | ||||
|                         }; | ||||
|                     }; | ||||
|   | ||||
| @@ -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> | ||||
|                     </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" %} | ||||
|                         </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> | ||||
| @@ -124,7 +131,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                     </li> | ||||
|                 {% else %} | ||||
|                     <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 | ||||
|                         </a> | ||||
|                     </li> | ||||
| @@ -138,6 +145,11 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         </div> | ||||
|     </nav> | ||||
|     <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 %} | ||||
|         <div id="messages"></div> | ||||
|         {% block content %} | ||||
|   | ||||
| @@ -16,6 +16,40 @@ | ||||
| {% endblock %} | ||||
|  | ||||
| {% 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 %} | ||||
|   | ||||
| @@ -10,9 +10,11 @@ | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script> | ||||
|     function refreshHistory() { | ||||
|         $("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list"); | ||||
|         $("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos"); | ||||
|     } | ||||
|         function refreshHistory() { | ||||
|             $("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list"); | ||||
|             $("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos"); | ||||
|         } | ||||
|  | ||||
|         window.history.replaceState({}, document.title, location.pathname); | ||||
|     </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -49,13 +49,13 @@ | ||||
|     </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' 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 %} | ||||
|         {% 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.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> | ||||
|         {% endif %}    </div> | ||||
| </div> | ||||
|   | ||||
| @@ -36,7 +36,6 @@ function getInfo() { | ||||
|     if (asked.length >= 1) { | ||||
|         $.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){ | ||||
|             let selected_id = buttons.results.map((a => "#row-"+a.id)); | ||||
|             console.log(selected_id.join()); | ||||
|             $(".table-row,"+selected_id.join()).show(); | ||||
|             $(".table-row").not(selected_id.join()).hide(); | ||||
|              | ||||
|   | ||||
| @@ -1,31 +1,23 @@ | ||||
| {% load render_table from django_tables2 %} | ||||
| {% load i18n %} | ||||
| <div class="accordion shadow" id="accordionProfile"> | ||||
|     <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 "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 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> | ||||
|         <div id="history_list"> | ||||
|             {% render_table history_list %} | ||||
|         </div> | ||||
| </div> | ||||
|   | ||||
| @@ -7,3 +7,14 @@ | ||||
| {% block profile_content %} | ||||
| {% include "member/profile_tables.html" %} | ||||
| {% 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 %} | ||||
|   | ||||
| @@ -44,7 +44,7 @@ | ||||
|     <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> | ||||
|         {% 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> | ||||
|         {% endif %} | ||||
|     </div> | ||||
|   | ||||
| @@ -1,31 +1,34 @@ | ||||
| {% load render_table from django_tables2 %} | ||||
| {% load i18n %} | ||||
| <div class="accordion shadow" id="accordionProfile"> | ||||
|     <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> | ||||
| {% load perms %} | ||||
|  | ||||
|     <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> | ||||
| {% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %} | ||||
|     <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> | ||||
| {% endif %} | ||||
|  | ||||
| <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 "View my memberships" %} | ||||
|         </a> | ||||
|     </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> | ||||
|   | ||||
| @@ -7,7 +7,13 @@ | ||||
|     <hr> | ||||
|  | ||||
|     <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> | ||||
|  | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -28,6 +28,11 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|                         {% trans "Debit" %} | ||||
|                     </label> | ||||
|                 {% 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> | ||||
| @@ -137,7 +142,7 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
|     <div class="form-row"> | ||||
|         <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> | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,6 @@ function getInfo() { | ||||
|     if (asked.length >= 1) { | ||||
|         $.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){ | ||||
|             let selected_id = buttons.results.map((a => "#row-"+a.id)); | ||||
|             console.log(selected_id.join()); | ||||
|             $(".table-row,"+selected_id.join()).show(); | ||||
|             $(".table-row").not(selected_id.join()).hide(); | ||||
|              | ||||
|   | ||||
							
								
								
									
										15
									
								
								templates/registration/email_validation_complete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/registration/email_validation_complete.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										7
									
								
								templates/registration/email_validation_email_sent.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/registration/email_validation_email_sent.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										119
									
								
								templates/registration/future_profile_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								templates/registration/future_profile_detail.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										53
									
								
								templates/registration/future_user_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								templates/registration/future_user_list.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										15
									
								
								templates/registration/mails/email_validation_email.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/registration/mails/email_validation_email.html
									
									
									
									
									
										Normal 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." %} | ||||
		Reference in New Issue
	
	Block a user