Skip to content

Commit 456dfc8

Browse files
zaryapre-commit-ci[bot]tykling
authored
Add IPEI to DECT registrations (#1825)
* Add IPEI to DECT registrations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/phonebook/views.py * Fixed review remarks * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/phonebook/dectutils.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/phonebook/views.py * Update scope description * Update src/bornhack/settings.py * Update src/phonebook/views.py * Add the forgotten migration * Removed unused * One more --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org>
1 parent 5a20b4e commit 456dfc8

9 files changed

Lines changed: 215 additions & 5 deletions

File tree

src/bornhack/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@
233233
"permissions:read": "Allow the remote site to read a list of your assigned permissions (scope: permissions:read).",
234234
"teams:read": "Allow the remote site to read a list of your team memberships and team lead status (scope: teams)",
235235
# api scopes
236-
"phonebook:admin": "Allow the remote site to read the camp phonebook including service numbers and unlisted numbers. Only relevant for POC team leads (scope: phonebook:admin).",
236+
"phonebook:admin": "Allow the remote site to read the camp phonebook including service numbers and unlisted numbers. Also allows access to the POC API. Only relevant for POC team leads (scope: phonebook:admin).",
237237
"phonebook:read": "Allow the remote site to read the camp phonebook (scope: phonebook:read).",
238238
},
239239
"PKCE_REQUIRED": True,

src/phonebook/dectutils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,23 @@ def letters_to_number(self, letters):
5353
for letter in letters:
5454
result += self.REVERSE_DECT_MATRIX[letter.upper()]
5555
return result
56+
57+
def hex_ipui_ipei(self, ipui):
58+
"""
59+
Convert a hexidecimal IPUI to a IPEI notation
60+
"""
61+
if len(ipui) == 10:
62+
emc_hex = ipui[:5]
63+
psn_hex = ipui[-5:]
64+
emc = int(emc_hex, 16)
65+
psn = int(psn_hex, 16)
66+
return [emc, psn]
67+
return []
68+
69+
def format_ipei(self, emc, psn):
70+
"""
71+
Format the IPEI stored as ints to the standard notation.
72+
"""
73+
emc_s = str(emc).zfill(5)
74+
psn_s = str(psn).zfill(7)
75+
return f"{emc_s} {psn_s}"

src/phonebook/forms.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Forms for the phonebook
3+
"""
4+
5+
import re
6+
7+
from django import forms
8+
9+
from django.core.exceptions import ValidationError
10+
11+
from .models import DectRegistration
12+
from .dectutils import DectUtils
13+
14+
dectutil = DectUtils()
15+
16+
17+
class DectRegistrationForm(forms.ModelForm):
18+
"""
19+
Dect Registration Form used in the phonebook registration create view
20+
"""
21+
22+
ipei_help_text = """
23+
Optional: Enter your IPEI (03562 0900847) or IPUI (00DEADBEEF)<br>
24+
Entering this will enable you to register your handset directly when you arrive<br>
25+
You can find your IPUI on most Siemens handsets by going to menu and dailing *#06#
26+
"""
27+
ipei = forms.CharField(
28+
max_length=13,
29+
help_text=ipei_help_text,
30+
required=False,
31+
label="IPEI/IPUI",
32+
)
33+
34+
class Meta:
35+
model = DectRegistration
36+
fields = ["number", "letters", "description", "publish_in_phonebook", "ipei"]
37+
38+
def clean_ipei(self):
39+
"""
40+
Detect IPEI type and convert both IPEI or IPUI to a array of ints
41+
"""
42+
ipei_s = self.cleaned_data["ipei"]
43+
if len(ipei_s) == 10:
44+
ipei = dectutil.hex_ipui_ipei(ipei_s)
45+
elif len(ipei_s) == 13:
46+
if re.match(r"^\d{5} \d{7}$", ipei_s):
47+
ipei = [int(a) for a in ipei_s.split(" ")]
48+
else:
49+
raise ValidationError("Unrecognized IPEI format")
50+
elif ipei_s == "":
51+
return None
52+
else:
53+
raise ValidationError("Unable to recognize IPEI/IPUI format")
54+
55+
if not ipei:
56+
raise ValidationError(f"unable to process {ipei}.")
57+
return ipei
58+
59+
def clean(self):
60+
cleaned_data = super().clean()
61+
ipei = cleaned_data.get("ipei")
62+
63+
if ipei and len(ipei) != 2:
64+
self.add_error("ipei", f"The ipei is incorrect. {ipei}")
65+
return cleaned_data
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.20 on 2025-04-20 07:23
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('phonebook', '0003_auto_20200310_1923'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='dectregistration',
16+
name='ipei',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, help_text='DECT phone IPEI (EMC+PSN)', null=True, size=2),
18+
),
19+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.20 on 2025-04-21 12:28
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('phonebook', '0004_dectregistration_ipei'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='dectregistration',
16+
name='ipei',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, help_text='DECT phone IPEI (03562,0900847)', null=True, size=2),
18+
),
19+
]

src/phonebook/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.contrib.auth.models import User
44
from django.core.exceptions import ValidationError
55
from django.db import models
6+
from django.contrib.postgres.fields import ArrayField
67
from django_prometheus.models import ExportModelOperationsMixin
78

89
from .dectutils import DectUtils
@@ -63,15 +64,36 @@ class Meta:
6364
help_text="Check to list this registration in the phonebook",
6465
)
6566

67+
ipei = ArrayField(
68+
models.IntegerField(),
69+
blank=True,
70+
null=True,
71+
size=2,
72+
help_text="DECT phone IPEI (03562,0900847)",
73+
)
74+
6675
def save(self, *args, **kwargs):
6776
"""
6877
This is just here so we get the validation in the admin as well.
6978
"""
7079
# validate that the phonenumber and letters are valid and then save()
7180
self.clean_number()
7281
self.clean_letters()
82+
self.check_unique_ipei()
7383
super().save(*args, **kwargs)
7484

85+
def check_unique_ipei(self):
86+
if self.ipei and len(self.ipei) == 2:
87+
# check for conflicts with the same IPEI
88+
if (
89+
DectRegistration.objects.filter(camp=self.camp, ipei=self.ipei)
90+
.exclude(pk=self.pk)
91+
.exists()
92+
):
93+
raise ValidationError(
94+
f"The IPEI {dectutil.format_ipei(self.ipei[0], self.ipei[1])} is in use",
95+
)
96+
7597
def clean_number(self):
7698
"""
7799
We call this from the views form_valid() so we have a Camp object available for the validation.

src/phonebook/templates/dectregistration_list.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<th>Description</th>
2424
<th>Publish in Phonebook</th>
2525
<th>Activation Code</th>
26+
<th>IPEI</th>
2627
<th>Created</th>
2728
<th>Modified</th>
2829
<th>Actions</th>
@@ -34,6 +35,7 @@
3435
<td>{{ entry.description|default:"N/A" }}</td>
3536
<td>{{ entry.publish_in_phonebook|yesno }}</td>
3637
<td>{{ entry.activation_code }}</td>
38+
<td><nobr>{{ entry.ipei.0|stringformat:"05d" }} {{ entry.ipei.1|stringformat:"07d" }}</nobr></td>
3739
<td>{{ entry.created }}</td>
3840
<td>{{ entry.updated }}</td>
3941
<td>

src/phonebook/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .views import DectRegistrationListView
99
from .views import DectRegistrationUpdateView
1010
from .views import PhonebookListView
11+
from .views import ApiDectUpdateIPEI
1112

