views.py 11.3 KB
Newer Older
1
"""Views provided by the members package"""
2
import json
3
from datetime import date, datetime
4 5 6 7 8 9

from django.contrib.auth.decorators import login_required
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Q, QuerySet
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
10
from django.template.response import TemplateResponse
11
from django.urls import reverse_lazy
12
from django.utils.decorators import method_decorator
13 14 15 16
from django.utils.translation import gettext_lazy as _
from django.views.generic import (ListView, DetailView, UpdateView,
                                  CreateView)
from django.views.generic.base import TemplateResponseMixin, View, TemplateView
17
from rest_framework.authtoken.models import Token
18
from rest_framework.authtoken.views import ObtainAuthToken
19
from rest_framework.response import Response
20

21
import pizzas.services
22
from members import services, emails
23 24
from members.decorators import membership_required
from members.models import EmailChange, Membership, Member, Profile
25
from utils.snippets import datetime_to_lectureyear
26
from . import models
27
from .forms import ProfileForm
28
from .services import member_achievements
29
from .services import member_societies
30 31
import events.services as event_services
import activemembers.services as activemembers_services
32

33

34
class ObtainThaliaAuthToken(ObtainAuthToken):
35 36 37
    """
    Custom override of the AuthToken view to force lowercase the username
    """
38

39
    def post(self, request, *args, **kwargs) -> HttpResponse:
40 41 42 43 44 45 46 47 48
        serializer = self.serializer_class(data={
            'username': request.data.get('username').lower()
            if 'username' in request.data else None,
            'password': request.data.get('password')
        }, context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})
49 50


51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
@method_decorator(login_required, 'dispatch')
@method_decorator(membership_required, 'dispatch')
class MembersIndex(ListView):
    """
    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'))
89
        # Members_query contains users that are not currently (honorary)member
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        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')))
116
        else:
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
            memberships = Membership.objects.filter(memberships_query)
            members_query &= Q(pk__in=memberships.values('user__pk'))
        return Member.objects.filter(members_query).order_by('first_name')

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

        page = context['page_obj'].number
        paginator = context['paginator']

        page_range = range(1, paginator.num_pages + 1)
        if paginator.num_pages > 7:
            if page > 3:
                page_range_end = paginator.num_pages
                if page + 3 <= paginator.num_pages:
                    page_range_end = page + 3

                page_range = range(page - 2, page_range_end)
                while page_range.stop - page_range.start < 5:
                    page_range = range(page_range.start - 1, page_range.stop)
            else:
                page_range = range(1, 6)

        context.update({
            'filter': self.query_filter,
            'page_range': page_range,
            'year_range': self.year_range,
            'keywords': self.keywords
        })
146

147
        return context
148 149


150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
@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:
            kwargs['pk'] = request.member.pk
        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,
        })
187

188
        return context
189

190

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
@method_decorator(login_required, 'dispatch')
class UserAccountView(TemplateView):
    """
    View that renders the account options page
    """
    template_name = 'members/user/index.html'


@method_decorator(login_required, 'dispatch')
class UserProfileUpdateView(SuccessMessageMixin, UpdateView):
    """
    View that allows a user to update their profile
    """
    template_name = 'members/user/edit_profile.html'
    model = Profile
    form_class = ProfileForm
    success_url = reverse_lazy('members:edit-profile')
    success_message = _('Your profile has been updated successfully.')
209

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    def get_object(self, queryset=None) -> Profile:
        return get_object_or_404(models.Profile, user=self.request.user)


@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)

        total = models.Member.current_members.count()

        context.update({
            "total_members": total,
            "statistics": json.dumps({
229 230
                "cohort_sizes":
                    services.gen_stats_year(),
231
                "member_type_distribution":
232 233 234
                    services.gen_stats_member_type(),
                "total_pizza_orders":
                    pizzas.services.gen_stats_pizza_orders(),
235 236
                "current_pizza_orders":
                    pizzas.services.gen_stats_current_pizza_orders(),
237 238 239 240
                "committee_sizes":
                    activemembers_services.generate_statistics(),
                "event_categories":
                    event_services.generate_category_statistics(),
241
            })
242
        })
243

244
        return context
245 246 247


@method_decorator(login_required, name='dispatch')
248
class EmailChangeFormView(CreateView):
249 250 251
    """
    View that renders the email change form
    """
252 253 254
    model = EmailChange
    fields = ['email', 'member']
    template_name = 'members/user/email_change.html'
255

256
    def get_initial(self) -> dict:
257 258 259 260
        initial = super().get_initial()
        initial['email'] = self.request.member.email
        return initial

261
    def post(self, request, *args, **kwargs) -> HttpResponse:
262 263 264 265
        request.POST = request.POST.dict()
        request.POST['member'] = request.member.pk
        return super().post(request, *args, **kwargs)

266
    def form_valid(self, form) -> HttpResponse:
267 268
        change_request = form.save()
        emails.send_email_change_confirmation_messages(change_request)
269 270 271
        return TemplateResponse(
            request=self.request,
            template='members/user/email_change_requested.html')
272 273 274 275 276 277 278


@method_decorator(login_required, name='dispatch')
class EmailChangeConfirmView(View, TemplateResponseMixin):
    """
    View that renders an HTML template and confirms the old email address
    """
279
    template_name = 'members/user/email_change_confirmed.html'
280

281
    def get(self, request, *args, **kwargs) -> HttpResponse:
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
        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
    """
297
    template_name = 'members/user/email_change_verified.html'
298

299
    def get(self, request, *args, **kwargs) -> HttpResponse:
300 301 302 303 304 305 306 307
        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({})