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 = (
|
||||
"camps.backoffice_permission",
|
||||
"program.contentteam_permission",
|
||||
"camps.contentteam_permission",
|
||||
)
|
||||
|
|
|
@ -53,6 +53,7 @@ INSTALLED_APPS = [
|
|||
"economy",
|
||||
"wishlist",
|
||||
"facilities",
|
||||
"phonebook",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth_2fa",
|
||||
|
@ -63,6 +64,7 @@ INSTALLED_APPS = [
|
|||
"django_extensions",
|
||||
"reversion",
|
||||
"leaflet",
|
||||
"oauth2_provider",
|
||||
]
|
||||
|
||||
# MEDIA_URL = '/media/'
|
||||
|
@ -98,6 +100,7 @@ TEMPLATES = [
|
|||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
"oauth2_provider.backends.OAuth2Backend",
|
||||
"django.contrib.auth.backends.ModelBackend", # Handles login to admin with username
|
||||
"allauth.account.auth_backends.AuthenticationBackend", # Handles regular logins
|
||||
)
|
||||
|
@ -127,6 +130,7 @@ MIDDLEWARE = [
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"utils.middleware.RedirectExceptionMiddleware",
|
||||
"oauth2_provider.middleware.OAuth2TokenMiddleware",
|
||||
]
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
|
|
@ -25,6 +25,7 @@ from villages.views import (
|
|||
admin.site.login = login_required(admin.site.login)
|
||||
|
||||
urlpatterns = [
|
||||
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||
path("profile/", include("allauth.urls")),
|
||||
path("profile/", include("allauth_2fa.urls")),
|
||||
path("profile/", include("profiles.urls", namespace="profiles")),
|
||||
|
@ -95,6 +96,12 @@ urlpatterns = [
|
|||
kwargs={"page": "backoffice:index"},
|
||||
name="backoffice_redirect",
|
||||
),
|
||||
path(
|
||||
"phonebook/",
|
||||
CampRedirectView.as_view(),
|
||||
kwargs={"page": "phonebook:list"},
|
||||
name="phone_book_redirect",
|
||||
),
|
||||
path("people/", PeopleView.as_view(), name="people"),
|
||||
# camp specific urls below here
|
||||
path(
|
||||
|
@ -142,6 +149,7 @@ urlpatterns = [
|
|||
path("economy/", include("economy.urls", namespace="economy")),
|
||||
path("wishlist/", include("wishlist.urls", namespace="wishlist")),
|
||||
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)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
class ProfileAdmin(admin.ModelAdmin):
|
||||
actions = ["approve_public_credit_names"]
|
||||
|
||||
list_display = [
|
||||
|
|
|
@ -13,6 +13,7 @@ django-cors-headers==3.2.1
|
|||
django-extensions==2.2.8
|
||||
django-filter==2.2.0
|
||||
django-leaflet==0.26.0
|
||||
django-oauth-toolkit==1.2.0
|
||||
django-reversion==3.0.7
|
||||
django-wkhtmltopdf==3.3.0
|
||||
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 '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">
|
||||
<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">
|
||||
<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 %}
|
||||
<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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
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):
|
||||
|
@ -12,7 +16,7 @@ class StaffMemberRequiredMixin(object):
|
|||
# only permit staff users
|
||||
if not request.user.is_staff:
|
||||
messages.error(request, "No thanks")
|
||||
return HttpResponseForbidden()
|
||||
raise PermissionDenied()
|
||||
|
||||
# continue with the request
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
|
|
@ -12,3 +12,12 @@ def menubuttonclass(context, appname):
|
|||
return "btn-primary"
|
||||
else:
|
||||
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