# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from datetime import date from django.conf import settings from django.shortcuts import redirect from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.views.generic import DetailView, UpdateView, ListView from django.views.generic.edit import DeleteView from django.views.generic.base import TemplateView from django.utils.translation import gettext_lazy as _ from django_tables2 import SingleTableView, MultiTableMixin from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin, ProtectedCreateView from django.urls import reverse_lazy from member.views import PictureUpdateView from .models import Family, Challenge, FamilyMembership, User, Achievement from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable, FamilyAchievementTable from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView): """ Create family """ model = Family extra_context = {"title": _('Create family')} form_class = FamilyForm def get_sample_object(self): return Family( name="", description="Sample family", score=0, rank=0, ) def get_success_url(self): self.object.refresh_from_db() return reverse_lazy("family:manage") class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Families """ model = Family table_class = FamilyTable extra_context = {"title": _('Families list')} class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ Display details of a family """ model = Family context_object_name = "family" extra_context = {"title": _('Family detail')} def get_context_data(self, **kwargs): """ Add members list """ context = super().get_context_data(**kwargs) family = self.object # member list family_member = FamilyMembership.objects.filter( family=family, year=date.today().year, ).filter(PermissionBackend.filter_queryset(self.request, FamilyMembership, "view"))\ .order_by("user__username") family_member = family_member.distinct("user__username")\ if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else family_member membership_table = FamilyMembershipTable(data=family_member, prefix="membership-") membership_table.paginate(per_page=5, 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 = FamilyMembership( family=family, user=User.objects.first(), year=date.today().year, ) context["can_add_members"] = PermissionBackend()\ .has_perm(self.request.user, "family.add_membership", empty_membership) # Défis réalisé par la famille achievements = Achievement.objects.filter(family=family) achievements_table = FamilyAchievementTable(data=achievements, prefix="achievement-") achievements_table.paginate(per_page=5, page=self.request.GET.get('achievement-page', 1)) context["achievement_list"] = achievements_table return context class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Update the information of a family. """ model = Family context_object_name = "family" form_class = FamilyForm extra_context = {"title": _('Update family')} def get_success_url(self): return reverse_lazy('family:family_detail', kwargs={'pk': self.object.pk}) class FamilyPictureUpdateView(PictureUpdateView): """ Update profile picture of the family """ model = Family extra_context = {"title": _("Update family picture")} template_name = 'family/picture_update.html' def get_success_url(self): """Redirect to family page after upload""" return reverse_lazy('family:family_detail', kwargs={'pk': self.object.id}) @transaction.atomic def form_valid(self, form): """ Save the image """ image = form.cleaned_data['image'] if image is None: image = "pic/default.png" else: # Rename as PNG or GIF extension = image.name.split(".")[-1] if extension == "gif": image.name = "{}_pic.gif".format(self.object.pk) else: image.name = "{}_pic.png".format(self.object.pk) class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): """ Add a membership to a family """ model = FamilyMembership form_class = FamilyMembershipForm template_name = 'family/add_member.html' extra_context = {"title": _("Add a new member to the family")} def get_sample_object(self): if "family_pk" in self.kwargs: family = Family.objects.get(pk=self.kwargs["family_pk"]) else: family = FamilyMembership.objects.get(pk=self.kwargs["pk"]).family return FamilyMembership( user=self.request.user, family=family, year=date.today().year, ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view"))\ .get(pk=self.kwargs['family_pk']) context['family'] = family return context @transaction.atomic def form_valid(self, form): """ Create family membership, check that everythinf is good """ family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view")) \ .get(pk=self.kwargs["family_pk"]) form.instance.family = family return super().form_valid(form) def get_success_url(self): return reverse_lazy('family:family_detail', kwargs={'pk': self.object.family.id}) class ChallengeCreateView(ProtectQuerysetMixin, ProtectedCreateView): """ Create challenge """ model = Challenge extra_context = {"title": _('Create challenge')} form_class = ChallengeForm def get_sample_object(self): return Challenge( name="", description="Sample challenge", points=0, ) def get_success_url(self): return reverse_lazy('family:manage') class ChallengeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List all challenges """ model = Challenge table_class = ChallengeTable extra_context = {"title": _('Challenges list')} class ChallengeDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ Display details of a challenge """ model = Challenge context_object_name = "challenge" extra_context = {"title": _('Details of:')} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) fields = ["name", "description", "points",] fields = dict([(field, getattr(self.object, field)) for field in fields]) context["fields"] = [( Challenge._meta.get_field(field).verbose_name.capitalize(), value) for field, value in fields.items()] context["obtained"] = self.object.obtained context["update"] = PermissionBackend.check_perm(self.request, "family.change_challenge") return context class ChallengeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Update the information of a challenge """ model = Challenge context_object_name = "challenge" extra_context = {"title": _('Update challenge')} form_class = ChallengeForm def get_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk}) class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Manage families and challenges """ model = Achievement template_name = 'family/manage.html' table_class = AchievementTable extra_context = {'title': _('Manage families and challenges')} def dispatch(self, request, *args, **kwargs): # Check that the user is authenticated if not request.user.is_authenticated: return self.handle_no_permission() return super().dispatch(request, *args, **kwargs) def get_queryset(self, **kwargs): # retrieves only Transaction that user has the right to see. return Achievement.objects.filter( PermissionBackend.filter_queryset(self.request, Achievement, "view") ).order_by("-obtained_at").all() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['all_challenges'] = Challenge.objects.filter( PermissionBackend.filter_queryset(self.request, Challenge, "view") ).order_by('name') context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.add_family") context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.add_challenge") return context def get_table(self, **kwargs): table = super().get_table(**kwargs) table.exclude = ('delete', 'validate',) table.orderable = False return table class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): """ List all achievements """ model = Achievement tables = [AchievementTable, AchievementTable, ] extra_context = {'title': _('Achievement list')} def get_tables(self, **kwargs): tables = super().get_tables(**kwargs) tables[0].prefix = 'invalid-' tables[1].prefix = 'valid-' tables[1].exclude = ('validate', 'delete',) return tables def get_tables_data(self): table_valid = self.get_queryset().filter(valid=True) table_invalid = self.get_queryset().filter(valid=False) return [table_invalid, table_valid, ] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tables = context['tables'] context['invalid'] = tables[0] context['valid'] = tables[1] return context class AchievementValidateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView): """ Validate an achievement obtained by a family """ template_name = 'family/achievement_confirm_validate.html' def post(self, request, pk): # On récupère l'objet à valider achievement = Achievement.objects.get(pk=pk) # On modifie le champ valid achievement.valid = True achievement.save() # On redirige vers la page de détail ou la liste return redirect(reverse_lazy('family:achievement_list')) class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): """ Delete an Achievement """ model = Achievement def get_success_url(self): return reverse_lazy('family:achievement_list')