Unverified Commit 9caeee75 authored by Joost Rijneveld's avatar Joost Rijneveld
Browse files

Merge branch 'master' into 1-documents

parents a5d4ed97 e2df81b0
......@@ -10,6 +10,10 @@
db.sqlite3
website/media/
website/static/
# Ignore local settings
website/thaliawebsite/settings/localsettings.py
# rope
.ropeproject/
......
......@@ -8,7 +8,8 @@ New new Thalia website, now with extra Django.
Getting started
---------------
0. Get at least Python 3.4
0. Get at least Python 3.4 and install the Pillow requirements as per below.
Also make sure that you have `lessc` installed (see below).
1. Clone this repository
2. Run `source ./source_me.sh` (or use your own favourite virtualenv solution)
3. Run `pip install -r requirements.txt`
......@@ -22,3 +23,27 @@ Testing and linting
-------------------
1. In the root folder of the project, run `tox`.
You may get errors about missing interpreters. That is normal and can be
ignored. If you want to run a specific check, you can do the following:
tox -e flake8 # Runs the flake8 linter
tox -e py34 # runs the tests with python 3.4
tox -e py35 # runs the tests with python 3.5
Pillow dependencies
-------------------
For Ubuntu 16.04, use:
apt-get install python-dev gettext gcc build-essential libtiff5-dev libjpeg62-turbo-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev
For other operating systems, see the [Pillow Documentation][pillow-install].
[pillow-install]: https://pillow.readthedocs.io/en/latest/installation.html
NodeJS dependencies
-----------------------
1. `lessc`:
* For Ubuntu use: `apt-get install node-less`
#!/bin/bash
pathname=$_
echo "run me as 'source ./source_me.sh', not as './source_me.sh'!"
if [[ "$pathname" = "$0" ]]; then
echo "Remember: you need to run me as 'source ./source_me.sh', not execute it!"
return
fi
if [ -d venv ]; then
source venv/bin/activate
......
......@@ -6,6 +6,7 @@ skipsdist = True
changedir={toxinidir}/website
commands =
python manage.py check
python manage.py makemigrations --no-input --check
python -Wall manage.py test
deps = -r{toxinidir}/requirements.txt
......
......@@ -2,6 +2,21 @@ from django.contrib import admin
from . import models
admin.site.register(models.Committee)
admin.site.register(models.CommitteeMembership)
# Register your models here.
@admin.register(models.Committee)
class CommitteeAdmin(admin.ModelAdmin):
list_filter = ('until',)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.exclude(board__is_board=True)
@admin.register(models.Board)
class BoardAdmin(admin.ModelAdmin):
exclude = ('is_board',)
@admin.register(models.CommitteeMembership)
class CommitteeMembershipAdmin(admin.ModelAdmin):
pass
# -*- coding: utf-8 -*-
# Generated by Django 1.10b1 on 2016-07-13 13:58
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
import django.db.models.manager
class Migration(migrations.Migration):
dependencies = [
('committees', '0001_squashed_0006_auto_20160707_1700'),
]
operations = [
migrations.AlterModelManagers(
name='committee',
managers=[
('active_committees', django.db.models.manager.Manager()),
],
),
migrations.AddField(
model_name='committee',
name='contact_email',
field=models.EmailField(default='commissie@thalia.nu', max_length=254, verbose_name='contact email address'),
preserve_default=False,
),
migrations.AddField(
model_name='committee',
name='since',
field=models.DateField(blank=True, null=True, verbose_name='founded in'),
),
migrations.AddField(
model_name='committee',
name='until',
field=models.DateField(blank=True, null=True, verbose_name='existed until'),
),
migrations.AlterField(
model_name='committeemembership',
name='since',
field=models.DateField(default=datetime.date.today, help_text='The date this member joined the committee in this role', verbose_name='Committee member since'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10b1 on 2016-07-13 15:00
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('committees', '0002_auto_20160713_1558'),
]
operations = [
migrations.CreateModel(
name='Board',
fields=[
('committee_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='committees.Committee')),
('is_board', models.BooleanField(default=True, verbose_name='Is this a board')),
],
bases=('committees.committee',),
),
migrations.AddField(
model_name='committeemembership',
name='role',
field=models.CharField(blank=True, help_text='The role of this member', max_length=255, null=True, verbose_name='role'),
),
]
from django.utils import timezone
import datetime
import logging
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.contrib.auth.models import Permission
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from members.models import Member
logger = logging.getLogger(__name__)
class CommitteeManager(models.Manager):
"""Returns committees only"""
def get_queryset(self):
return (super().get_queryset()
.exclude(board__is_board=True))
class ActiveCommitteeManager(models.Manager):
"""Returns active committees only"""
def get_queryset(self):
return (super().get_queryset()
.exclude(until__lt=timezone.now().date()))
class Committee(models.Model):
"""A committee"""
active_committees = ActiveCommitteeManager()
objects = CommitteeManager()
name = models.CharField(
max_length=40,
verbose_name=_('Committee name'),
......@@ -36,6 +58,20 @@ class Committee(models.Model):
blank=True,
)
since = models.DateField(
_('founded in'),
null=True,
blank=True,
)
until = models.DateField(
_('existed until'),
null=True,
blank=True,
)
contact_email = models.EmailField(_('contact email address'))
def __str__(self):
return self.name
......@@ -44,11 +80,25 @@ class Committee(models.Model):
verbose_name_plural = _('committees')
class BoardManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_board=True)
class Board(Committee):
objects = BoardManager()
is_board = models.BooleanField(
verbose_name=_('Is this a board'),
default=True,
)
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())
return super().get_queryset().exclude(until__lt=timezone.now().date())
class CommitteeMembership(models.Model):
......@@ -70,7 +120,7 @@ class CommitteeMembership(models.Model):
since = models.DateField(
verbose_name=_('Committee member since'),
help_text=_('The date this member joined the committee in this role'),
auto_now_add=True,
default=datetime.date.today
)
until = models.DateField(
......@@ -87,14 +137,30 @@ class CommitteeMembership(models.Model):
default=False,
)
role = models.CharField(
_('role'),
help_text=_('The role of this member'),
max_length=255,
blank=True,
null=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.pk is None:
self._was_chair = bool(self.chair)
else:
self._was_chair = False
@property
def is_active(self):
"""Is this membership currently active"""
return self.until is None or self.until > timezone.now()
return self.until is None or self.until > timezone.now().date()
def clean(self):
"""Validation"""
if self.until and self.until > timezone.now():
if self.until and self.until > timezone.now().date():
raise ValidationError({
'until': _("Membership expiration date can't be in the future:"
" '{}'").format(self.until)
......@@ -110,6 +176,7 @@ class CommitteeMembership(models.Model):
# Check if a committee has more than one chair
chairs = (CommitteeMembership.active_memberships
.filter(committee=self.committee)
.exclude(member=self.member)
.filter(chair=True)
.count())
if chairs >= 1 and self.chair:
......@@ -118,12 +185,35 @@ class CommitteeMembership(models.Model):
_('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')})
if self.pk is None:
members = (self.committee.members
.filter(pk=self.member.pk)
.count())
if members >= 1:
raise ValidationError({
'member': _('This member is already in the committee')})
def save(self, *args, **kwargs):
"""Save the instance"""
# If the chair changed and we're still active, we create a new instance
# Inactive instances should be handled manually
if (self.pk is not None and self._was_chair != self.chair and
not self.until):
logger.info("Creating new membership instance")
self.until = timezone.now().date()
super().save(*args, **kwargs)
self.pk = None # forces INSERT
self.since = self.until # Set since date to older expiration
self.until = None
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Deactivates active memberships, deletes inactive ones"""
if self.is_active:
self.until = timezone.now().date()
self.save()
else:
super().delete(*args, **kwargs)
def __str__(self):
return "{} membership of {} since {}".format(self.member,
......
......@@ -11,15 +11,14 @@ 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 setUp(self):
# Don't use setUpTestData because delete() will cause problems
self.testcie = Committee.objects.get(name='testcie1')
self.testuser = Member.objects.get(pk=1)
self.m = CommitteeMembership(committee=self.testcie,
member=self.testuser,
chair=False)
self.m.save()
def test_unique(self):
with self.assertRaises(IntegrityError):
......@@ -43,56 +42,80 @@ class CommitteeMembersTest(TestCase):
def test_until_date(self):
m = CommitteeMembership(committee=self.testcie,
member=self.testuser,
until=timezone.now().replace(year=2000),
until=timezone.now().date().replace(year=2000),
chair=False)
with self.assertRaises(ValidationError):
m.clean()
m.since = timezone.now().replace(year=1900)
m.since = timezone.now().date().replace(year=1900)
m.clean()
def test_inactive(self):
self.assertTrue(self.m.is_active)
self.m.until = timezone.now().replace(year=1900)
self.m.until = timezone.now().date().replace(year=1900)
self.assertFalse(self.m.is_active)
def test_delete(self):
self.m.delete()
self.assertIsNotNone(self.m.until)
self.assertIsNotNone(self.m.pk)
self.m.delete()
self.assertIsNone(self.m.pk)
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)
self.testuser2 = Member.objects.get(pk=2)
self.m1 = CommitteeMembership(committee=self.testcie,
member=self.testuser,
chair=True)
self.m1.full_clean()
self.m1.save()
def test_second_chair_fails(self):
testuser2 = Member.objects.get(pk=2)
m = CommitteeMembership(committee=self.testcie,
member=testuser2,
member=self.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.until = timezone.now().date().replace(year=1900)
self.m1.save()
m = CommitteeMembership(committee=self.testcie,
member=testuser2,
member=self.testuser2,
chair=True)
m.full_clean()
def test_clean_self_chair(self):
self.m1.chair = True
self.m1.full_clean()
def test_change_chair(self):
pk = self.m1.pk
original_chair = self.m1.chair
self.m1.save()
self.assertEqual(self.m1.pk, pk, "new object created")
self.m1.chair = not original_chair
self.m1.save()
self.assertNotEqual(self.m1.pk, pk, "No new object created")
def test_change_chair_inactive(self):
pk = self.m1.pk
original_chair = self.m1.chair
self.m1.until = timezone.now().date()
self.m1.save()
self.assertEqual(self.m1.pk, pk, "new object created")
self.m1.chair = not original_chair
self.m1.save()
self.assertEqual(self.m1.pk, pk, "No new object created")
class BackendTest(TestCase):
class PermissionsBackendTest(TestCase):
fixtures = ['members.json', 'committees.json']
@classmethod
......
from django.apps import AppConfig
class LedenConfig(AppConfig):
name = 'leden'
class MembersConfig(AppConfig):
name = 'members'
from django.utils.translation import ugettext as _
main = [
{'title': _('Home'), 'name': 'index'},
{'title': _('Association'), 'name': '#', 'submenu': [
{'title': _('Board'), 'name': '#'},
{'title': _('Committees'), 'name': '#'},
{'title': _('Members'), 'name': '#'},
{'title': _('Documents'), 'name': '#'},
{'title': _('Members'), 'name': '#'},
{'title': _('Sister Associations'), 'name': '#'},
{'title': _('Become Member'), 'name': '#'},
{'title': _('Thabloid'), 'name': '#'},
]},
{'title': _('For Members'), 'name': '#', 'submenu': [
{'title': _('Photos'), 'name': '#'},
{'title': _('Statistics'), 'name': '#'},
{'title': _('Become Active'), 'name': '#'},
{'title': _('Wiki'), 'name': '#'},
]},
{'title': _('Calendar'), 'name': '#'},
{'title': _('Career'), 'name': '#', 'submenu': [
{'title': _('Sponsor'), 'name': '#'},
{'title': _('Vacancies'), 'name': '#'},
]},
{'title': _('Education'), 'name': '#', 'submenu': [
{'title': _('Book Sale'), 'name': '#'},
{'title': _('Course Overview'), 'name': '#'},
{'title': _('Add Exam'), 'name': '#'},
{'title': _('Add Summary'), 'name': '#'},
]},
{'title': _('Contact'), 'name': '#'},
]
# flake8: noqa
from .settings import *
try:
from .localsettings import *
except ImportError:
pass
......@@ -14,8 +14,9 @@ import os
from django.utils.translation import ugettext_lazy as _
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'..', '..'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
......@@ -38,6 +39,8 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Dependencies
'static_precompiler',
# Our apps
'thaliawebsite', # include for admin settings
'members',
......@@ -141,7 +144,18 @@ LOCALE_PATHS = ('locale',)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/dev/howto/static-files/
# Where to store uploaded files
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Where to store uploaded files
MEDIA_ROOT = './media/'
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other finders
'static_precompiler.finders.StaticPrecompilerFinder',
)
# Precompiler settings
STATIC_PRECOMPILER_LIST_FILES = True
This diff is collapsed.
/*! fancyBox v2.1.5 fancyapps.com | fancyapps.com/fancybox/#license */
.fancybox-wrap,
.fancybox-skin,
.fancybox-outer,
.fancybox-inner,
.fancybox-image,
.fancybox-wrap iframe,
.fancybox-wrap object,
.fancybox-nav,
.fancybox-nav span,
.fancybox-tmp
{
padding: 0;
margin: 0;
border: 0;
outline: none;
vertical-align: top;
}
.fancybox-wrap {
position: absolute;
top: 0;
left: 0;
z-index: 8020;
}
.fancybox-skin {
position: relative;
background: #f9f9f9;
color: #444;
text-shadow: none;