svthalia/concrexit

View on GitHub
website/members/views.py

Summary

Maintainability
A
1 hr
Test Coverage
"""Views provided by the members package."""
import json
from datetime import date, datetime

from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import RedirectURLMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.db.models import Q, QuerySet
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView
from django.views.generic.base import TemplateResponseMixin, TemplateView, View

import activemembers.services as activemembers_services
import events.services as event_services
import pizzas.services
from members import emails, services
from members.decorators import membership_required
from members.models import EmailChange, Member, Membership, Profile
from thaliawebsite.views import PagedView
from utils.media.services import fetch_thumbnails
from utils.snippets import datetime_to_lectureyear

from . import models
from .forms import ProfileForm
from .services import member_achievements, member_societies


@method_decorator(login_required, "dispatch")
@method_decorator(membership_required, "dispatch")
class MembersIndex(PagedView):
    """View that renders the members overview."""

    model = Member
    paginate_by = 28
    template_name = "members/index.html"
    context_object_name = "members"
    keywords = None
    query_filter = ""
    year_range = []

    def setup(self, request, *args, **kwargs) -> None:
        super().setup(request, *args, **kwargs)
        current_lectureyear = datetime_to_lectureyear(date.today())
        self.year_range = list(
            reversed(range(current_lectureyear - 5, current_lectureyear + 1))
        )
        self.keywords = request.GET.get("keywords", "").split() or None
        self.query_filter = kwargs.get("filter", None)

    def get_queryset(self) -> QuerySet:
        memberships_query = Q(until__gt=datetime.now()) | Q(until=None)
        members_query = ~Q(id=None)

        if self.query_filter and self.query_filter.isdigit():
            members_query &= Q(profile__starting_year=int(self.query_filter))
            memberships_query &= Q(type=Membership.MEMBER)
        elif self.query_filter == "older":
            members_query &= Q(profile__starting_year__lt=self.year_range[-1])
            memberships_query &= Q(type=Membership.MEMBER)
        elif self.query_filter == "former":
            # Filter out all current active memberships
            memberships_query &= Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)
            memberships = Membership.objects.filter(memberships_query)
            members_query &= ~Q(pk__in=memberships.values("user__pk"))
        # Members_query contains users that are not currently (honorary)member
        elif self.query_filter == "benefactors":
            memberships_query &= Q(type=Membership.BENEFACTOR)
        elif self.query_filter == "honorary":
            memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None)
            memberships_query &= Q(type=Membership.HONORARY)

        if self.keywords:
            for key in self.keywords:
                # Works because relevant options all have `nick` in their key
                members_query &= (
                    (
                        Q(profile__nickname__icontains=key)
                        & Q(profile__display_name_preference__contains="nick")
                    )
                    | Q(first_name__icontains=key)
                    | Q(last_name__icontains=key)
                    | Q(username__icontains=key)
                )

        if self.query_filter == "former":
            memberships_query = Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)
            memberships = Membership.objects.filter(memberships_query)
            all_memberships = Membership.objects.all()
            # Only keep members that were once members, or are legacy users
            # that do not have any memberships at all
            members_query &= Q(pk__in=memberships.values("user__pk")) | ~Q(
                pk__in=all_memberships.values("user__pk")
            )
        else:
            memberships = Membership.objects.filter(memberships_query)
            members_query &= Q(pk__in=memberships.values("user__pk"))
        members = (
            Member.objects.filter(members_query)
            .order_by("first_name")
            .select_related("profile")
        )
        return members

    def get_context_data(self, **kwargs) -> dict:
        context = super().get_context_data(**kwargs)

        context.update(
            {
                "filter": self.query_filter,
                "year_range": self.year_range,
                "keywords": self.keywords,
            }
        )

        fetch_thumbnails(
            [x.profile.photo for x in context["object_list"] if x.profile.photo]
        )

        return context


@method_decorator(login_required, "dispatch")
class ProfileDetailView(DetailView):
    """View that renders a member's profile."""

    context_object_name = "member"
    model = Member
    template_name = "members/user/profile.html"

    def setup(self, request, *args, **kwargs) -> None:
        if "pk" not in kwargs and request.member:
            kwargs["pk"] = request.member.pk
        elif not (request.member and request.member.has_active_membership()):
            raise PermissionDenied
        super().setup(request, *args, **kwargs)

    def get_context_data(self, **kwargs) -> dict:
        context = super().get_context_data(**kwargs)
        member = context["member"]

        achievements = member_achievements(member)
        societies = member_societies(member)

        membership = member.current_membership
        membership_type = _("Unknown membership history")
        if membership:
            membership_type = membership.get_type_display()
        elif member.has_been_honorary_member():
            membership_type = _("Former honorary member")
        elif member.has_been_member():
            membership_type = _("Former member")
        elif member.latest_membership:
            membership_type = _("Former benefactor")

        context.update(
            {
                "achievements": achievements,
                "societies": societies,
                "membership_type": membership_type,
            }
        )

        return context


@method_decorator(login_required, "dispatch")
class UserProfileUpdateView(RedirectURLMixin, SuccessMessageMixin, UpdateView):
    """View that allows a user to update their profile."""

    template_name = "members/user/edit_profile.html"
    model = Profile
    form_class = ProfileForm
    next_page = reverse_lazy("members:edit-profile")
    success_message = _("Your profile has been updated successfully.")

    def get_object(self, queryset=None) -> Profile:
        return get_object_or_404(models.Profile, user=self.request.user)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["require_address"] = bool(self.request.GET.get("require_address", False))
        return kwargs


@method_decorator(login_required, "dispatch")
class StatisticsView(TemplateView):
    """View that renders the statistics page."""

    template_name = "members/statistics.html"

    def get_context_data(self, **kwargs) -> dict:
        context = super().get_context_data(**kwargs)
        context.update(
            {
                "total_members": models.Member.current_members.count(),
                "cohort_sizes": json.dumps(services.gen_stats_year()),
                "member_type_distribution": json.dumps(
                    services.gen_stats_member_type()
                ),
                "committee_sizes": json.dumps(
                    activemembers_services.generate_statistics()
                ),
                "event_categories": json.dumps(
                    event_services.generate_category_statistics()
                ),
                "total_pizza_orders": json.dumps(
                    pizzas.services.gen_stats_pizza_orders()
                ),
                "active_members": json.dumps(services.gen_stats_active_members()),
            }
        )

        return context


@method_decorator(login_required, name="dispatch")
class EmailChangeFormView(CreateView):
    """View that renders the email change form."""

    model = EmailChange
    fields = ["email", "member"]
    template_name = "members/user/email_change.html"

    def get_initial(self) -> dict:
        initial = super().get_initial()
        initial["email"] = self.request.member.email
        return initial

    def post(self, request, *args, **kwargs) -> HttpResponse:
        request.POST = request.POST.dict()
        request.POST["member"] = request.member.pk
        return super().post(request, *args, **kwargs)

    def form_valid(self, form) -> HttpResponse:
        change_request = form.save()
        emails.send_email_change_confirmation_messages(change_request)
        return TemplateResponse(
            request=self.request, template="members/user/email_change_requested.html"
        )


@method_decorator(login_required, name="dispatch")
class EmailChangeConfirmView(View, TemplateResponseMixin):
    """View that renders an HTML template and confirms the old email address."""

    template_name = "members/user/email_change_confirmed.html"

    def get(self, request, *args, **kwargs) -> HttpResponse:
        if not EmailChange.objects.filter(confirm_key=kwargs["key"]).exists():
            raise Http404

        change_request = EmailChange.objects.get(confirm_key=kwargs["key"])

        services.confirm_email_change(change_request)

        return self.render_to_response({})


@method_decorator(login_required, name="dispatch")
class EmailChangeVerifyView(View, TemplateResponseMixin):
    """View that renders an HTML template and verifies the new email address."""

    template_name = "members/user/email_change_verified.html"

    def get(self, request, *args, **kwargs) -> HttpResponse:
        if not EmailChange.objects.filter(verify_key=kwargs["key"]).exists():
            raise Http404

        change_request = EmailChange.objects.get(verify_key=kwargs["key"])

        services.verify_email_change(change_request)

        return self.render_to_response({})