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:
parent
bf7578a833
commit
c52bf300ff
|
@ -35,5 +35,5 @@ class ContentTeamPermissionMixin(RaisePermissionRequiredMixin):
|
||||||
|
|
||||||
permission_required = (
|
permission_required = (
|
||||||
"camps.backoffice_permission",
|
"camps.backoffice_permission",
|
||||||
"program.contentteam_permission",
|
"camps.contentteam_permission",
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
0
src/phonebook/__init__.py
Normal file
0
src/phonebook/__init__.py
Normal file
17
src/phonebook/admin.py
Normal file
17
src/phonebook/admin.py
Normal 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
5
src/phonebook/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PhonebookConfig(AppConfig):
|
||||||
|
name = "phonebook"
|
58
src/phonebook/dectutils.py
Normal file
58
src/phonebook/dectutils.py
Normal 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
|
87
src/phonebook/migrations/0001_initial.py
Normal file
87
src/phonebook/migrations/0001_initial.py
Normal 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")},},
|
||||||
|
),
|
||||||
|
]
|
0
src/phonebook/migrations/__init__.py
Normal file
0
src/phonebook/migrations/__init__.py
Normal file
8
src/phonebook/mixins.py
Normal file
8
src/phonebook/mixins.py
Normal 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
130
src/phonebook/models.py
Normal 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
|
26
src/phonebook/templates/dectregistration_delete.html
Normal file
26
src/phonebook/templates/dectregistration_delete.html
Normal 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 %}
|
24
src/phonebook/templates/dectregistration_form.html
Normal file
24
src/phonebook/templates/dectregistration_form.html
Normal 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 %}
|
58
src/phonebook/templates/dectregistration_list.html
Normal file
58
src/phonebook/templates/dectregistration_list.html
Normal 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 %}
|
||||||
|
|
53
src/phonebook/templates/phonebook.html
Normal file
53
src/phonebook/templates/phonebook.html
Normal 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
48
src/phonebook/urls.py
Normal 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
168
src/phonebook/views.py
Normal 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}
|
||||||
|
)
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.backoffice_permission %}
|
||||||
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
|
<li class="{% menudropdownclass 'backoffice' %}"><a href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a></li>
|
||||||
<li><a href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a></li>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if perms.camps.backoffice_permission %}
|
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
|
||||||
<li><a href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a></li>
|
<li class="{% menudropdownclass 'economy' %}"><a href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a></li>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue