Phonebook (#465)

* first version of dect registration and phonebook functionality, missing export functionality for dect phone system, the rest should more or less work

* add a missing button and message

* fix typo

* add django-oauth-toolkit to implement oauth2 auth for the DECT csv export

* remove unused HMAC code

* add logger

* only show buttons when user is logged in

* remove unneeded enctype
This commit is contained in:
Thomas Steen Rasmussen 2020-03-05 12:31:11 +01:00 committed by GitHub
parent bf7578a833
commit c52bf300ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 731 additions and 17 deletions

View file

@ -35,5 +35,5 @@ class ContentTeamPermissionMixin(RaisePermissionRequiredMixin):
permission_required = ( permission_required = (
"camps.backoffice_permission", "camps.backoffice_permission",
"program.contentteam_permission", "camps.contentteam_permission",
) )

View file

@ -53,6 +53,7 @@ INSTALLED_APPS = [
"economy", "economy",
"wishlist", "wishlist",
"facilities", "facilities",
"phonebook",
"allauth", "allauth",
"allauth.account", "allauth.account",
"allauth_2fa", "allauth_2fa",
@ -63,6 +64,7 @@ INSTALLED_APPS = [
"django_extensions", "django_extensions",
"reversion", "reversion",
"leaflet", "leaflet",
"oauth2_provider",
] ]
# MEDIA_URL = '/media/' # MEDIA_URL = '/media/'
@ -98,6 +100,7 @@ TEMPLATES = [
] ]
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
"oauth2_provider.backends.OAuth2Backend",
"django.contrib.auth.backends.ModelBackend", # Handles login to admin with username "django.contrib.auth.backends.ModelBackend", # Handles login to admin with username
"allauth.account.auth_backends.AuthenticationBackend", # Handles regular logins "allauth.account.auth_backends.AuthenticationBackend", # Handles regular logins
) )
@ -127,6 +130,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"utils.middleware.RedirectExceptionMiddleware", "utils.middleware.RedirectExceptionMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
] ]
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True

View file

@ -25,6 +25,7 @@ from villages.views import (
admin.site.login = login_required(admin.site.login) admin.site.login = login_required(admin.site.login)
urlpatterns = [ urlpatterns = [
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("profile/", include("allauth.urls")), path("profile/", include("allauth.urls")),
path("profile/", include("allauth_2fa.urls")), path("profile/", include("allauth_2fa.urls")),
path("profile/", include("profiles.urls", namespace="profiles")), path("profile/", include("profiles.urls", namespace="profiles")),
@ -95,6 +96,12 @@ urlpatterns = [
kwargs={"page": "backoffice:index"}, kwargs={"page": "backoffice:index"},
name="backoffice_redirect", name="backoffice_redirect",
), ),
path(
"phonebook/",
CampRedirectView.as_view(),
kwargs={"page": "phonebook:list"},
name="phone_book_redirect",
),
path("people/", PeopleView.as_view(), name="people"), path("people/", PeopleView.as_view(), name="people"),
# camp specific urls below here # camp specific urls below here
path( path(
@ -142,6 +149,7 @@ urlpatterns = [
path("economy/", include("economy.urls", namespace="economy")), path("economy/", include("economy.urls", namespace="economy")),
path("wishlist/", include("wishlist.urls", namespace="wishlist")), path("wishlist/", include("wishlist.urls", namespace="wishlist")),
path("facilities/", include("facilities.urls", namespace="facilities")), path("facilities/", include("facilities.urls", namespace="facilities")),
path("phonebook/", include("phonebook.urls", namespace="phonebook")),
] ]
), ),
), ),

View file

17
src/phonebook/admin.py Normal file
View file

@ -0,0 +1,17 @@
from django.contrib import admin
from .models import DectRegistration
@admin.register(DectRegistration)
class ProfileAdmin(admin.ModelAdmin):
list_display = [
"camp",
"user",
"number",
"letters",
"description",
"activation_code",
"publish_in_phonebook",
]
list_filter = ["camp", "publish_in_phonebook", "user"]

5
src/phonebook/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PhonebookConfig(AppConfig):
name = "phonebook"

View file

@ -0,0 +1,58 @@
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
class DectUtils:
"""
This class contains dect number <> letter related utilities
"""
DECT_MATRIX = {
"2": ["A", "B", "C"],
"3": ["D", "E", "F"],
"4": ["G", "H", "I"],
"5": ["J", "K", "L"],
"6": ["M", "N", "O"],
"7": ["P", "Q", "R", "S"],
"8": ["T", "U", "V"],
"9": ["W", "X", "Y", "Z"],
}
def __init__(self):
"""
Build a reverse lookup matrix based on self.DECT_MATRIX
"""
self.REVERSE_DECT_MATRIX = {}
for digit in self.DECT_MATRIX.keys():
for letter in self.DECT_MATRIX[digit]:
self.REVERSE_DECT_MATRIX[letter] = digit
def get_dect_letter_combinations(self, numbers):
"""
Generator to recursively get all combinations of letters for this number
"""
if "0" in numbers or "1" in numbers:
logger.error(
"Numbers with 0 or 1 in them are not expressible as letters, bail out"
)
return False
# loop over the possible letters for the first digit
for letter in self.DECT_MATRIX[numbers[0]]:
# if we have more digits..
if len(numbers) > 1:
# call recursively with the remaining digits, and loop over the result
for nextletter in self.get_dect_letter_combinations(numbers[1:]):
yield letter + nextletter
else:
# no more digits left, just yield the current letter
yield letter
def letters_to_number(self, letters):
"""
Coverts "TYKL" to "8955"
"""
result = ""
for letter in letters:
result += self.REVERSE_DECT_MATRIX[letter]
return result

View file

@ -0,0 +1,87 @@
# Generated by Django 3.0.3 on 2020-02-25 18:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("camps", "0034_add_team_permission_sets"),
]
operations = [
migrations.CreateModel(
name="DectRegistration",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"number",
models.CharField(
help_text="The DECT number, numeric or as letters", max_length=9
),
),
(
"letters",
models.CharField(
blank=True,
help_text="The letters chosen to represent this DECT number in the phonebook. Optional.",
max_length=9,
),
),
(
"description",
models.TextField(
blank=True,
help_text="Description of this registration, like a name or a location or a service.",
),
),
(
"activation_code",
models.CharField(
blank=True,
help_text="The 10 digit numeric activation code",
max_length=10,
),
),
(
"publish_in_phonebook",
models.BooleanField(
default=True,
help_text="Check to list this registration in the phonebook",
),
),
(
"camp",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="dect_registrations",
to="camps.Camp",
),
),
(
"user",
models.ForeignKey(
help_text="The django user who created this DECT registration",
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={"unique_together": {("camp", "number")},},
),
]

View file

8
src/phonebook/mixins.py Normal file
View file

@ -0,0 +1,8 @@
from camps.mixins import CampViewMixin
# from .models import DectRegistration
class DectRegistrationViewMixin(CampViewMixin):
def get_object(self, *args, **kwargs):
return self.model.objects.get(camp=self.camp, number=self.kwargs["dect_number"])

130
src/phonebook/models.py Normal file
View file

@ -0,0 +1,130 @@
import logging
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from utils.models import CampRelatedModel
from .dectutils import DectUtils
logger = logging.getLogger("bornhack.%s" % __name__)
class DectRegistration(CampRelatedModel):
"""
This model contains DECT registrations for users and services
"""
class Meta:
unique_together = [("camp", "number")]
camp = models.ForeignKey(
"camps.Camp", related_name="dect_registrations", on_delete=models.PROTECT,
)
user = models.ForeignKey(
User,
on_delete=models.PROTECT,
help_text="The django user who created this DECT registration",
)
number = models.CharField(
max_length=9, help_text="The DECT number, numeric or as letters",
)
letters = models.CharField(
max_length=9,
blank=True,
help_text="The letters chosen to represent this DECT number in the phonebook. Optional.",
)
description = models.TextField(
blank=True,
help_text="Description of this registration, like a name or a location or a service.",
)
activation_code = models.CharField(
max_length=10, blank=True, help_text="The 10 digit numeric activation code",
)
publish_in_phonebook = models.BooleanField(
default=True, help_text="Check to list this registration in the phonebook",
)
def save(self, *args, **kwargs):
"""
This is just here so we get the validation in the admin as well.
"""
self.clean_number()
self.clean_letters()
super().save(*args, **kwargs)
def clean_number(self):
"""
We call this from the views form_valid() so we have a Camp object available for the validation.
This code really belongs in model.clean(), but that gets called before form_valid()
which is where we set the Camp object for the model instance.
"""
# check for conflicts with the same number
if (
DectRegistration.objects.filter(camp=self.camp, number=self.number)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError("This DECT number is in use")
# check for conflicts with a longer number
if (
DectRegistration.objects.filter(
camp=self.camp, number__startswith=self.number
)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError(
"This DECT number is not available, it conflicts with a longer number."
)
# check if a shorter number is blocking
i = len(self.number) - 1
while i:
if (
DectRegistration.objects.filter(camp=self.camp, number=self.number[:i])
.exclude(pk=self.pk)
.exists()
):
raise ValidationError(
"This DECT number is not available, it conflicts with a shorter number."
)
i -= 1
def clean_letters(self):
"""
We call this from the views form_valid() so we have a Camp object available for the validation.
This code really belongs in model.clean(), but that gets called before form_valid()
which is where we set the Camp object for the model instance.
"""
# if we have a letter representation of this number they should have the same length
if self.letters:
if len(self.letters) != len(self.number):
raise ValidationError(
f"Wrong number of letters ({len(self.letters)}) - should be {len(self.number)}"
)
# loop over the digits in the phonenumber
dectutil = DectUtils()
combinations = list(dectutil.get_dect_letter_combinations(self.number))
if not combinations:
raise ValidationError(
"Numbers with 0 and 1 in them can not be expressed as letters"
)
if self.letters not in list(combinations):
# something is fucky, loop over letters to give a better error message
i = 0
for digit in self.number:
if self.letters[i].upper() not in dectutil.DECT_MATRIX[digit]:
raise ValidationError(
f"The digit '{digit}' does not match the letter '{self.letters[i]}'. Valid letters for the digit '{digit}' are: {dectutil.DECT_MATRIX[digit]}"
)
i += 1

View file

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
Delete DECT Registration | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="h3">Really delete DECT registration {{ dectregistration.number }}?</span>
</div>
<div class="panel-body">
<p class="lead">Are you sure you want to delete the registration of DECT number {{ dectregistration.number }}?
<ul>
<li>Letters: {{ dectregistration.letters|default:"N/A" }}</li>
<li>Description: {{ dectregistration.description|default:"N/A" }}</li>
</ul>
<form method="post">
{% csrf_token %}
<button class="btn btn-danger" type="submit"><i class="fas fa-times"></i> Delete DECT Registration</button>
<a href="{% url 'phonebook:dectregistration_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends 'program_base.html' %}
{% load bootstrap3 %}
{% block title %}
{% if request.resolver_match.url_name == "dectregistration_update" %}Update{% else %}Create{% endif %} DECT Registration | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="h3">{% if request.resolver_match.url_name == "dectregistration_update" %}Update{% else %}Create New{% endif %} DECT Registration</span>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-success">
<i class="fas fa-check"></i>
{% if request.resolver_match.url_name == "dectregistration_update" %}Update{% else %}Create{% endif %} DECT Registration</button>
<a href="{% url 'phonebook:dectregistration_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}
Your DECT Registrations | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="h3">Your {{ camp.title }} DECT Registrations</span>
</div>
<div class="panel-body">
<p class="lead">This is a list of all the registered DECT numbers you have for {{ camp.title }}. To activate a handset just dial the <i>activation code</i> from the handset and follow instructions.</p>
{% if dectregistration_list %}
<p>
<a href="{% url 'phonebook:list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Phonebook</a>
<a href="{% url 'phonebook:dectregistration_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create DECT Registration</a>
</p>
<table class="table table-hover table-striped">
<tr>
<th>Number</th>
<th>Letters</th>
<th>Description</th>
<th>Publish in Phonebook</th>
<th>Activation Code</th>
<th>Created</th>
<th>Modified</th>
<th>Actions</th>
</tr>
{% for entry in dectregistration_list %}
<tr>
<td>{{ entry.number }}</td>
<td>{{ entry.letters|default:"N/A" }}</td>
<td>{{ entry.description|default:"N/A" }}</td>
<td>{{ entry.publish_in_phonebook|yesno }}</td>
<td>{{ entry.activation_code }}</td>
<td>{{ entry.created }}</td>
<td>{{ entry.updated }}</td>
<td>
<a href="{% url 'phonebook:dectregistration_update' camp_slug=camp.slug dect_number=entry.number %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
<a href="{% url 'phonebook:dectregistration_delete' camp_slug=camp.slug dect_number=entry.number %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="lead"><i>No DECT registrations found. Go create one!</i></p>
<p>
<a href="{% url 'phonebook:list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Phonebook</a>
<a href="{% url 'phonebook:dectregistration_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create DECT Registration</a>
</p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}
Phonebook | {{ block.super }}
{% endblock %}
{% block extra_head %}
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
{% endblock extra_head %}
{% block content %}
<h2>{{ camp.title }} Phonebook</h2>
<p class="lead">
This is a list of all the registered DECT numbers in our phonebook for {{ camp.title }}.
</p>
{% if request.user.is_authenticated %}
<p>
<a href="{% url 'phonebook:dectregistration_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-user"></i> Your DECT Registrations</a>
<a href="{% url 'phonebook:dectregistration_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create DECT Registration</a>
</p>
{% endif %}
{% if dectregistration_list %}
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Number</th>
<th>Letters</th>
<th>Description</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for entry in dectregistration_list %}
<tr{% if entry.user == request.user %} class="info"{% endif %}>
<td>{{ entry.number }}</td>
<td>{{ entry.letters|default:"N/A" }}</td>
<td>{{ entry.description|default:"N/A" }}</td>
<td>{{ entry.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="lead"><i>No DECT registrations found</i></p>
{% endif %}
{% endblock %}

48
src/phonebook/urls.py Normal file
View file

@ -0,0 +1,48 @@
from django.urls import include, path
from .views import (
DectExportView,
DectRegistrationCreateView,
DectRegistrationDeleteView,
DectRegistrationListView,
DectRegistrationUpdateView,
PhonebookListView,
)
app_name = "phonebook"
urlpatterns = [
path("", PhonebookListView.as_view(), name="list"),
path("csv/", DectExportView.as_view(), name="csv"),
path(
"dectregistrations/",
include(
[
path(
"", DectRegistrationListView.as_view(), name="dectregistration_list"
),
path(
"create/",
DectRegistrationCreateView.as_view(),
name="dectregistration_create",
),
path(
"<int:dect_number>/",
include(
[
path(
"update/",
DectRegistrationUpdateView.as_view(),
name="dectregistration_update",
),
path(
"delete/",
DectRegistrationDeleteView.as_view(),
name="dectregistration_delete",
),
]
),
),
]
),
),
]

168
src/phonebook/views.py Normal file
View file

@ -0,0 +1,168 @@
import csv
import logging
import secrets
import string
from camps.mixins import CampViewMixin
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from oauth2_provider.views.generic import ProtectedResourceView
from utils.mixins import RaisePermissionRequiredMixin, UserIsObjectOwnerMixin
from .mixins import DectRegistrationViewMixin
from .models import DectRegistration
logger = logging.getLogger("bornhack.%s" % __name__)
class DectExportView(
CampViewMixin, RaisePermissionRequiredMixin, ProtectedResourceView
):
"""
CSV export for the POC team / DECT system
"""
permission_required = "camps.pocteam_permission"
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
response[
"Content-Disposition"
] = f'attachment; filename="{self.camp.slug}-dect-export-{timezone.now()}.csv"'
writer = csv.writer(response)
for dect in DectRegistration.objects.filter(
camp=self.camp, publish_in_phonebook=True
):
writer.writerow(
[dect.number, dect.letters, dect.description, dect.activation_code]
)
return response
class PhonebookListView(CampViewMixin, ListView):
"""
Our phonebook view currently only shows DectRegistration entries,
but could later be extended to show maybe GSM or other kinds of
phone numbers.
"""
model = DectRegistration
template_name = "phonebook.html"
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
return qs.filter(publish_in_phonebook=True)
class DectRegistrationListView(LoginRequiredMixin, CampViewMixin, ListView):
model = DectRegistration
template_name = "dectregistration_list.html"
def get_queryset(self, *args, **kwargs):
"""
Show only DectRegistration entries belonging to the current user
"""
qs = super().get_queryset(*args, **kwargs)
return qs.filter(user=self.request.user)
class DectRegistrationCreateView(LoginRequiredMixin, CampViewMixin, CreateView):
model = DectRegistration
fields = ["number", "letters", "description", "publish_in_phonebook"]
template_name = "dectregistration_form.html"
def form_valid(self, form):
dect = form.save(commit=False)
dect.camp = self.camp
dect.user = self.request.user
# this check needs to be in this form, but not in model.save(), because then we cant save service numbers from the admin
if len(dect.number) < 4:
form.add_error(
"number",
ValidationError(
"Numbers with fewer than 4 digits are reserved for special use"
),
)
return super().form_invalid(form)
try:
dect.clean_number()
except ValidationError as E:
form.add_error("number", E)
return super().form_invalid(form)
try:
dect.clean_letters()
except ValidationError as E:
form.add_error("letters", E)
return super().form_invalid(form)
# generate a 10 digit activation code for this dect registration?
if not dect.activation_code:
dect.activation_code = "".join(
secrets.choice(string.digits) for i in range(10)
)
# all good, save and return to list
dect.save()
messages.success(
self.request,
"New DECT registration created successfully. Call the activation number from your handset to activate it!",
)
return redirect(
reverse(
"phonebook:dectregistration_list", kwargs={"camp_slug": self.camp.slug}
)
)
class DectRegistrationUpdateView(
LoginRequiredMixin, DectRegistrationViewMixin, UserIsObjectOwnerMixin, UpdateView
):
model = DectRegistration
fields = ["letters", "description", "publish_in_phonebook"]
template_name = "dectregistration_form.html"
def form_valid(self, form):
dect = form.save(commit=False)
# check if the letters match the DECT number
try:
dect.clean_letters()
except ValidationError as E:
form.add_error("letters", E)
return super().form_invalid(form)
# save and return
dect.save()
messages.success(
self.request, "Your DECT registration has been updated successfully"
)
return redirect(
reverse(
"phonebook:dectregistration_list", kwargs={"camp_slug": self.camp.slug}
)
)
class DectRegistrationDeleteView(
LoginRequiredMixin, DectRegistrationViewMixin, UserIsObjectOwnerMixin, DeleteView
):
model = DectRegistration
template_name = "dectregistration_delete.html"
def get_success_url(self):
messages.success(
self.request,
f"Your DECT registration for number {self.get_object().number} has been deleted successfully",
)
return reverse(
"phonebook:dectregistration_list", kwargs={"camp_slug": self.camp.slug}
)

View file

@ -4,7 +4,7 @@ from .models import Profile
@admin.register(Profile) @admin.register(Profile)
class OrderAdmin(admin.ModelAdmin): class ProfileAdmin(admin.ModelAdmin):
actions = ["approve_public_credit_names"] actions = ["approve_public_credit_names"]
list_display = [ list_display = [

View file

@ -13,6 +13,7 @@ django-cors-headers==3.2.1
django-extensions==2.2.8 django-extensions==2.2.8
django-filter==2.2.0 django-filter==2.2.0
django-leaflet==0.26.0 django-leaflet==0.26.0
django-oauth-toolkit==1.2.0
django-reversion==3.0.7 django-reversion==3.0.7
django-wkhtmltopdf==3.3.0 django-wkhtmltopdf==3.3.0
future==0.18.2 future==0.18.2

View file

@ -6,21 +6,27 @@
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a> <a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a> <a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>
{% if request.user.is_authenticated %}
<a class="btn {% menubuttonclass 'rideshare' %}" href="{% url 'rideshare:list' camp_slug=camp.slug %}">Rideshare</a>
<a class="btn {% menubuttonclass 'feedback' %}" href="{% url 'feedback' camp_slug=camp.slug %}">Feedback</a>
{% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">More <span class="caret"></span></button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">More <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'facilities:facility_type_list' camp_slug=camp.slug %}">Facilities</a></li>
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
<li><a href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a></li>
{% endif %}
{% if perms.camps.backoffice_permission %} {% if perms.camps.backoffice_permission %}
<li><a href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a></li> <li class="{% menudropdownclass 'backoffice' %}"><a href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a></li>
{% endif %}
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
<li class="{% menudropdownclass 'economy' %}"><a href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a></li>
{% endif %}
<li class="{% menudropdownclass 'facilities' %}"><a href="{% url 'facilities:facility_type_list' camp_slug=camp.slug %}">Facilities</a></li>
{% if request.user.is_authenticated %}
<li class="{% menudropdownclass 'feedback' %}"><a href="{% url 'feedback' camp_slug=camp.slug %}">Feedback</a></li>
{% endif %}
<li class="{% menudropdownclass 'phonebook' %}"><a href="{% url 'phonebook:list' camp_slug=camp.slug %}">Phonebook</a></li>
{% if request.user.is_authenticated %}
<li class="{% menudropdownclass 'rideshare' %}"><a href="{% url 'rideshare:list' camp_slug=camp.slug %}">Rideshare</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View file

@ -1,6 +1,10 @@
import logging
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
from django.http import HttpResponseForbidden from django.core.exceptions import PermissionDenied
logger = logging.getLogger("bornhack.%s" % __name__)
class StaffMemberRequiredMixin(object): class StaffMemberRequiredMixin(object):
@ -12,7 +16,7 @@ class StaffMemberRequiredMixin(object):
# only permit staff users # only permit staff users
if not request.user.is_staff: if not request.user.is_staff:
messages.error(request, "No thanks") messages.error(request, "No thanks")
return HttpResponseForbidden() raise PermissionDenied()
# continue with the request # continue with the request
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)

View file

@ -12,3 +12,12 @@ def menubuttonclass(context, appname):
return "btn-primary" return "btn-primary"
else: else:
return "btn-default" return "btn-default"
@register.simple_tag(takes_context=True)
def menudropdownclass(context, appname):
if (
appname
== context["request"].resolver_match.func.view_class.__module__.split(".")[0]
):
return "active"