1213
app_name = "phonebook"
1314
urlpatterns = [
@@ -31,6 +32,11 @@
3132
r"^(?P<dect_number>\d{4,9})/",
3233
include(
3334
[
35+
path(
36+
"update/ipei/",
37+
ApiDectUpdateIPEI.as_view(),
38+
name="dectregistration_api_update_ipei",
39+
),
3440
path(
3541
"update/",
3642
DectRegistrationUpdateView.as_view(),

src/phonebook/views.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import logging
22
import secrets
33
import string
4+
import json
45

56
from django.contrib import messages
67
from django.contrib.auth.mixins import LoginRequiredMixin
78
from django.core.exceptions import ValidationError
89
from django.shortcuts import redirect
10+
from django.shortcuts import get_object_or_404
911
from django.urls import reverse
12+
from django.http import JsonResponse
13+
from django.utils.decorators import method_decorator
14+
from django.views.decorators.csrf import csrf_exempt
1015
from django.views.generic import CreateView
1116
from django.views.generic import DeleteView
1217
from django.views.generic import ListView
1318
from django.views.generic import UpdateView
19+
from django.views.generic import View
1420
from jsonview.views import JsonView
1521
from oauth2_provider.views.generic import ScopedProtectedResourceView
1622

1723
from .mixins import DectRegistrationViewMixin
1824
from .models import DectRegistration
25+
from .dectutils import DectUtils
26+
from .forms import DectRegistrationForm
1927
from camps.mixins import CampViewMixin
20-
from teams.models import Team
2128
from utils.mixins import UserIsObjectOwnerMixin
2229

2330
logger = logging.getLogger("bornhack.%s" % __name__)
31+
dectutil = DectUtils()
2432

2533

2634
class DectExportJsonView(
@@ -36,8 +44,9 @@ class DectExportJsonView(
3644

3745
def get_context_data(self, **kwargs):
3846
context = super().get_context_data(**kwargs)
39-
team = Team.objects.get(name="POC", camp=self.camp)
40-
poc = self.request.user in team.leads and self.request.access_token.is_valid(
47+
poc = self.request.user.has_perm(
48+
"camps.poc_team_lead",
49+
) and self.request.access_token.is_valid(
4150
["phonebook:admin"],
4251
)
4352
context["phonebook"] = self.dump_phonebook(poc=poc)
@@ -59,16 +68,58 @@ def dump_phonebook(self, poc: bool) -> list[DectRegistration]:
5968
}
6069
if poc:
6170
# POC member, include extra info
71+
if dect.ipei:
72+
ipei = dectutil.format_ipei(dect.ipei[0], dect.ipei[1])
73+
else:
74+
ipei = None
6275
entry.update(
6376
{
6477
"activation_code": dect.activation_code,
6578
"publish_in_phonebook": dect.publish_in_phonebook,
79+
"ipei": ipei,
6680
},
6781
)
6882
phonebook.append(entry)
6983
return phonebook
7084

7185

86+
@method_decorator(csrf_exempt, name="dispatch")
87+
class ApiDectUpdateIPEI(
88+
CampViewMixin,
89+
ScopedProtectedResourceView,
90+
View,
91+
):
92+
"""
93+
API endpoint to update IPEI after user registered their phone on the network. Used by the POC system.
94+
"""
95+
96+
required_scopes = ["phonebook:admin"]
97+
98+
def post(self, request, dect_number, *args, **kwargs):
99+
if not self.request.user.has_perm(
100+
"camps.poc_team_lead",
101+
) and self.request.access_token.is_valid(
102+
["phonebook:admin"],
103+
):
104+
return JsonResponse({"error": "Authentication failed"}, status=403)
105+
try:
106+
data = json.loads(request.body)
107+
except json.JSONDecodeError:
108+
return JsonResponse({"error": "Invalid JSON"}, status=400)
109+
110+
instance = get_object_or_404(
111+
DectRegistration,
112+
number=dect_number,
113+
camp=self.camp,
114+
)
115+
instance.ipei = data["ipei"]
116+
instance.save()
117+
return JsonResponse(
118+
{"message": "Record updated successfully", "data": data},
119+
status=200,
120+
)
121+
122+
72123
class PhonebookListView(CampViewMixin, ListView):
73124
"""
74125
Our phonebook view currently only shows DectRegistration entries,
@@ -98,7 +149,7 @@ def get_queryset(self, *args, **kwargs):
98149

99150
class DectRegistrationCreateView(LoginRequiredMixin, CampViewMixin, CreateView):
100151
model = DectRegistration
101-
fields = ["number", "letters", "description", "publish_in_phonebook"]
152+
form_class = DectRegistrationForm
102153
template_name = "dectregistration_form.html"
103154

104155
def form_valid(self, form):
@@ -118,6 +169,12 @@ def form_valid(self, form):
118169
form.add_error("letters", E)
119170
return super().form_invalid(form)
120171

172+
try:
173+
dect.check_unique_ipei()
174+
except ValidationError as E:
175+
form.add_error("ipei", E)
176+
return super().form_invalid(form)
177+
121178
# this check needs to be in this form, but not in model.save(), because then we cant save service numbers from the admin
122179
if len(dect.number) < 4:
123180
form.add_error(

0 commit comments

Comments
 (0)