From 6e1495a76f241dc1a7767514d14bd78068a46efc Mon Sep 17 00:00:00 2001 From: Joost Rijneveld Date: Fri, 5 Aug 2016 15:44:02 +0200 Subject: [PATCH] Allow for storage of membership history --- website/members/admin.py | 36 ++++++- website/members/fixtures/members.json | 9 -- .../migrations/0004_auto_20160805_1435.py | 46 ++++++++ website/members/models.py | 102 +++++++++++++----- 4 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 website/members/migrations/0004_auto_20160805_1435.py diff --git a/website/members/admin.py b/website/members/admin.py index bfdf8704..69a7cb2d 100644 --- a/website/members/admin.py +++ b/website/members/admin.py @@ -4,22 +4,50 @@ This module registers admin pages for the models from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User +from django.utils.translation import ugettext_lazy as _ from . import models +class MembershipInline(admin.StackedInline): + model = models.Membership + extra = 0 + + class MemberInline(admin.StackedInline): model = models.Member can_delete = False +class MembershipTypeListFilter(admin.SimpleListFilter): + title = _('membership type') + parameter_name = 'membership' + + def lookups(self, request, model_admin): + return models.Membership.MEMBERSHIP_TYPES + + def queryset(self, request, queryset): + if not self.value(): + return queryset + queryset.prefetch_related('user__memberships') + users = set() + for user in queryset: + try: + if user.member.current_membership: + if user.member.current_membership.type == self.value(): + users.add(user.pk) + except models.Member.DoesNotExist: + # The superuser does not have a .member object attached. + pass + return queryset.filter(pk__in=users) + + class UserAdmin(BaseUserAdmin): - inlines = (MemberInline,) + inlines = (MemberInline, MembershipInline) # FIXME include proper filter for expiration # https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter - list_filter = ('member__type', - 'member__membership_expiration', - 'is_superuser') + list_filter = (MembershipTypeListFilter, + 'is_superuser',) # FIXME use nicer form # form = forms.AdminForm (base on ModelForm, reorder elements, etc). diff --git a/website/members/fixtures/members.json b/website/members/fixtures/members.json index 09f5bfdf..3bd6f1c7 100644 --- a/website/members/fixtures/members.json +++ b/website/members/fixtures/members.json @@ -60,9 +60,6 @@ "user": 1, "programme": null, "student_number": "", - "type": "member", - "registration_year": 2011, - "membership_expiration": null, "address_street": "Heyendaalseweg 135", "address_street2": "", "address_postal_code": "1245 TG", @@ -89,9 +86,6 @@ "user": 2, "programme": null, "student_number": "", - "type": "member", - "registration_year": 2011, - "membership_expiration": null, "address_street": "testuserv 2", "address_street2": "", "address_postal_code": "2545 TG", @@ -118,9 +112,6 @@ "user": 3, "programme": null, "student_number": "", - "type": "member", - "registration_year": 1999, - "membership_expiration": null, "address_street": "testuser 2", "address_street2": "", "address_postal_code": "6525 TE", diff --git a/website/members/migrations/0004_auto_20160805_1435.py b/website/members/migrations/0004_auto_20160805_1435.py new file mode 100644 index 00000000..5914f1d3 --- /dev/null +++ b/website/members/migrations/0004_auto_20160805_1435.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2016-08-05 12:35 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('members', '0003_merge_20160727_2333'), + ] + + operations = [ + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('member', 'Member'), ('supporter', 'Supporter'), ('honorary', 'Honorary Member')], max_length=40, verbose_name='Membership type')), + ('since', models.DateField(default=django.utils.timezone.now, help_text='The date the member started holding this membership.', verbose_name='Membership since')), + ('until', models.DateField(blank=True, help_text='The date the member stops holding this membership.', null=True, verbose_name='Membership until')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + ), + migrations.RemoveField( + model_name='member', + name='membership_expiration', + ), + migrations.RemoveField( + model_name='member', + name='registration_year', + ), + migrations.RemoveField( + model_name='member', + name='type', + ), + migrations.AddField( + model_name='member', + name='starting_year', + field=models.IntegerField(blank=True, help_text='The year this member started studying.', null=True, verbose_name='Starting year'), + ), + ] diff --git a/website/members/models.py b/website/members/models.py index 1167ad76..f3f56c81 100644 --- a/website/members/models.py +++ b/website/members/models.py @@ -14,11 +14,6 @@ class Member(models.Model): """This class describes a member""" # No longer yearly membership as a type, use expiration date instead. - MEMBERSHIP_TYPES = ( - ('benefactor', _('Benefactor')), - ('member', _('Member')), - ('honorary', _('Honorary Member'))) - PROGRAMME_CHOICES = ( ('computingscience', _('Computing Science')), ('informationscience', _('Information Sciences'))) @@ -47,23 +42,39 @@ class Member(models.Model): null=True, ) - type = models.CharField( - max_length=40, - choices=MEMBERSHIP_TYPES, - verbose_name=_('Membership type'), + starting_year = models.IntegerField( + verbose_name=_('Starting year'), + help_text=_('The year this member started studying.'), + blank=True, + null=True, ) - registration_year = models.IntegerField( - verbose_name=_('Registration year'), - help_text=_('The year this member first became a part of Thalia'), - ) + @property + def current_membership(self): + membership = self.latest_membership + if membership and not membership.is_active(): + return None + return membership - membership_expiration = models.DateField( - verbose_name=_('Expiration date of membership'), - help_text=_('Let the membership expire after this time'), - null=True, - blank=True, - ) + @property + def latest_membership(self): + if not self.membership_set.exists(): + return None + return self.membership_set.latest('since') + + @property + def membership_set(self): + return self.user.membership_set + + def is_active(self): + """Is this member currently active + + Tested by checking if the expiration date has passed. + """ + return self.current_membership is not None + # Special properties for admin site + is_active.boolean = True + is_active.short_description = _('Is this user currently active') # ---- Address information ----- @@ -221,16 +232,6 @@ class Member(models.Model): blank=True, ) - def is_active(self): - """Is this member currently active - - Tested by checking if the expiration date has passed. - """ - return self.membership_expiration > timezone.now() - # Special properties for admin site - is_active.boolean = True - is_active.short_description = _('Is this user currently active') - def display_name(self): pref = self.display_name_preference if pref == 'nickname': @@ -252,6 +253,49 @@ class Member(models.Model): return self.display_name() +class Membership(models.Model): + + MEMBERSHIP_TYPES = ( + ('member', _('Member')), + ('supporter', _('Supporter')), + ('honorary', _('Honorary Member'))) + + type = models.CharField( + max_length=40, + choices=MEMBERSHIP_TYPES, + verbose_name=_('Membership type'), + ) + + # Preferably this would have been a foreign key to Member instead, + # but Django currently does not support nested inlines in the Admin UI. + # This is necessary to create an inline in the User form. + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + verbose_name=_('User'), + ) + + since = models.DateField( + verbose_name=_("Membership since"), + help_text=_("The date the member started holding this membership."), + default=timezone.now + ) + + until = models.DateField( + verbose_name=_("Membership until"), + help_text=_("The date the member stops holding this membership."), + blank=True, + null=True, + ) + + @property + def member(self): + return self.user.member + + def is_active(self): + return not self.until or self.until > timezone.now() + + class BecomeAMemberDocument(models.Model): name = models.CharField(max_length=200) file = models.FileField( -- GitLab