From 2950f9c9c56a12ae0baa0b263183eb02a85c7495 Mon Sep 17 00:00:00 2001 From: Thom Wiggers Date: Thu, 7 Jul 2016 16:19:49 +0200 Subject: [PATCH] Attempt to implement committees Includes tests and an authentication backend to check the permissions granted to committees. --- .gitignore | 2 + dev-requirements.txt | 1 + requirements.txt | 1 + website/committees/__init__.py | 0 website/committees/admin.py | 7 + website/committees/apps.py | 5 + website/committees/backends.py | 55 +++++++ website/committees/fixtures/committees.json | 26 ++++ .../0001_squashed_0006_auto_20160707_1700.py | 103 +++++++++++++ website/committees/migrations/__init__.py | 0 website/committees/models.py | 135 +++++++++++++++++ website/committees/tests.py | 119 +++++++++++++++ website/committees/views.py | 3 + website/members/fixtures/members.json | 143 ++++++++++++++++++ ... 0001_squashed_0002_auto_20160707_1512.py} | 16 +- .../migrations/0002_auto_20160706_1932.py | 26 ---- .../migrations/0003_auto_20160706_2304.py | 21 --- website/members/models.py | 6 +- website/thaliawebsite/settings.py | 10 ++ 19 files changed, 621 insertions(+), 58 deletions(-) create mode 100644 website/committees/__init__.py create mode 100644 website/committees/admin.py create mode 100644 website/committees/apps.py create mode 100644 website/committees/backends.py create mode 100644 website/committees/fixtures/committees.json create mode 100644 website/committees/migrations/0001_squashed_0006_auto_20160707_1700.py create mode 100644 website/committees/migrations/__init__.py create mode 100644 website/committees/models.py create mode 100644 website/committees/tests.py create mode 100644 website/committees/views.py create mode 100644 website/members/fixtures/members.json rename website/members/migrations/{0001_squashed_0004_auto_20160706_1532.py => 0001_squashed_0002_auto_20160707_1512.py} (91%) delete mode 100644 website/members/migrations/0002_auto_20160706_1932.py delete mode 100644 website/members/migrations/0003_auto_20160706_2304.py diff --git a/.gitignore b/.gitignore index cf2a6a59..de7e1074 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ .tox/ db.sqlite3 +website/media/ + # rope .ropeproject/ diff --git a/dev-requirements.txt b/dev-requirements.txt index 053148f8..ccdfca62 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,2 @@ tox +flake8 diff --git a/requirements.txt b/requirements.txt index bf4a506e..29a60535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django==1.10b1 django-localflavor==1.3 +Pillow diff --git a/website/committees/__init__.py b/website/committees/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/committees/admin.py b/website/committees/admin.py new file mode 100644 index 00000000..11d9bb0d --- /dev/null +++ b/website/committees/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from . import models + +admin.site.register(models.Committee) +admin.site.register(models.CommitteeMembership) +# Register your models here. diff --git a/website/committees/apps.py b/website/committees/apps.py new file mode 100644 index 00000000..7db8292f --- /dev/null +++ b/website/committees/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CommitteesConfig(AppConfig): + name = 'committees' diff --git a/website/committees/backends.py b/website/committees/backends.py new file mode 100644 index 00000000..df130f43 --- /dev/null +++ b/website/committees/backends.py @@ -0,0 +1,55 @@ +""" +Authentication backend to check permissions +""" +from django.contrib.auth.models import Permission + +from members.models import Member + + +class CommitteeBackend(object): + """Check permissions against committees""" + + def authenticate(self, *args, **kwargs): + """Not implemented in this backend""" + return + + def get_user(self, *args, **kwargs): + """Not implemented in this backend""" + return + + def _get_permissions(self, user, obj): + if not user.is_active or user.is_anonymous or obj is not None: + return set() + perm_cache_name = '_committee_perm_cache' + try: + committees = user.member.committee_set.all() + except Member.DoesNotExist: + return set() + if not hasattr(user, perm_cache_name): + perms = (Permission.objects + .filter(committee=committees) + .values_list('content_type__app_label', 'codename') + .order_by()) + setattr(user, perm_cache_name, + set("{}.{}".format(ct, name) for ct, name in perms)) + return getattr(user, perm_cache_name) + + def get_all_permissions(self, user, obj=None): + return self._get_permissions(user, obj) + + def get_group_permissions(self, user, obj=None): + return self._get_permissions(user, obj) + + def has_perm(self, user, perm, obj=None): + if not user.is_active: + return False + return perm in self.get_all_permissions(user, obj) + + def has_module_perms(self, user, app_label): + """Returns True if user has any permissions in the given app_label""" + if not user.is_active: + return False + for perm in self.get_all_permissions(user): + if perm[:perm.index('.')] == app_label: + return True + return False diff --git a/website/committees/fixtures/committees.json b/website/committees/fixtures/committees.json new file mode 100644 index 00000000..aeb84a59 --- /dev/null +++ b/website/committees/fixtures/committees.json @@ -0,0 +1,26 @@ +[ +{ + "model": "committees.committee", + "pk": 1, + "fields": { + "name": "testcie1", + "description": "Test", + "photo": "Thom_Wiggers.jpg", + "permissions": [ + 25, + 26, + 27 + ] + } +}, +{ + "model": "committees.committee", + "pk": 2, + "fields": { + "name": "testcie2", + "description": "testdesc2", + "photo": "Thom_Wiggers_4YRoxV3.jpg", + "permissions": [] + } +} +] diff --git a/website/committees/migrations/0001_squashed_0006_auto_20160707_1700.py b/website/committees/migrations/0001_squashed_0006_auto_20160707_1700.py new file mode 100644 index 00000000..1ea5cd45 --- /dev/null +++ b/website/committees/migrations/0001_squashed_0006_auto_20160707_1700.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10b1 on 2016-07-07 15:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.manager + + +class Migration(migrations.Migration): + + replaces = [('committees', '0001_initial'), ('committees', '0002_committee_permissions'), ('committees', '0003_auto_20160707_1356'), ('committees', '0004_auto_20160707_1357'), ('committees', '0005_auto_20160707_1512'), ('committees', '0006_auto_20160707_1700')] + + initial = True + + dependencies = [ + ('members', '0001_initial'), + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Committee', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40, verbose_name='Committee name')), + ('description', models.TextField(verbose_name='Description')), + ('photo', models.ImageField(upload_to='', verbose_name='Image')), + ], + ), + migrations.CreateModel( + name='CommitteeMembership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('since', models.DateField(auto_now_add=True)), + ('until', models.DateField()), + ('chair', models.BooleanField()), + ('committee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='committees.Committee')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.Member')), + ], + managers=[ + ('active_memberships', django.db.models.manager.Manager()), + ], + ), + migrations.AddField( + model_name='committee', + name='members', + field=models.ManyToManyField(through='committees.CommitteeMembership', to='members.Member'), + ), + migrations.AddField( + model_name='committee', + name='permissions', + field=models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='permissions'), + ), + migrations.AlterModelOptions( + name='committee', + options={'verbose_name': 'committee', 'verbose_name_plural': 'committees'}, + ), + migrations.AlterModelOptions( + name='committeemembership', + options={'verbose_name': 'committee membership', 'verbose_name_plural': 'committee memberships'}, + ), + migrations.AlterField( + model_name='committeemembership', + name='chair', + field=models.BooleanField(help_text='There can only be one chair at a time!', verbose_name='Chair of the committee'), + ), + migrations.AlterField( + model_name='committeemembership', + name='committee', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='committees.Committee', verbose_name='Committee'), + ), + migrations.AlterField( + model_name='committeemembership', + name='member', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.Member', verbose_name='Member'), + ), + migrations.AlterField( + model_name='committeemembership', + name='since', + field=models.DateField(auto_now_add=True, help_text='The date this member joined the committee in this role', verbose_name='Committee member since'), + ), + migrations.AlterField( + model_name='committeemembership', + name='until', + field=models.DateField(blank=True, help_text="A member of this committee until this time (can't be in the future).", verbose_name='Committee member until'), + ), + migrations.AlterField( + model_name='committeemembership', + name='until', + field=models.DateField(blank=True, help_text="A member of this committee until this time (can't be in the future).", null=True, verbose_name='Committee member until'), + ), + migrations.AlterField( + model_name='committee', + name='name', + field=models.CharField(max_length=40, unique=True, verbose_name='Committee name'), + ), + migrations.AlterField( + model_name='committeemembership', + name='chair', + field=models.BooleanField(default=False, help_text='There can only be one chair at a time!', verbose_name='Chair of the committee'), + ), + ] diff --git a/website/committees/migrations/__init__.py b/website/committees/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/committees/models.py b/website/committees/models.py new file mode 100644 index 00000000..39fd8e69 --- /dev/null +++ b/website/committees/models.py @@ -0,0 +1,135 @@ +from django.utils import timezone + +from django.core.exceptions import ValidationError, NON_FIELD_ERRORS +from django.contrib.auth.models import Permission +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from members.models import Member + + +class Committee(models.Model): + """A committee""" + + name = models.CharField( + max_length=40, + verbose_name=_('Committee name'), + unique=True, + ) + + description = models.TextField( + verbose_name=_('Description'), + ) + + photo = models.ImageField( + verbose_name=_('Image'), + ) + + members = models.ManyToManyField( + Member, + through='CommitteeMembership' + ) + + permissions = models.ManyToManyField( + Permission, + verbose_name=_('permissions'), + blank=True, + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('committee') + verbose_name_plural = _('committees') + + +class ActiveMembershipManager(models.Manager): + """Get only active memberships""" + def get_queryset(self): + """Get the currently active committee memberships""" + return super().get_queryset().exclude(until__lt=timezone.now()) + + +class CommitteeMembership(models.Model): + active_memberships = ActiveMembershipManager() + objects = models.Manager() + + member = models.ForeignKey( + Member, + on_delete=models.CASCADE, + verbose_name=_('Member'), + ) + + committee = models.ForeignKey( + Committee, + on_delete=models.CASCADE, + verbose_name=_('Committee'), + ) + + since = models.DateField( + verbose_name=_('Committee member since'), + help_text=_('The date this member joined the committee in this role'), + auto_now_add=True, + ) + + until = models.DateField( + verbose_name=_('Committee member until'), + help_text=_("A member of this committee until this time " + "(can't be in the future)."), + blank=True, + null=True, + ) + + chair = models.BooleanField( + verbose_name=_('Chair of the committee'), + help_text=_('There can only be one chair at a time!'), + default=False, + ) + + @property + def is_active(self): + """Is this membership currently active""" + return self.until is None or self.until > timezone.now() + + def clean(self): + """Validation""" + if self.until and self.until > timezone.now(): + raise ValidationError({ + 'until': _("Membership expiration date can't be in the future:" + " '{}'").format(self.until) + }) + + if self.until and (not self.since or self.until < self.since): + raise ValidationError( + {'until': _("End date can't be before start date")}) + + def validate_unique(self, *args, **kwargs): + """ Check uniqueness""" + super().validate_unique(*args, **kwargs) + # Check if a committee has more than one chair + chairs = (CommitteeMembership.active_memberships + .filter(committee=self.committee) + .filter(chair=True) + .count()) + if chairs >= 1 and self.chair: + raise ValidationError({ + NON_FIELD_ERRORS: + _('This committee already has a chair')}) + + # check if this member is already in the committee + members = (self.committee.members + .filter(pk=self.member.pk) + .count()) + if members >= 1: + raise ValidationError({ + 'member': _('This member is already in the committee')}) + + def __str__(self): + return "{} membership of {} since {}".format(self.member, + self.committee, + self.since) + + class Meta: + verbose_name = _('committee membership') + verbose_name_plural = _('committee memberships') diff --git a/website/committees/tests.py b/website/committees/tests.py new file mode 100644 index 00000000..b0d8469e --- /dev/null +++ b/website/committees/tests.py @@ -0,0 +1,119 @@ +from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model +from django.db.utils import IntegrityError +from django.test import TestCase +from django.utils import timezone + +from committees.models import Committee, CommitteeMembership +from members.models import Member + + +class CommitteeMembersTest(TestCase): + fixtures = ['members.json', 'committees.json'] + + @classmethod + def setUpTestData(cls): + cls.testcie = Committee.objects.get(name='testcie1') + cls.testuser = Member.objects.get(pk=1) + + cls.m = CommitteeMembership(committee=cls.testcie, + member=cls.testuser, + chair=False) + cls.m.save() + + def test_unique(self): + with self.assertRaises(IntegrityError): + Committee.objects.create(name="testcie1", + description="desc3", + photo="") + + def test_join(self): + testuser2 = Member.objects.get(pk=2) + m = CommitteeMembership(committee=self.testcie, + member=testuser2) + m.full_clean() + m.save() + + def test_join_unique(self): + m = CommitteeMembership(committee=self.testcie, + member=self.testuser) + with self.assertRaises(ValidationError): + m.full_clean() + + def test_until_date(self): + m = CommitteeMembership(committee=self.testcie, + member=self.testuser, + until=timezone.now().replace(year=2000), + chair=False) + with self.assertRaises(ValidationError): + m.clean() + m.since = timezone.now().replace(year=1900) + m.clean() + + def test_inactive(self): + self.assertTrue(self.m.is_active) + self.m.until = timezone.now().replace(year=1900) + self.assertFalse(self.m.is_active) + + +class CommitteeMembersChairTest(TestCase): + fixtures = ['members.json', 'committees.json'] + + @classmethod + def setUpTestData(cls): + testcie = Committee.objects.get(name='testcie1') + testuser = Member.objects.get(pk=1) + cls.m1 = CommitteeMembership(committee=testcie, + member=testuser, + chair=True) + cls.m1.full_clean() + cls.m1.save() + + def setUp(self): + self.testcie = Committee.objects.get(name='testcie1') + self.testuser = Member.objects.get(pk=1) + + def test_second_chair_fails(self): + testuser2 = Member.objects.get(pk=2) + + m = CommitteeMembership(committee=self.testcie, + member=testuser2, + chair=True) + with self.assertRaises(ValidationError): + m.full_clean() + + def test_inactive_chair(self): + testuser2 = Member.objects.get(pk=2) + self.m1.until = timezone.now().replace(year=1900) + self.m1.save() + m = CommitteeMembership(committee=self.testcie, + member=testuser2, + chair=True) + m.full_clean() + + +class BackendTest(TestCase): + fixtures = ['members.json', 'committees.json'] + + @classmethod + def setUpTestData(cls): + cls.u1 = Member.objects.get(pk=1) + cls.u1.user.is_superuser = False + cls.u1.save() + cls.u2 = Member.objects.get(pk=2) + cls.u3 = Member.objects.get(pk=3) + cls.c1 = Committee.objects.get(pk=1) + cls.c2 = Committee.objects.get(pk=2) + cls.m1 = CommitteeMembership.objects.create(committee=cls.c1, + member=cls.u1) + cls.m2 = CommitteeMembership.objects.create(committee=cls.c2, + member=cls.u2) + + def test_permissions(self): + self.assertEqual(3, len(self.u1.user.get_all_permissions())) + self.assertEqual(set(), self.u2.user.get_all_permissions()) + self.assertEqual(set(), self.u3.user.get_all_permissions()) + + def test_nonmember_user(self): + u = get_user_model().objects.create(username='foo') + self.assertEqual(set(), u.get_all_permissions()) diff --git a/website/committees/views.py b/website/committees/views.py new file mode 100644 index 00000000..fd0e0449 --- /dev/null +++ b/website/committees/views.py @@ -0,0 +1,3 @@ +# from django.shortcuts import render + +# Create your views here. diff --git a/website/members/fixtures/members.json b/website/members/fixtures/members.json new file mode 100644 index 00000000..09f5bfdf --- /dev/null +++ b/website/members/fixtures/members.json @@ -0,0 +1,143 @@ +[ +{ + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$30000$HIt9lBUpgkYG$T2ofXIOlAhsqfMUqzl3Vl9vyaDq50d1JJJNEYeZ9/OM=", + "last_login": "2016-07-07T11:37:43Z", + "is_superuser": true, + "username": "thom", + "first_name": "Thom", + "last_name": "Wiggers", + "email": "", + "is_staff": true, + "is_active": true, + "date_joined": "2016-07-07T11:37:38Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$30000$80KR811he3aB$W11Exs1wY0tXw9kLsyunh1dzvRDcn1a+Hc9m1lTirFY=", + "last_login": "2016-07-07T12:01:02.638Z", + "is_superuser": false, + "username": "testuser", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": true, + "is_active": true, + "date_joined": "2016-07-07T12:00:21Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.user", + "pk": 3, + "fields": { + "password": "", + "last_login": null, + "is_superuser": false, + "username": "testuser2", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": true, + "is_active": true, + "date_joined": "2016-07-07T14:50:26Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "members.member", + "pk": 1, + "fields": { + "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", + "address_city": "Nijmegen", + "phone_number": "", + "emergency_contact": "", + "emergency_contact_phone_number": "", + "birthday": "1993-03-02", + "show_birthday": true, + "website": "", + "profile_description": "", + "nickname": "", + "display_name_preference": "full", + "language": "nl", + "receive_optin": true, + "direct_debit_authorized": false, + "bank_account": "" + } +}, +{ + "model": "members.member", + "pk": 2, + "fields": { + "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", + "address_city": "Nijmegen", + "phone_number": "", + "emergency_contact": "", + "emergency_contact_phone_number": "", + "birthday": "2016-07-07", + "show_birthday": true, + "website": "", + "profile_description": "", + "nickname": "", + "display_name_preference": "full", + "language": "nl", + "receive_optin": true, + "direct_debit_authorized": false, + "bank_account": "" + } +}, +{ + "model": "members.member", + "pk": 3, + "fields": { + "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", + "address_city": "Nijmegen", + "phone_number": "", + "emergency_contact": "", + "emergency_contact_phone_number": "", + "birthday": "2016-07-07", + "show_birthday": true, + "website": "", + "profile_description": "", + "nickname": "", + "display_name_preference": "full", + "language": "nl", + "receive_optin": true, + "direct_debit_authorized": false, + "bank_account": "" + } +} +] diff --git a/website/members/migrations/0001_squashed_0004_auto_20160706_1532.py b/website/members/migrations/0001_squashed_0002_auto_20160707_1512.py similarity index 91% rename from website/members/migrations/0001_squashed_0004_auto_20160706_1532.py rename to website/members/migrations/0001_squashed_0002_auto_20160707_1512.py index 5a4affc2..405d7a48 100644 --- a/website/members/migrations/0001_squashed_0004_auto_20160706_1532.py +++ b/website/members/migrations/0001_squashed_0002_auto_20160707_1512.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10b1 on 2016-07-06 13:32 +# Generated by Django 1.10b1 on 2016-07-07 15:04 from __future__ import unicode_literals from django.conf import settings @@ -11,7 +11,7 @@ import localflavor.generic.models class Migration(migrations.Migration): - replaces = [('members', '0001_initial'), ('members', '0002_auto_20160706_1411'), ('members', '0003_auto_20160706_1424'), ('members', '0004_auto_20160706_1532')] + replaces = [('members', '0001_initial'), ('members', '0002_auto_20160707_1512')] initial = True @@ -29,24 +29,24 @@ class Migration(migrations.Migration): ('type', models.CharField(choices=[('benefactor', 'Benefactor'), ('member', 'Member'), ('honorary', 'Honorary Member')], max_length=40, verbose_name='Membership type')), ('registration_year', models.IntegerField(help_text='The year this member first became a part of Thalia', verbose_name='Registration year')), ('membership_expiration', models.DateField(blank=True, help_text='Let the membership expire after this time', null=True, verbose_name='Expiration date of membership')), - ('address_street', models.CharField(max_length=100, null=True, validators=[django.core.validators.RegexValidator(message='Include the house number', regex='^.+ \\d+.+')], verbose_name='Street and house number')), + ('address_street', models.CharField(max_length=100, null=True, validators=[django.core.validators.RegexValidator(message='Include the house number', regex='^.+ \\d+.*')], verbose_name='Street and house number')), ('address_street2', models.CharField(blank=True, max_length=100, null=True, verbose_name='Second address line')), ('address_postal_code', models.CharField(max_length=10, null=True, verbose_name='Postal code')), ('address_city', models.CharField(max_length=40, null=True, verbose_name='City')), - ('phone_number', models.CharField(blank=True, help_text='Enter a phone number so Thalia may reach you', max_length=13, null=True, validators=[django.core.validators.RegexValidator(message='Please enter a valid phone number', regex='^\\+?\\d+$')], verbose_name='Phone number')), + ('phone_number', models.CharField(blank=True, help_text='Enter a phone number so Thalia may reach you', max_length=20, null=True, validators=[django.core.validators.RegexValidator(message='Please enter a valid phone number', regex='^\\+?\\d+$')], verbose_name='Phone number')), + ('emergency_contact', models.CharField(blank=True, help_text='Who should we contact in case of emergencies', max_length=255, null=True, verbose_name='Emergency contact name')), + ('emergency_contact_phone_number', models.CharField(blank=True, help_text='The phone number for the emergency contact', max_length=20, null=True, validators=[django.core.validators.RegexValidator(message='Please enter a valid phone number', regex='^\\+?\\d+$')], verbose_name='Emergency contact phone number')), ('birthday', models.DateField(null=True, verbose_name='Birthday')), + ('show_birthday', models.BooleanField(default=True, help_text='Show the birthday on your profile page and in the birthday calendar', verbose_name='Display birthday')), ('website', models.URLField(blank=True, help_text='Website to display on your profile page', null=True, verbose_name='Website')), ('profile_description', models.TextField(blank=True, help_text='Text to display on your profile', verbose_name='Profile text')), ('nickname', models.CharField(blank=True, max_length=30, null=True, verbose_name='Nickname')), + ('display_name_preference', models.CharField(choices=[('full', 'Show full name'), ('nickname', 'Show only nickname'), ('initials', 'Show initials and last name'), ('fullnick', 'Show name like "John \'nickname\' Doe"'), ('nicklast', 'Show nickname and last name')], default='full', max_length=10, verbose_name='How to display name')), ('language', models.CharField(choices=[('en', 'English'), ('nl', 'Dutch')], default='nl', help_text='Preferred language for e.g. news letters', max_length=5, verbose_name='Preferred language')), ('receive_optin', models.BooleanField(default=True, help_text="Receive mailings about vacancies and events from Thalia's sponsors.", verbose_name='Receive opt-in mailings')), ('direct_debit_authorized', models.BooleanField(choices=[(True, 'Yes, I want Thalia to take the membership fees from my bank account through direct debit for each year.'), (False, 'No, I will pay the contribution myself')], default=False, help_text='Each year, have Thalia take the membership fees from my bank account', verbose_name='Direct debit')), ('bank_account', localflavor.generic.models.IBANField(False, ('AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GI', 'GR', 'HR', 'HU', 'IE', 'IS', 'IT', 'LI', 'LT', 'LU', 'LV', 'MC', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'SM'), blank=True, help_text='Bank account for direct debit', verbose_name='Bank account')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('display_name_preference', models.CharField(choices=[('full', 'Show full name'), ('nickname', 'Show only nickname'), ('initials', 'Show initials and last name'), ('fullnick', 'Show name like "John \'nickname\' Doe"'), ('nicklast', 'Show nickname and last name')], default='full', max_length=10, verbose_name='How to display name')), - ('emergency_contact', models.CharField(blank=True, help_text='Who should we contact in case of emergencies', max_length=255, null=True, verbose_name='Emergency contact name')), - ('emergency_contact_phone_number', models.CharField(blank=True, help_text='The phone number for the emergency contact', max_length=13, null=True, validators=[django.core.validators.RegexValidator(message='Voer een geldig telefoonnummer in.', regex='^\\+?\\d+$')], verbose_name='Emergency contact phone number')), - ('show_birthday', models.BooleanField(default=True, help_text='Show the birthday on your profile page and in the birthday calendar', verbose_name='Display birthday')), ], ), ] diff --git a/website/members/migrations/0002_auto_20160706_1932.py b/website/members/migrations/0002_auto_20160706_1932.py deleted file mode 100644 index e34c3e6b..00000000 --- a/website/members/migrations/0002_auto_20160706_1932.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10b1 on 2016-07-06 17:32 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('members', '0001_squashed_0004_auto_20160706_1532'), - ] - - operations = [ - migrations.AlterField( - model_name='member', - name='emergency_contact_phone_number', - field=models.CharField(blank=True, help_text='The phone number for the emergency contact', max_length=20, null=True, validators=[django.core.validators.RegexValidator(message='Voer een geldig telefoonnummer in.', regex='^\\+?\\d+$')], verbose_name='Emergency contact phone number'), - ), - migrations.AlterField( - model_name='member', - name='phone_number', - field=models.CharField(blank=True, help_text='Enter a phone number so Thalia may reach you', max_length=20, null=True, validators=[django.core.validators.RegexValidator(message='Please enter a valid phone number', regex='^\\+?\\d+$')], verbose_name='Phone number'), - ), - ] diff --git a/website/members/migrations/0003_auto_20160706_2304.py b/website/members/migrations/0003_auto_20160706_2304.py deleted file mode 100644 index e1843973..00000000 --- a/website/members/migrations/0003_auto_20160706_2304.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10b1 on 2016-07-06 21:04 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('members', '0002_auto_20160706_1932'), - ] - - operations = [ - migrations.AlterField( - model_name='member', - name='emergency_contact_phone_number', - field=models.CharField(blank=True, help_text='The phone number for the emergency contact', max_length=20, null=True, validators=[django.core.validators.RegexValidator(message='Please enter a valid phone number', regex='^\\+?\\d+$')], verbose_name='Emergency contact phone number'), - ), - ] diff --git a/website/members/models.py b/website/members/models.py index 2ae6fd94..67a0cfb8 100644 --- a/website/members/models.py +++ b/website/members/models.py @@ -1,4 +1,4 @@ -import datetime +from django.utils import timezone from django.db import models from django.core import validators @@ -69,7 +69,7 @@ class Member(models.Model): address_street = models.CharField( max_length=100, validators=[validators.RegexValidator( - regex=r'^.+ \d+.+', + regex=r'^.+ \d+.*', message=_('Include the house number'), )], verbose_name=_('Street and house number'), @@ -218,7 +218,7 @@ class Member(models.Model): Tested by checking if the expiration date has passed. """ - return self.membership_expiration > datetime.utcnow() + return self.membership_expiration > timezone.now() # Special properties for admin site is_active.boolean = True is_active.short_description = _('Is this user currently active') diff --git a/website/thaliawebsite/settings.py b/website/thaliawebsite/settings.py index 7ab15abd..d12023e3 100644 --- a/website/thaliawebsite/settings.py +++ b/website/thaliawebsite/settings.py @@ -41,6 +41,7 @@ INSTALLED_APPS = [ # Our apps 'thaliawebsite', # include for admin settings 'members', + 'committees', ] MIDDLEWARE = [ @@ -56,6 +57,7 @@ MIDDLEWARE = [ ROOT_URLCONF = 'thaliawebsite.urls' + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -108,6 +110,11 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'committees.backends.CommitteeBackend', +] + # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ @@ -134,3 +141,6 @@ LOCALE_PATHS = ('locale',) # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_URL = '/static/' + +# Where to store uploaded files +MEDIA_ROOT = './media/' -- GitLab