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

Document and clean up thaliawebsite module

Closes #586
parent d370b125
"""
The main module for the Thalia website.
This module defines settings and the URI layout.
We also handle some site-wide API stuff here.
"""
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when # This will make sure the app is always imported when
......
"""Settings for the admin site"""
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
......
"""Celery entry point"""
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import os import os
from celery import Celery from celery import Celery
...@@ -5,7 +6,7 @@ from celery import Celery ...@@ -5,7 +6,7 @@ from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'thaliawebsite.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'thaliawebsite.settings')
app = Celery('thaliawebsite') app = Celery('thaliawebsite') # pylint: disable=invalid-name
# Using a string here means the worker doesn't have to serialize # Using a string here means the worker doesn't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
......
...@@ -6,4 +6,5 @@ import os ...@@ -6,4 +6,5 @@ import os
def source_commit(_): def source_commit(_):
"""Get the SOURCE_COMMIT environment variable"""
return {'SOURCE_COMMIT': os.environ.get('SOURCE_COMMIT', 'unknown')} return {'SOURCE_COMMIT': os.environ.get('SOURCE_COMMIT', 'unknown')}
"""Special forms"""
from django.contrib.auth.forms import (AuthenticationForm as from django.contrib.auth.forms import (AuthenticationForm as
BaseAuthenticationForm) BaseAuthenticationForm)
class AuthenticationForm(BaseAuthenticationForm): class AuthenticationForm(BaseAuthenticationForm):
def __init__(self, request=None, *args, **kwargs): """Override the authentication form provided by Django"""
super(AuthenticationForm, self).__init__(request, *args, **kwargs)
def clean(self): def clean(self):
"""Lowercase the username"""
if 'username' in self.cleaned_data: if 'username' in self.cleaned_data:
self.cleaned_data['username'] = (self.cleaned_data['username'] self.cleaned_data['username'] = (self.cleaned_data['username']
.lower()) .lower())
......
"""
This file defines the menu layout.
We set the variable `:py:main` to form the menu tree.
"""
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
main = [ __all__ = ['MAIN_MENU']
#: Defines the menu layout as a nested dict.
#:
#: The authenticated key indicates something should only
#: be visible for logged-in users. **Do not** rely on that for
#: authentication!
MAIN_MENU = [
{'title': _('Home'), 'name': 'index'}, {'title': _('Home'), 'name': 'index'},
{'title': _('Association'), 'name': 'association', 'submenu': [ {
'title': _('Association'),
'name': 'association',
'submenu': [
{'title': _('Board'), 'name': 'activemembers:boards'}, {'title': _('Board'), 'name': 'activemembers:boards'},
{'title': _('Committees'), 'name': 'activemembers:committees'}, {'title': _('Committees'), 'name': 'activemembers:committees'},
{'title': _('Documents'), 'name': 'documents:index'}, {'title': _('Documents'), 'name': 'documents:index'},
...@@ -10,8 +26,11 @@ main = [ ...@@ -10,8 +26,11 @@ main = [
{'title': _('Sister Associations'), 'name': 'sister-associations'}, {'title': _('Sister Associations'), 'name': 'sister-associations'},
{'title': _('Become a Member'), 'name': 'registrations:index'}, {'title': _('Become a Member'), 'name': 'registrations:index'},
{'title': _('Thabloid'), 'name': 'thabloid:index'}, {'title': _('Thabloid'), 'name': 'thabloid:index'},
]}, ],
{'title': _('For Members'), 'name': 'for-members', },
{
'title': _('For Members'),
'name': 'for-members',
'submenu': [ 'submenu': [
{'title': _('Member list'), 'name': 'members:index'}, {'title': _('Member list'), 'name': 'members:index'},
{'title': _('Photos'), 'name': 'photos:index'}, {'title': _('Photos'), 'name': 'photos:index'},
...@@ -19,25 +38,46 @@ main = [ ...@@ -19,25 +38,46 @@ main = [
{'title': _('Styleguide'), 'name': 'styleguide'}, {'title': _('Styleguide'), 'name': 'styleguide'},
{'title': _('Become Active'), 'name': 'become-active'}, {'title': _('Become Active'), 'name': 'become-active'},
{'title': _('Wiki'), 'url': '/wiki/', 'authenticated': True}, {'title': _('Wiki'), 'url': '/wiki/', 'authenticated': True},
]}, ],
{'title': _('Calendar'), 'name': 'events:index', },
{
'title': _('Calendar'),
'name': 'events:index',
'submenu': [ 'submenu': [
{'title': _('Order Pizza'), 'name': 'pizzas:index'}, {'title': _('Order Pizza'), 'name': 'pizzas:index'},
]}, ],
{'title': _('Career'), 'name': 'partners:index', 'submenu': [ },
{
'title': _('Career'),
'name': 'partners:index',
'submenu': [
{'title': _('Partners'), 'name': 'partners:index'}, {'title': _('Partners'), 'name': 'partners:index'},
{'title': _('Vacancies'), 'name': 'partners:vacancies'}, {'title': _('Vacancies'), 'name': 'partners:vacancies'},
]}, ],
{'title': _('Education'), 'name': 'education:index', 'submenu': [ },
{
'title': _('Education'),
'name': 'education:index',
'submenu': [
{'title': _('Book Sale'), 'name': 'education:books'}, {'title': _('Book Sale'), 'name': 'education:books'},
{'title': _('Student Participation'), {
'name': 'education:student-participation'}, 'title': _('Student Participation'),
{'title': _('Course Overview'), 'name': 'education:courses', 'name': 'education:student-participation'
},
{
'title': _('Course Overview'), 'name': 'education:courses',
'submenu': [ 'submenu': [
{'title': _('Submit Exam'), 'name': 'education:submit-exam'}, {
{'title': _('Submit Summary'), 'title': _('Submit Exam'),
'name': 'education:submit-summary'}, 'name': 'education:submit-exam'
]}, },
]}, {
'title': _('Submit Summary'),
'name': 'education:submit-summary'
},
],
},
]
},
{'title': _('Contact'), 'name': 'contact'}, {'title': _('Contact'), 'name': 'contact'},
] ]
...@@ -6,27 +6,26 @@ This file controls what settings are loaded. ...@@ -6,27 +6,26 @@ This file controls what settings are loaded.
Using environment variables you can control the loading of various Using environment variables you can control the loading of various
overrides. overrides.
""" """
# flake8: noqa import os
# Load all default settings because we need to use settings.configure # Load all default settings because we need to use settings.configure
# for sphinx documentation generation. # for sphinx documentation generation.
from django.conf.global_settings import * from django.conf.global_settings import * # pylint: disable=wildcard-import
import os
# Load base settings # Load base settings
from .settings import * from .settings import * # pylint: disable=wildcard-import
# Attempt to load local overrides # Attempt to load local overrides
try: try:
from .localsettings import * from .localsettings import * # pylint: disable=wildcard-import
except ImportError: except ImportError:
pass pass
# Load production settings if DJANGO_PRODUCTION is set # Load production settings if DJANGO_PRODUCTION is set
if os.environ.get('DJANGO_PRODUCTION'): # pragma: nocover if os.environ.get('DJANGO_PRODUCTION'): # pragma: nocover
from .production import * from .production import * # pylint: disable=wildcard-import
# Load testing settings if GITLAB_CI is set # Load testing settings if GITLAB_CI is set
if os.environ.get('GITLAB_CI'): # pragma: nocover if os.environ.get('GITLAB_CI'): # pragma: nocover
from .testing import * from .testing import * # pylint: disable=wildcard-import
...@@ -30,12 +30,12 @@ PASSWORD_HASHERS = ( ...@@ -30,12 +30,12 @@ PASSWORD_HASHERS = (
) )
# Strip unneeded apps # Strip unneeded apps
[INSTALLED_APPS.remove(x) for x in ( _ = [INSTALLED_APPS.remove(x) for x in (
'corsheaders', 'corsheaders',
)] )]
# Strip unneeded middlewares # Strip unneeded middlewares
[MIDDLEWARE.remove(x) for x in ( _ = [MIDDLEWARE.remove(x) for x in (
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django.middleware.http.ConditionalGetMiddleware', 'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
......
"""Defines site maps."""
from django.contrib import sitemaps from django.contrib import sitemaps
from django.urls import reverse from django.urls import reverse
class StaticViewSitemap(sitemaps.Sitemap): class StaticViewSitemap(sitemaps.Sitemap):
"""Sitemap items for static pages"""
def items(self): def items(self):
return ['index', 'become-active', 'sister-associations', 'contact'] """
The items of the site map.
def location(self, item): :return: the items in the site map
return reverse(item) :rtype: [str]
"""
# Need to be valid entries for reverse()
return [
'index',
'become-active',
'sister-associations',
'contact',
]
def location(self, obj):
"""
Get the location for a site map item.
Example::
>>> sitemap = StaticViewSitemap()
>>> sitemap.location(sitemap.items()[0])
:param obj: the item to reverse.
:type obj: str
:return: the URI to the item.
"""
return reverse(obj)
"""Obtain the base url"""
from django.template import Library from django.template import Library
register = Library() register = Library() # pylint: disable=invalid-name
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def baseurl(context): def baseurl(context):
""" """
Return a BASE_URL template context for the current request. :return: a BASE_URL template context for the current request.
""" """
request = context['request'] request = context['request']
......
from __future__ import absolute_import, print_function, unicode_literals """
Bleach allows to clean up user input to make it safe to display, but
allow some HTML.
"""
from bleach import clean from bleach import clean
from django import template from django import template
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
register = template.Library() register = template.Library() # pylint: disable=invalid-name
def _allow_iframe_attrs(tag, name, value): def _allow_iframe_attrs(tag, name, value):
"""
Filter to allow certain attributes for tags
:param tag: the tag
:param name: the attribute name
:param value: the value of the item.
"""
# these are fine
if name in ('class', 'width', 'height', 'frameborder', 'allowfullscreen'): if name in ('class', 'width', 'height', 'frameborder', 'allowfullscreen'):
return True return True
elif tag == 'iframe' and name == 'src':
# youtube is allowed to have `src`
if tag == 'iframe' and name == 'src':
return (value.startswith('https://www.youtube.com/embed/') or return (value.startswith('https://www.youtube.com/embed/') or
value.startswith('https://www.youtube-nocookie.com/embed/')) value.startswith('https://www.youtube-nocookie.com/embed/'))
......
"""
Get the field type for a form field
"""
from django import template from django import template
register = template.Library() register = template.Library() # pylint: disable=invalid-name
@register.filter(name='fieldtype') @register.filter(name='fieldtype')
def fieldtype(field): def fieldtype(field):
"""
Get the field type for a form field.
:param field: field for which to get the field type
:return: field type
:rtype: str
"""
return field.field.widget.__class__.__name__ return field.field.widget.__class__.__name__
"""Provides a template handler that renders the menu"""
from django import template from django import template
from ..menus import main from ..menus import MAIN_MENU
register = template.Library() register = template.Library() # pylint: disable=invalid-name
@register.inclusion_tag('menu/menu.html', takes_context=True) @register.inclusion_tag('menu/menu.html', takes_context=True)
def render_main_menu(context): def render_main_menu(context):
return {'menu': main, 'request': context.get('request')} """
Renders the main menu in this place.
Accounts for logged-in status and locale.
"""
return {'menu': MAIN_MENU, 'request': context.get('request')}
"""
Get a random header image
"""
import functools import functools
import os import os
import random import random
...@@ -7,17 +10,18 @@ from django.conf import settings ...@@ -7,17 +10,18 @@ from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
register = template.Library() register = template.Library() # pylint: disable=invalid-name
bannerdir = 'images/header_banners' BANNERDIR = 'images/header_banners'
@functools.lru_cache() @functools.lru_cache()
def _banners(): def _banners():
imgdir = finders.find(bannerdir) """Get the available banners"""
imgdir = finders.find(BANNERDIR)
return [pic for pic in os.listdir(imgdir) if pic.endswith('.jpg')] return [pic for pic in os.listdir(imgdir) if pic.endswith('.jpg')]
@register.simple_tag @register.simple_tag
def pick_header_image(): def pick_header_image():
"""Renders a random header image""" """Renders a random header image"""
return settings.STATIC_URL + bannerdir + '/' + random.choice(_banners()) return settings.STATIC_URL + BANNERDIR + '/' + random.choice(_banners())
"""Tests for things provided by this module"""
import doctest import doctest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
...@@ -6,14 +7,16 @@ from django.test import TestCase, override_settings ...@@ -6,14 +7,16 @@ from django.test import TestCase, override_settings
from members.models import Profile from members.models import Profile
from thaliawebsite.templatetags import bleach_tags from thaliawebsite.templatetags import bleach_tags
from thaliawebsite import sitemaps
def load_tests(loader, tests, ignore): def load_tests(_loader, tests, _ignore):
""" """
Load all tests in this module Load all tests in this module
""" """
# Adds the doctests in bleach_tags # Adds the doctests in bleach_tags
tests.addTests(doctest.DocTestSuite(bleach_tags)) tests.addTests(doctest.DocTestSuite(bleach_tags))
tests.addTests(doctest.DocTestSuite(sitemaps))
return tests return tests
...@@ -29,12 +32,14 @@ class WikiLoginTestCase(TestCase): ...@@ -29,12 +32,14 @@ class WikiLoginTestCase(TestCase):
email='foo@bar.com', email='foo@bar.com',
password='top secret') password='top secret')
def test_login_GET_denied(self): def test_login_get_request_denied(self):
"""GET shouldn't work for the wiki API"""
response = self.client.get('/api/wikilogin') response = self.client.get('/api/wikilogin')
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
@override_settings(WIKI_API_KEY='wrongkey') @override_settings(WIKI_API_KEY='wrongkey')
def test_login_wrong_apikey(self): def test_login_wrong_apikey(self):
"""API key should be verified"""
response = self.client.post('/api/wikilogin', response = self.client.post('/api/wikilogin',
{'apikey': 'rightkey', {'apikey': 'rightkey',
'username': 'testuser', 'username': 'testuser',
...@@ -44,6 +49,7 @@ class WikiLoginTestCase(TestCase): ...@@ -44,6 +49,7 @@ class WikiLoginTestCase(TestCase):
@override_settings(WIKI_API_KEY='key') @override_settings(WIKI_API_KEY='key')
def test_login(self): def test_login(self):
"""Test a correct log in attempt"""
response = self.client.post('/api/wikilogin', response = self.client.post('/api/wikilogin',
{'apikey': 'key', {'apikey': 'key',
'user': 'testuser', 'user': 'testuser',
...@@ -59,6 +65,7 @@ class WikiLoginTestCase(TestCase): ...@@ -59,6 +65,7 @@ class WikiLoginTestCase(TestCase):
@override_settings(WIKI_API_KEY='key') @override_settings(WIKI_API_KEY='key')
def test_login_with_profile(self): def test_login_with_profile(self):
"""A user that has a profile should be able to log in"""
Profile.objects.create( Profile.objects.create(
user=self.user, user=self.user,
student_number='s1234567' student_number='s1234567'
...@@ -79,6 +86,7 @@ class WikiLoginTestCase(TestCase): ...@@ -79,6 +86,7 @@ class WikiLoginTestCase(TestCase):
@override_settings(WIKI_API_KEY='key') @override_settings(WIKI_API_KEY='key')
def test_board_permission(self): def test_board_permission(self):
"""The board should get access to the board wiki"""
self.user.user_permissions.add( self.user.user_permissions.add(
Permission.objects.get(codename='board_wiki')) Permission.objects.get(codename='board_wiki'))
response = self.client.post('/api/wikilogin', response = self.client.post('/api/wikilogin',
...@@ -95,6 +103,7 @@ class WikiLoginTestCase(TestCase): ...@@ -95,6 +103,7 @@ class WikiLoginTestCase(TestCase):
@override_settings(WIKI_API_KEY='key') @override_settings(WIKI_API_KEY='key')
def test_wrongargs(self): def test_wrongargs(self):
"""Check that the arguments are correct"""
response = self.client.post('/api/wikilogin', response = self.client.post('/api/wikilogin',
{'apikey': 'key', {'apikey': 'key',
'username': 'testuser', 'username': 'testuser',
...@@ -110,6 +119,7 @@ class WikiLoginTestCase(TestCase): ...@@ -110,6 +119,7 @@ class WikiLoginTestCase(TestCase):
@override_settings(WIKI_API_KEY='key') @override_settings(WIKI_API_KEY='key')
def test_login_wrong_password(self): def test_login_wrong_password(self):
"""Check that the password is actually checked"""
response = self.client.post('/api/wikilogin', response = self.client.post('/api/wikilogin',
{'apikey': 'key', {'apikey': 'key',
'user': 'testuser', 'user': 'testuser',
......
...@@ -40,30 +40,34 @@ from django.views.generic import TemplateView ...@@ -40,30 +40,34 @@ from django.views.generic import TemplateView
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
import members import members
from members.sitemaps import sitemap as members_sitemap
from members.views import ObtainThaliaAuthToken
from activemembers.sitemaps import sitemap as activemembers_sitemap from activemembers.sitemaps import sitemap as activemembers_sitemap