diff --git a/apps/activity/forms.py b/apps/activity/forms.py index e622a59d..b6cf098b 100644 --- a/apps/activity/forms.py +++ b/apps/activity/forms.py @@ -54,6 +54,15 @@ class ActivityForm(forms.ModelForm): class GuestForm(forms.ModelForm): def clean(self): + """ + Someone can be invited as a Guest to an Activity if: + - the activity has not already started. + - the activity is validated. + - the Guest has not already been invited more than 5 times. + - the Guest is already invited. + - the inviter already invited 3 peoples. + """ + cleaned_data = super().clean() if timezone.now() > timezone.localtime(self.activity.date_start): diff --git a/apps/activity/views.py b/apps/activity/views.py index a0f812d9..ab154a0e 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, TemplateView, UpdateView from django_tables2.views import SingleTableView + from note.models import Alias, NoteSpecial, NoteUser from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin, ProtectedCreateView @@ -21,6 +22,9 @@ from .tables import ActivityTable, EntryTable, GuestTable class ActivityCreateView(ProtectedCreateView): + """ + View to create a new Activity + """ model = Activity form_class = ActivityForm extra_context = {"title": _("Create new activity")} @@ -47,6 +51,9 @@ class ActivityCreateView(ProtectedCreateView): class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + Displays all Activities, and classify if they are on-going or upcoming ones. + """ model = Activity table_class = ActivityTable ordering = ('-date_start',) @@ -73,6 +80,9 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Shows details about one activity. Add guest to context + """ model = Activity context_object_name = "activity" extra_context = {"title": _("Activity detail")} @@ -90,6 +100,9 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Updates one Activity + """ model = Activity form_class = ActivityForm extra_context = {"title": _("Update activity")} @@ -99,11 +112,15 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): + """ + Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm` + """ model = Guest form_class = GuestForm template_name = "activity/activity_invite.html" def get_sample_object(self): + """ Creates a standart Guest binds to the Activity""" activity = Activity.objects.get(pk=self.kwargs["pk"]) return Guest( activity=activity, @@ -134,6 +151,9 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): class ActivityEntryView(LoginRequiredMixin, TemplateView): + """ + Manages entry to an activity + """ template_name = "activity/activity_entry.html" def dispatch(self, request, *args, **kwargs): @@ -154,14 +174,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): raise PermissionDenied(_("This activity is closed.")) return super().dispatch(request, *args, **kwargs) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ - .distinct().get(pk=self.kwargs["pk"]) - context["activity"] = activity - - matched = [] + def get_invited_guest(self,activity): + """ + Retrieves all Guests to the activity + """ guest_qs = Guest.objects\ .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ @@ -182,11 +198,13 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): else: pattern = None guest_qs = guest_qs.none() + return guest_qs - for guest in guest_qs: - guest.type = "Invité" - matched.append(guest) - + def get_invited_note(self,activity): + """ + Retrieves all Note that can attend the activity, + they need to have an up-to-date membership in the attendees_club. + """ note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"), first_name=F("note__noteuser__user__first_name"), username=F("note__noteuser__user__username"), @@ -223,8 +241,25 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page. # In production mode, please use PostgreSQL. note_qs = note_qs.distinct()[:20] + return note_qs - for note in note_qs: + def get_context_data(self, **kwargs): + """ + Query the list of Guest and Note to the activity and add information to makes entry with JS. + """ + context = super().get_context_data(**kwargs) + + activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ + .distinct().get(pk=self.kwargs["pk"]) + context["activity"] = activity + + matched=[] + + for guest in get_invited_guest(self,activity): + guest.type = "Invité" + matched.append(guest) + + for note in get_invited_note(self,activity): note.type = "Adhérent" note.activity = activity matched.append(note) diff --git a/apps/member/forms.py b/apps/member/forms.py index a2e977eb..60819db6 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -65,6 +65,18 @@ class ProfileForm(forms.ModelForm): exclude = ('user', 'email_confirmed', 'registration_valid', ) +class ImageForm(forms.Form): + """ + Form used for the js interface for profile picture + """ + image = forms.ImageField(required=False, + label=_('select an image'), + help_text=_('Maximal size: 2MB')) + x = forms.FloatField(widget=forms.HiddenInput()) + y = forms.FloatField(widget=forms.HiddenInput()) + width = forms.FloatField(widget=forms.HiddenInput()) + height = forms.FloatField(widget=forms.HiddenInput()) + class ClubForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() diff --git a/apps/member/views.py b/apps/member/views.py index 8cb384e8..52432ee6 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -3,8 +3,8 @@ import io from datetime import timedelta, date - from PIL import Image + from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.mixins import LoginRequiredMixin @@ -18,8 +18,8 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, UpdateView, TemplateView 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, SpecialTransaction from note.tables import HistoryTable, AliasTable @@ -28,7 +28,8 @@ from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin, ProtectedCreateView -from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm +from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\ + CustomAuthenticationForm, MembershipRolesForm, from .models import Club, Membership from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable @@ -49,6 +50,7 @@ class CustomLoginView(LoginView): class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Update the user information. + On this view both `:models:member.User` and `:models:member.Profile` are updated through forms """ model = User form_class = UserForm @@ -77,14 +79,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return context 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( - alias__normalized_name=Alias.normalize(new_username)) - if note.exists() and note.get().user != self.object: - form.add_error('username', - _("An alias with a similar name already exists.")) - return super().form_invalid(form) + """ + Check if ProfileForm is correct + then check if username is not already taken by someone else or by the user, + then check if email has changed, and if so ask for new validation. + """ profile_form = ProfileForm( data=self.request.POST, @@ -93,31 +92,35 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): profile_form.full_clean() if not profile_form.is_valid(): return super().form_invalid(form) - new_username = form.data['username'] + # Check if the new username is not already taken as an alias of someone else. + note = NoteUser.objects.filter( + alias__normalized_name=Alias.normalize(new_username)) + if note.exists() and note.get().user != self.object: + form.add_error('username', + _("An alias with a similar name already exists.")) + return super().form_invalid(form) + # Check if the username is one of user's aliases. alias = Alias.objects.filter(name=new_username) - # Si le nouveau pseudo n'est pas un de nos alias, - # on supprime éventuellement un alias similaire pour le remplacer if not alias.exists(): similar = Alias.objects.filter( normalized_name=Alias.normalize(new_username)) 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.save() user.profile.send_email_validation_link() + profile = profile_form.save(commit=False) + profile.user = user + profile.save() + user.save() + return super().form_valid(form) def get_success_url(self, **kwargs): @@ -127,7 +130,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ - Affiche les informations sur un utilisateur, sa note, ses clubs... + Display all information about a user. """ model = User context_object_name = "user_object" @@ -141,6 +144,9 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): return super().get_queryset().filter(profile__registration_valid=True) def get_context_data(self, **kwargs): + """ + Add history of transaction and list of membership of user. + """ context = super().get_context_data(**kwargs) user = context['user_object'] history_list = \ @@ -356,22 +362,26 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): extra_context = {"title": _("Club detail")} def get_context_data(self, **kwargs): + """ + Add list of managers (peoples with Permission/Roles in this club), history of transactions and members list + """ context = super().get_context_data(**kwargs) club = context["club"] if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): club.update_membership_dates() - + # managers list managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\ .order_by('user__last_name').all() context["managers"] = ClubManagerTable(data=managers, prefix="managers-") - + # transaction history club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ .order_by('-created_at') 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 + # member list club_member = Membership.objects.filter( club=club, date_end__gte=date.today(), @@ -469,15 +479,19 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): ) def get_context_data(self, **kwargs): + """ + Membership can be created, or renewed + In case of creation the url is /club//add_member + For a renewal it will be `club/renew_membership/` + """ context = super().get_context_data(**kwargs) form = context['form'] - if "club_pk" in self.kwargs: - # We create a new membership. + 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"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid - + # Ensure that the user is member of the parent club and all its the family tree. c = club clubs_renewal = [] additional_fee_renewal = 0 @@ -498,8 +512,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): 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. + else: # This is a renewal. Fields can be pre-completed. context["renewal"] = True old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) @@ -511,6 +524,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): additional_fee_renewal = 0 while c.parent_club is not None: c = c.parent_club + # check if a valid membership exists for the parent club if c.membership_start and not Membership.objects.filter( club=c, user=user, @@ -529,7 +543,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): 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 this is a renewal of a BDE membership, Société générale can pays, if it has not been already done. if (club.name != "BDE" and club.name != "Kfet") or user.profile.soge: del form.fields['soge'] else: @@ -559,11 +573,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): Create membership, check that all is good, make transactions """ # Get the club that is concerned by the membership - if "club_pk" in self.kwargs: + if "club_pk" in self.kwargs: # get from url of new membership club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \ .get(pk=self.kwargs["club_pk"]) user = form.instance.user - else: + else: # get from url for renewal old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) club = old_membership.club user = old_membership.user @@ -572,6 +586,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): # Get form data credit_type = form.cleaned_data["credit_type"] + # but with this way users can customize their section as they want. credit_amount = form.cleaned_data["credit_amount"] last_name = form.cleaned_data["last_name"] first_name = form.cleaned_data["first_name"] @@ -589,6 +604,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): fee = 0 c = club + # collect the fees required to be paid while c is not None and c.membership_start: if not Membership.objects.filter( club=c, @@ -632,9 +648,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): # Now, all is fine, the membership can be created. if club.name == "BDE" or club.name == "Kfet": - # When we renew the BDE membership, we update the profile section. - # We could automate that and remove the section field from the Profile model, - # but with this way users can customize their section as they want. + # When we renew the BDE membership, we update the profile section + # that should happens at least once a year. user.profile.section = user.profile.section_generated user.profile.save() diff --git a/apps/note/forms.py b/apps/note/forms.py index fe81783e..2c468706 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -10,15 +10,6 @@ from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput from .models import TransactionTemplate, NoteClub, Alias -class ImageForm(forms.Form): - image = forms.ImageField(required=False, - label=_('select an image'), - help_text=_('Maximal size: 2MB')) - x = forms.FloatField(widget=forms.HiddenInput()) - y = forms.FloatField(widget=forms.HiddenInput()) - width = forms.FloatField(widget=forms.HiddenInput()) - height = forms.FloatField(widget=forms.HiddenInput()) - class TransactionTemplateForm(forms.ModelForm): class Meta: diff --git a/apps/note/templates/note/amount_input.html b/apps/note/templates/note/amount_input.html index f4515234..43ac1687 100644 --- a/apps/note/templates/note/amount_input.html +++ b/apps/note/templates/note/amount_input.html @@ -1,12 +1,14 @@ +{# Select amount to transfert in € #}

-
\ No newline at end of file + diff --git a/apps/note/templates/note/conso_form.html b/apps/note/templates/note/conso_form.html index 29acb3de..18cac578 100644 --- a/apps/note/templates/note/conso_form.html +++ b/apps/note/templates/note/conso_form.html @@ -45,7 +45,7 @@ - + {# Summary of consumption and consume button #}
@@ -91,7 +91,6 @@
{# Regroup buttons under categories #} - {# {% regroup transaction_templates by category as categories %} #}
{# Tabs for button categories #} @@ -148,7 +147,7 @@
- + {# history of transaction #}

diff --git a/apps/note/templates/note/transaction_form.html b/apps/note/templates/note/transaction_form.html index 4715e79f..aec1f2e2 100644 --- a/apps/note/templates/note/transaction_form.html +++ b/apps/note/templates/note/transaction_form.html @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later {% load i18n static django_tables2 perms %} {% block content %} - +{# bandeau transfert/crédit/débit/activité #}

@@ -34,8 +34,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
-
+ {# Preview note profile (picture, username and balance) #} - + {# list of emitters #}
@@ -66,7 +66,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
- +{# list of receiver #}
@@ -83,7 +83,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
- +{# Information on transaction (amount, reason, name,...) #}
@@ -108,7 +108,7 @@ SPDX-License-Identifier: GPL-2.0-or-later

- + {# in case of special transaction add identity information #}
@@ -149,7 +149,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
- +{# transaction history #}

diff --git a/apps/note/templates/note/transactiontemplate_list.html b/apps/note/templates/note/transactiontemplate_list.html index 91b34728..76b116e9 100644 --- a/apps/note/templates/note/transactiontemplate_list.html +++ b/apps/note/templates/note/transactiontemplate_list.html @@ -5,6 +5,7 @@ {% block content %}

+ {# Search field , see js #}
{% trans "New button" %} diff --git a/apps/note/views.py b/apps/note/views.py index 0312f11f..9b383b1f 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -29,21 +29,19 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial` """ template_name = "note/transaction_form.html" - + # SingleTableView creates `context["table"]` we will load it with transaction history model = Transaction # Transaction history table table_class = HistoryTable extra_context = {"title": _("Transfer money")} def get_queryset(self, **kwargs): + # retrieves only Transaction that user has the right to see. return Transaction.objects.filter( PermissionBackend.filter_queryset(self.request.user, Transaction, "view") ).order_by("-created_at").all()[:20] def get_context_data(self, **kwargs): - """ - Add some context variables in template such as page title - """ context = super().get_context_data(**kwargs) context['amount_widget'] = AmountInput(attrs={"id": "amount"}) context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk @@ -146,7 +144,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ The Magic View that make people pay their beer and burgers. - (Most of the magic happens in the dark world of Javascript see consos.js) + (Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`) """ model = Transaction template_name = "note/conso_form.html" @@ -168,29 +166,30 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): return super().dispatch(request, *args, **kwargs) def get_queryset(self, **kwargs): + """ + restrict to the transaction history the user can see. + """ return Transaction.objects.filter( PermissionBackend.filter_queryset(self.request.user, Transaction, "view") ).order_by("-created_at").all()[:20] def get_context_data(self, **kwargs): - """ - Add some context variables in template such as page title - """ context = super().get_context_data(**kwargs) + categories = TemplateCategory.objects.order_by('name').all() + # for each category, find which transaction templates the user can see. for category in categories: category.templates_filtered = category.templates.filter( PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view") ).filter(display=True).order_by('name').all() + context['categories'] = [cat for cat in categories if cat.templates_filtered] + # some transactiontemplate are put forward to find them easily context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter( PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view") ).order_by('name').all() context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk - # select2 compatibility - context['no_cache'] = True - return context diff --git a/apps/registration/views.py b/apps/registration/views.py index 42d9ffc7..bf68a8ed 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -29,7 +29,7 @@ from .tokens import email_validation_token class UserCreateView(CreateView): """ - Une vue pour inscrire un utilisateur et lui créer un profil + A view to create a User and add a Profile """ form_class = SignUpForm diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 480ed290..c2265289 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -57,7 +57,6 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView): form_set = ProductFormSet(instance=form.instance) context['formset'] = form_set context['helper'] = ProductFormSetHelper() - context['no_cache'] = True return context @@ -125,7 +124,6 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form_set = ProductFormSet(instance=self.object) context['formset'] = form_set context['helper'] = ProductFormSetHelper() - context['no_cache'] = True if self.object.locked: for field_name in form.fields: diff --git a/note_kfet/templates/base.html b/note_kfet/templates/base.html index 671661e3..52c48b1b 100644 --- a/note_kfet/templates/base.html +++ b/note_kfet/templates/base.html @@ -22,12 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later - - {# Disable turbolink cache for some pages #} - {% if no_cache %} - - {% endif %} - {# Bootstrap CSS #}