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
# This will make sure the app is always imported when
"""Settings for the admin site"""
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
"""Celery entry point"""
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
......@@ -5,7 +6,7 @@ from celery import Celery
# set the default Django settings module for the 'celery' program.
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
# the configuration object to child processes.
......@@ -6,4 +6,5 @@ import os
def source_commit(_):
"""Get the SOURCE_COMMIT environment variable"""
return {'SOURCE_COMMIT': os.environ.get('SOURCE_COMMIT', 'unknown')}
"""Special forms"""
from django.contrib.auth.forms import (AuthenticationForm as
class AuthenticationForm(BaseAuthenticationForm):
def __init__(self, request=None, *args, **kwargs):
super(AuthenticationForm, self).__init__(request, *args, **kwargs)
"""Override the authentication form provided by Django"""
def clean(self):
"""Lowercase the username"""
if 'username' in self.cleaned_data:
self.cleaned_data['username'] = (self.cleaned_data['username']
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 _
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!
{'title': _('Home'), 'name': 'index'},
{'title': _('Association'), 'name': 'association', 'submenu': [
'title': _('Association'),
'name': 'association',
'submenu': [
{'title': _('Board'), 'name': 'activemembers:boards'},
{'title': _('Committees'), 'name': 'activemembers:committees'},
{'title': _('Documents'), 'name': 'documents:index'},
......@@ -10,8 +26,11 @@ main = [
{'title': _('Sister Associations'), 'name': 'sister-associations'},
{'title': _('Become a Member'), 'name': 'registrations:index'},
{'title': _('Thabloid'), 'name': 'thabloid:index'},
{'title': _('For Members'), 'name': 'for-members',
'title': _('For Members'),
'name': 'for-members',
'submenu': [
{'title': _('Member list'), 'name': 'members:index'},
{'title': _('Photos'), 'name': 'photos:index'},
......@@ -19,25 +38,46 @@ main = [
{'title': _('Styleguide'), 'name': 'styleguide'},
{'title': _('Become Active'), 'name': 'become-active'},
{'title': _('Wiki'), 'url': '/wiki/', 'authenticated': True},
{'title': _('Calendar'), 'name': 'events:index',
'title': _('Calendar'),
'name': 'events:index',
'submenu': [
{'title': _('Order Pizza'), 'name': 'pizzas:index'},
{'title': _('Career'), 'name': 'partners:index', 'submenu': [
'title': _('Career'),
'name': 'partners:index',
'submenu': [
{'title': _('Partners'), 'name': 'partners:index'},
{'title': _('Vacancies'), 'name': 'partners:vacancies'},
{'title': _('Education'), 'name': 'education:index', 'submenu': [
'title': _('Education'),
'name': 'education:index',
'submenu': [
{'title': _('Book Sale'), 'name': 'education:books'},
{'title': _('Student Participation'),
'name': 'education:student-participation'},
{'title': _('Course Overview'), 'name': 'education:courses',
'title': _('Student Participation'),
'name': 'education:student-participation'
'title': _('Course Overview'), 'name': 'education:courses',
'submenu': [
{'title': _('Submit Exam'), 'name': 'education:submit-exam'},
{'title': _('Submit Summary'),
'name': 'education:submit-summary'},
'title': _('Submit Exam'),
'name': 'education:submit-exam'
'title': _('Submit Summary'),
'name': 'education:submit-summary'
{'title': _('Contact'), 'name': 'contact'},
......@@ -6,27 +6,26 @@ This file controls what settings are loaded.
Using environment variables you can control the loading of various
# flake8: noqa
import os
# Load all default settings because we need to use settings.configure
# 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
from .settings import *
from .settings import * # pylint: disable=wildcard-import
# Attempt to load local overrides
from .localsettings import *
from .localsettings import * # pylint: disable=wildcard-import
except ImportError:
# Load production settings if DJANGO_PRODUCTION is set
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
if os.environ.get('GITLAB_CI'): # pragma: nocover
from .testing import *
from .testing import * # pylint: disable=wildcard-import
......@@ -30,12 +30,12 @@ PASSWORD_HASHERS = (
# Strip unneeded apps
[INSTALLED_APPS.remove(x) for x in (
_ = [INSTALLED_APPS.remove(x) for x in (
# Strip unneeded middlewares
[MIDDLEWARE.remove(x) for x in (
_ = [MIDDLEWARE.remove(x) for x in (
"""Defines site maps."""
from django.contrib import sitemaps
from django.urls import reverse
class StaticViewSitemap(sitemaps.Sitemap):
"""Sitemap items for static pages"""
def items(self):
return ['index', 'become-active', 'sister-associations', 'contact']
The items of the site map.
def location(self, item):
return reverse(item)
:return: the items in the site map
:rtype: [str]
# Need to be valid entries for reverse()
return [
def location(self, obj):
Get the location for a site map item.
>>> 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
register = Library()
register = Library() # pylint: disable=invalid-name
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']
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 django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
register = template.Library()
register = template.Library() # pylint: disable=invalid-name
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'):
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
Get the field type for a form field
from django import template
register = template.Library()
register = template.Library() # pylint: disable=invalid-name
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__
"""Provides a template handler that renders the menu"""
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)
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 os
import random
......@@ -7,17 +10,18 @@ from django.conf import settings
from django.contrib.staticfiles import finders
register = template.Library()
bannerdir = 'images/header_banners'
register = template.Library() # pylint: disable=invalid-name
BANNERDIR = 'images/header_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')]
def pick_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
from django.contrib.auth import get_user_model
......@@ -6,14 +7,16 @@ from django.test import TestCase, override_settings
from members.models import Profile
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
# Adds the doctests in bleach_tags
return tests
......@@ -29,12 +32,14 @@ class WikiLoginTestCase(TestCase):
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')
self.assertEqual(response.status_code, 405)
def test_login_wrong_apikey(self):
"""API key should be verified"""
response = self.client.post('/api/wikilogin',
{'apikey': 'rightkey',
'username': 'testuser',
......@@ -44,6 +49,7 @@ class WikiLoginTestCase(TestCase):
def test_login(self):
"""Test a correct log in attempt"""
response = self.client.post('/api/wikilogin',
{'apikey': 'key',
'user': 'testuser',
......@@ -59,6 +65,7 @@ class WikiLoginTestCase(TestCase):
def test_login_with_profile(self):
"""A user that has a profile should be able to log in"""
......@@ -79,6 +86,7 @@ class WikiLoginTestCase(TestCase):
def test_board_permission(self):
"""The board should get access to the board wiki"""
response = self.client.post('/api/wikilogin',
......@@ -95,6 +103,7 @@ class WikiLoginTestCase(TestCase):
def test_wrongargs(self):
"""Check that the arguments are correct"""
response = self.client.post('/api/wikilogin',
{'apikey': 'key',
'username': 'testuser',
......@@ -110,6 +119,7 @@ class WikiLoginTestCase(TestCase):
def test_login_wrong_password(self):
"""Check that the password is actually checked"""
response = self.client.post('/api/wikilogin',
{'apikey': 'key',
'user': 'testuser',
......@@ -40,30 +40,34 @@ from django.views.generic import TemplateView
from django.views.i18n import JavaScriptCatalog
import members
from members.sitemaps import sitemap as members_sitemap
from members.views import ObtainThaliaAuthToken
from activemembers.sitemaps import sitemap as activemembers_sitemap
from documents.sitemaps import sitemap as documents_sitemap
from events.sitemaps import sitemap as events_sitemap
from members.sitemaps import sitemap as members_sitemap
from members.views import ObtainAuthToken, ObtainThaliaAuthToken
from partners.sitemaps import sitemap as partners_sitemap
from thabloid.sitemaps import sitemap as thabloid_sitemap
from thaliawebsite.forms import AuthenticationForm
from utils.views import private_thumbnails, generate_thumbnail, \
from utils.views import (generate_thumbnail, private_thumbnails,
from . import views
from .sitemaps import StaticViewSitemap
thalia_sitemap = {
__all__ = ['urlpatterns']
'main-static': StaticViewSitemap,
urlpatterns = [
# pragma pylint: disable=line-too-long
urlpatterns = [ # pylint: disable=invalid-name
url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
url(r'^privacy-policy/', TemplateView.as_view(template_name='singlepages/privacy_policy.html'), name='privacy-policy'),
url(r'^event-registration-terms/', TemplateView.as_view(template_name='singlepages/event_registration_terms.html'), name='event-registration-terms'),
......@@ -122,13 +126,13 @@ urlpatterns = [
url(r'^', include('django.contrib.auth.urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
# Sitemap
url(r'^sitemap\.xml$', sitemap, {'sitemaps': thalia_sitemap},
url(r'^sitemap\.xml$', sitemap, {'sitemaps': THALIA_SITEMAP},
# Dependencies
url(r'^tinymce/', include('tinymce.urls')),
# Javascript translation catalog
url(r'jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'),
# Provide something to test error handling. Limited to admins.
url(r'crash/$', views.crash),
] + static(settings.MEDIA_URL + 'public/',
document_root=os.path.join(settings.MEDIA_ROOT, 'public'))
"""General views for the website"""
import os.path
from django.conf import settings
......@@ -17,6 +18,7 @@ from sendfile import sendfile
def styleguide(request):
"""Static page with the style guide"""
return render(request, 'singlepages/styleguide.html')
......@@ -25,6 +27,9 @@ def styleguide(request):
def wiki_login(request):
Provides an API endpoint to the wiki to authenticate Thalia members
apikey = request.POST.get('apikey')
user = request.POST.get('user')
password = request.POST.get('password')
......@@ -39,7 +44,8 @@ def wiki_login(request):
user = authenticate(username=user, password=password)
if user is not None:
memberships = [cmm.committee.wiki_namespace for cmm in
memberships = [
cmm.committee.wiki_namespace for cmm in
......@@ -61,6 +67,7 @@ def wiki_login(request):
def styleguide_file(request, filename):
"""Obtain the styleguide files"""
path = os.path.join(settings.MEDIA_ROOT, 'styleguide')
filepath = os.path.join(path, filename)
if not (os.path.commonpath([path, filepath]) == path and
......@@ -71,6 +78,7 @@ def styleguide_file(request, filename):
def crash(request):
"""Intentionally crash to test the error handling."""
if not request.user.is_superuser:
return HttpResponseForbidden("This is not for you")
raise Exception("Test exception")
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment