From 39a47836e54930ec5e3b711d942f87168186ecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastiaan=20Versteeg?= Date: Fri, 17 May 2019 20:28:07 +0200 Subject: [PATCH] Refactor members package to use class-based views --- docs/members.rst | 8 + docs/utils.templatetags.rst | 8 + website/members/admin.py | 13 +- website/members/admin_views.py | 46 ++ website/members/api/serializers.py | 5 + website/members/api/urls.py | 1 + website/members/api/viewsets.py | 6 +- website/members/apps.py | 1 + website/members/decorators.py | 4 + website/members/emails.py | 1 + website/members/forms.py | 18 +- website/members/middleware.py | 4 + website/members/models.py | 1 + website/members/services.py | 40 +- website/members/sitemaps.py | 2 + .../templates/admin/members/change_list.html | 2 +- website/members/templates/members/index.html | 33 +- .../members/{ => user}/edit_profile.html | 7 +- .../members/{ => user}/email_change.html | 0 .../{ => user}/email_change_confirmed.html | 0 .../{ => user}/email_change_requested.html | 0 .../{ => user}/email_change_verified.html | 0 .../members/{user.html => user/index.html} | 0 .../templates/members/{ => user}/profile.html | 0 website/members/tests/test_models.py | 54 +-- website/members/tests/test_views.py | 58 +++ website/members/urls.py | 28 +- website/members/views.py | 425 +++++++++--------- website/thaliawebsite/urls.py | 3 +- website/utils/templatetags/urlparams.py | 12 + 30 files changed, 460 insertions(+), 320 deletions(-) create mode 100644 website/members/admin_views.py rename website/members/templates/members/{ => user}/edit_profile.html (93%) rename website/members/templates/members/{ => user}/email_change.html (100%) rename website/members/templates/members/{ => user}/email_change_confirmed.html (100%) rename website/members/templates/members/{ => user}/email_change_requested.html (100%) rename website/members/templates/members/{ => user}/email_change_verified.html (100%) rename website/members/templates/members/{user.html => user/index.html} (100%) rename website/members/templates/members/{ => user}/profile.html (100%) create mode 100644 website/members/tests/test_views.py create mode 100644 website/utils/templatetags/urlparams.py diff --git a/docs/members.rst b/docs/members.rst index 688f1f18..82954818 100644 --- a/docs/members.rst +++ b/docs/members.rst @@ -24,6 +24,14 @@ members.admin module :undoc-members: :show-inheritance: +members.admin\_views module +--------------------------- + +.. automodule:: members.admin_views + :members: + :undoc-members: + :show-inheritance: + members.apps module ------------------- diff --git a/docs/utils.templatetags.rst b/docs/utils.templatetags.rst index 445de90a..e9454c40 100644 --- a/docs/utils.templatetags.rst +++ b/docs/utils.templatetags.rst @@ -25,3 +25,11 @@ utils.templatetags.thumbnail module :undoc-members: :show-inheritance: +utils.templatetags.urlparams module +----------------------------------- + +.. automodule:: utils.templatetags.urlparams + :members: + :undoc-members: + :show-inheritance: + diff --git a/website/members/admin.py b/website/members/admin.py index 5abef9a3..5ad277ba 100644 --- a/website/members/admin.py +++ b/website/members/admin.py @@ -8,10 +8,11 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User from django.db.models import Q, Count from django.http import HttpResponse +from django.urls import path from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from members import services +from members import services, admin_views from members.models import EmailChange, Member from . import forms, models @@ -206,6 +207,16 @@ class UserAdmin(BaseUserAdmin): ) minimise_data.short_description = _('Minimise data for the selected users') + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('iban-export/', + self.admin_site.admin_view( + admin_views.IbanExportView.as_view()), + name='members_member_ibanexport'), + ] + return custom_urls + urls + @admin.register(models.Member) class MemberAdmin(UserAdmin): diff --git a/website/members/admin_views.py b/website/members/admin_views.py new file mode 100644 index 00000000..de6d62cb --- /dev/null +++ b/website/members/admin_views.py @@ -0,0 +1,46 @@ +"""Admin views provided by the members package""" +import csv + +from django.contrib.admin.views.decorators import staff_member_required +from django.contrib.auth.decorators import permission_required +from django.http import HttpResponse +from django.utils.decorators import method_decorator +from django.views import View + +from members.models import Member + + +@method_decorator(staff_member_required, 'dispatch') +@method_decorator(permission_required('auth.change_user'), 'dispatch') +class IbanExportView(View): + """ + Exports IBANs of users that have set auto renew to true in their accounts + """ + def get(self, request, **kwargs) -> HttpResponse: + header_fields = ['name', 'username', 'iban', 'bic'] + rows = [] + + members = Member.current_members.filter( + profile__auto_renew=True) + + for member in members: + if (member.current_membership.type != 'honorary' and + member.bank_accounts.exists()): + bank_account = member.bank_accounts.last() + rows.append({ + 'name': bank_account.name, + 'username': member.username, + 'iban': bank_account.iban, + 'bic': bank_account.bic + }) + + response = HttpResponse(content_type='text/csv') + writer = csv.DictWriter(response, header_fields) + writer.writeheader() + + for row in rows: + writer.writerow(row) + + response['Content-Disposition'] = ( + 'attachment; filename="iban-export.csv"') + return response diff --git a/website/members/api/serializers.py b/website/members/api/serializers.py index bf5c13d0..be71e23f 100644 --- a/website/members/api/serializers.py +++ b/website/members/api/serializers.py @@ -1,3 +1,4 @@ +"""DRF serializers defined by the members package""" from django.templatetags.static import static from django.urls import reverse from rest_framework import serializers @@ -9,6 +10,7 @@ from thaliawebsite.api.services import create_image_thumbnail_dict class MemberBirthdaySerializer(CalenderJSSerializer): + """Serializer that renders the member birthdays to the CalendarJS format""" class Meta(CalenderJSSerializer.Meta): model = Member @@ -47,6 +49,7 @@ class MemberBirthdaySerializer(CalenderJSSerializer): class ProfileRetrieveSerializer(serializers.ModelSerializer): + """Serializer that renders a member profile""" class Meta: model = Profile fields = ('pk', 'display_name', 'avatar', 'profile_description', @@ -92,6 +95,7 @@ class ProfileRetrieveSerializer(serializers.ModelSerializer): class MemberListSerializer(serializers.ModelSerializer): + """Serializer that renders a list of members""" class Meta: model = Member fields = ('pk', 'display_name', 'avatar') @@ -114,6 +118,7 @@ class MemberListSerializer(serializers.ModelSerializer): class ProfileEditSerializer(serializers.ModelSerializer): + """Serializer that renders a profile to be edited""" class Meta: model = Profile fields = ('pk', 'email', 'first_name', 'last_name', 'address_street', diff --git a/website/members/api/urls.py b/website/members/api/urls.py index 35d75933..e3601152 100644 --- a/website/members/api/urls.py +++ b/website/members/api/urls.py @@ -1,3 +1,4 @@ +"""DRF routes defined by the members package""" from rest_framework import routers from members.api import viewsets diff --git a/website/members/api/viewsets.py b/website/members/api/viewsets.py index 80eb74d6..451279fb 100644 --- a/website/members/api/viewsets.py +++ b/website/members/api/viewsets.py @@ -1,7 +1,8 @@ +"""DRF viewsets defined by the members package""" import copy from rest_framework import permissions -from rest_framework import viewsets, filters +from rest_framework import viewsets, filters, mixins from rest_framework.decorators import action from rest_framework.response import Response @@ -14,7 +15,8 @@ from utils.snippets import extract_date_range class MemberViewset(viewsets.ReadOnlyModelViewSet, - viewsets.mixins.UpdateModelMixin): + mixins.UpdateModelMixin): + """Viewset that renders or edits a member""" queryset = Member.objects.all() filter_backends = (filters.OrderingFilter, filters.SearchFilter,) ordering_fields = ('profile__starting_year', 'first_name', 'last_name') diff --git a/website/members/apps.py b/website/members/apps.py index cf3d2508..ade1e03a 100644 --- a/website/members/apps.py +++ b/website/members/apps.py @@ -1,3 +1,4 @@ +"""Configuration for the members package""" from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ diff --git a/website/members/decorators.py b/website/members/decorators.py index f1f36b40..dcf8252c 100644 --- a/website/members/decorators.py +++ b/website/members/decorators.py @@ -1,3 +1,4 @@ +"""Decorators provided by the members package""" from django.core.exceptions import PermissionDenied @@ -6,6 +7,9 @@ def membership_required(view_function): class ActiveMembershipRequired(object): + """ + Decorator that checks if the user has an active membership + """ def __init__(self, view_function): self.view_function = view_function diff --git a/website/members/emails.py b/website/members/emails.py index 8386087d..acf0581b 100644 --- a/website/members/emails.py +++ b/website/members/emails.py @@ -1,3 +1,4 @@ +"""The emails defined by the members package""" from datetime import timedelta import logging diff --git a/website/members/forms.py b/website/members/forms.py index ff30b79e..0e4a13ef 100644 --- a/website/members/forms.py +++ b/website/members/forms.py @@ -1,14 +1,16 @@ +"""Forms defined by the members package""" from django import forms from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ -from members import emails, models +from members import emails from .models import Profile class ProfileForm(forms.ModelForm): + """Form with all the user editable fields of a Profile model""" class Meta: fields = ['address_street', 'address_street2', 'address_postal_code', 'address_city', 'address_country', @@ -22,6 +24,10 @@ class ProfileForm(forms.ModelForm): class UserCreationForm(BaseUserCreationForm): + """ + Custom Form that removes the password fields from user creation + and sends a welcome message when a user is created + """ # Don't forget to edit the formset in admin.py! # This is a stupid quirk of the user admin. @@ -71,6 +77,10 @@ class UserCreationForm(BaseUserCreationForm): class UserChangeForm(BaseUserChangeForm): + """ + Custom user edit form that adds fields for first/last name and email + It also force-lowercases the username on save + """ first_name = forms.CharField( label=_('First name'), required=True, @@ -104,9 +114,3 @@ class UserChangeForm(BaseUserChangeForm): self.cleaned_data['username'] = (self.cleaned_data['username'] .lower()) super().clean() - - -class EmailChangeForm(forms.ModelForm): - class Meta: - model = models.EmailChange - fields = ['email', 'member'] diff --git a/website/members/middleware.py b/website/members/middleware.py index b4c2a999..15d69b79 100644 --- a/website/members/middleware.py +++ b/website/members/middleware.py @@ -1,3 +1,4 @@ +"""Middleware provided by the members package""" from django.utils.functional import SimpleLazyObject from members.models import Member @@ -11,6 +12,9 @@ def get_member(request): class MemberMiddleware: + """ + Adds the member attribute to requests + """ def __init__(self, get_response): self.get_response = get_response diff --git a/website/members/models.py b/website/members/models.py index 43ca2536..b7bff7f4 100644 --- a/website/members/models.py +++ b/website/members/models.py @@ -1,3 +1,4 @@ +"""Models defined in the members package""" import logging import operator import os diff --git a/website/members/services.py b/website/members/services.py index 651207b0..2df3e470 100644 --- a/website/members/services.py +++ b/website/members/services.py @@ -1,4 +1,6 @@ +"""Services defined in the members package""" from datetime import date +from typing import Callable, List, Dict, Union, Any from django.db.models import Q, Count from django.utils import timezone @@ -9,7 +11,13 @@ from members.models import Membership, Member from utils.snippets import datetime_to_lectureyear -def _member_group_memberships(member, skip_condition): +def _member_group_memberships( + member: Member, skip_condition: Callable[[Membership], bool] +) -> Dict[str, Any]: + """ + Determines the group membership of a user based on a condition + :return: Object with group memberships + """ memberships = member.membergroupmembership_set.all() data = {} @@ -45,9 +53,13 @@ def _member_group_memberships(member, skip_condition): return data -def member_achievements(member): +def member_achievements(member) -> List: + """ + Derives a list of achievements of a member + Committee and board memberships + mentorships + """ achievements = _member_group_memberships( - member, lambda membership: hasattr(membership.group, 'society')) + member, lambda membership: hasattr(membership, 'society')) mentor_years = member.mentorship_set.all() for mentor_year in mentor_years: @@ -63,14 +75,21 @@ def member_achievements(member): return sorted(achievements.values(), key=lambda x: x['earliest']) -def member_societies(member): +def member_societies(member) -> List: + """ + Derives a list of societies a member was part of + """ societies = _member_group_memberships(member, lambda membership: ( hasattr(membership.group, 'board') or hasattr(membership.group, 'committee'))) return sorted(societies.values(), key=lambda x: x['earliest']) -def gen_stats_member_type(member_types): +def gen_stats_member_type(member_types) -> Dict[str, int]: + """ + Generate a dictionary where every key is a member type with + the value being the number of current members of that type + """ total = dict() for member_type in member_types: total[member_type] = (Membership @@ -83,7 +102,8 @@ def gen_stats_member_type(member_types): return total -def gen_stats_year(member_types): +def gen_stats_year( + member_types) -> List[Dict[Union[str, Any], Union[int, Any]]]: """ Generate list with 6 entries, where each entry represents the total amount of Thalia members in a year. The sixth element contains all the multi-year @@ -123,7 +143,7 @@ def gen_stats_year(member_types): return stats_year -def verify_email_change(change_request): +def verify_email_change(change_request) -> None: """ Mark the email change request as verified @@ -135,7 +155,7 @@ def verify_email_change(change_request): process_email_change(change_request) -def confirm_email_change(change_request): +def confirm_email_change(change_request) -> None: """ Mark the email change request as verified @@ -147,7 +167,7 @@ def confirm_email_change(change_request): process_email_change(change_request) -def process_email_change(change_request): +def process_email_change(change_request) -> None: """ Change the user's email address if the request was completed and send the completion email @@ -164,7 +184,7 @@ def process_email_change(change_request): emails.send_email_change_completion_message(change_request) -def execute_data_minimisation(dry_run=False, members=None): +def execute_data_minimisation(dry_run=False, members=None) -> List[Member]: """ Clean the profiles of members/users of whom the last membership ended at least 31 days ago diff --git a/website/members/sitemaps.py b/website/members/sitemaps.py index b39bbc33..2f43d914 100644 --- a/website/members/sitemaps.py +++ b/website/members/sitemaps.py @@ -1,8 +1,10 @@ +"""Sitemaps defined by the members package""" from django.contrib import sitemaps from django.urls import reverse class StaticViewSitemap(sitemaps.Sitemap): + """Static sitemap with members page""" priority = 0.5 changefreq = 'daily' diff --git a/website/members/templates/admin/members/change_list.html b/website/members/templates/admin/members/change_list.html index 196f15f3..f263a2b3 100644 --- a/website/members/templates/admin/members/change_list.html +++ b/website/members/templates/admin/members/change_list.html @@ -3,7 +3,7 @@ {% block object-tools-items %}
  • - {% trans "Export IBANs for Direct Debit" %} + {% trans "Export IBANs for Direct Debit" %}
  • {{ block.super }} {% endblock %} diff --git a/website/members/templates/members/index.html b/website/members/templates/members/index.html index b8c5bb44..b9ac889e 100644 --- a/website/members/templates/members/index.html +++ b/website/members/templates/members/index.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static i18n thumbnail bootstrap4 member_card alert %} +{% load static i18n thumbnail bootstrap4 member_card alert urlparams %} {% block title %}{% trans "members"|capfirst %} — {{ block.super }}{% endblock %} {% block opengraph_title %}{% trans "members"|capfirst %} — {{ block.super }}{% endblock %} @@ -19,50 +19,49 @@

    - - + -