Unverified Commit 2950f9c9 authored by Thom Wiggers's avatar Thom Wiggers 📐
Browse files

Attempt to implement committees

Includes tests and an authentication backend to check the permissions
granted to committees.
parent 9bb0291e
......@@ -9,6 +9,8 @@
.tox/
db.sqlite3
website/media/
# rope
.ropeproject/
......
from django.contrib import admin
from . import models
admin.site.register(models.Committee)
admin.site.register(models.CommitteeMembership)
# Register your models here.
from django.apps import AppConfig
class CommitteesConfig(AppConfig):
name = 'committees'
"""
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
[
{
"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": []
}
}
]
# -*- 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'),
),
]
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')
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())
# from django.shortcuts import render
# Create your views here.
[
{
"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": ""