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