Skip to content

Commit 6606095

Browse files
authoredFeb 16, 2024··
Add link and sector endpoints (#199)
* Add endpoints for Link and Sector with recursive object resolution * Add test for monster recursive result from link field * Fix test failures from rebase
1 parent b33b018 commit 6606095

File tree

6 files changed

+532
-4
lines changed

6 files changed

+532
-4
lines changed
 

‎src/meshapi/serializers/model_api.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from rest_framework.fields import empty
88
from rest_framework.serializers import raise_errors_on_nested_writes
99

10-
from meshapi.models import Building, Install, Member
10+
from meshapi.models import Building, Install, Link, Member, Sector
1111
from meshapi.permissions import check_has_model_view_permission
1212

1313

@@ -148,9 +148,41 @@ class Meta:
148148
building = BuildingSerializer(exclude_fields=["installs"])
149149

150150

151+
class LinkSerializer(RecursiveSerializer):
152+
class Meta:
153+
model = Link
154+
fields = "__all__"
155+
156+
from_building = BuildingSerializer(exclude_fields=["links_from", "links_to"])
157+
to_building = BuildingSerializer(exclude_fields=["links_from", "links_to"])
158+
159+
160+
class SectorSerializer(RecursiveSerializer):
161+
class Meta:
162+
model = Sector
163+
fields = "__all__"
164+
165+
building = BuildingSerializer(exclude_fields=["sectors"])
166+
167+
151168
# This is a bit hacky, but gets around the fact that we can't call InstallSerializer() until after
152169
# MemberSerializer has already been declared
153170
MemberSerializer._declared_fields["installs"] = InstallSerializer(exclude_fields=["member"], many=True, read_only=True)
154171
BuildingSerializer._declared_fields["installs"] = InstallSerializer(
155172
exclude_fields=["building"], many=True, read_only=True
156173
)
174+
BuildingSerializer._declared_fields["links_from"] = LinkSerializer(
175+
exclude_fields=["building"],
176+
many=True,
177+
read_only=True,
178+
)
179+
BuildingSerializer._declared_fields["links_to"] = LinkSerializer(
180+
exclude_fields=["building"],
181+
many=True,
182+
read_only=True,
183+
)
184+
BuildingSerializer._declared_fields["sectors"] = SectorSerializer(
185+
exclude_fields=["building"],
186+
many=True,
187+
read_only=True,
188+
)

‎src/meshapi/tests/test_link.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import json
2+
3+
from django.contrib.auth.models import User
4+
from django.test import Client, TestCase
5+
6+
from ..models import Building, Link
7+
8+
9+
class TestLink(TestCase):
10+
c = Client()
11+
12+
def setUp(self):
13+
self.admin_user = User.objects.create_superuser(
14+
username="admin", password="admin_password", email="admin@example.com"
15+
)
16+
self.c.login(username="admin", password="admin_password")
17+
18+
self.building_1 = Building(
19+
id=1,
20+
building_status=Building.BuildingStatus.ACTIVE,
21+
address_truth_sources="",
22+
latitude=0,
23+
longitude=0,
24+
altitude=0,
25+
invalid=True,
26+
)
27+
self.building_1.save()
28+
29+
self.building_2 = Building(
30+
id=2,
31+
building_status=Building.BuildingStatus.ACTIVE,
32+
address_truth_sources="",
33+
latitude=0,
34+
longitude=0,
35+
altitude=0,
36+
invalid=True,
37+
)
38+
self.building_2.save()
39+
40+
def test_new_link(self):
41+
response = self.c.post(
42+
"/api/v1/links/",
43+
{
44+
"from_building": self.building_1.id,
45+
"to_building": self.building_2.id,
46+
"status": "Active",
47+
},
48+
)
49+
code = 201
50+
self.assertEqual(
51+
code,
52+
response.status_code,
53+
f"status code incorrect. Should be {code}, but got {response.status_code}",
54+
)
55+
56+
def test_broken_link(self):
57+
response = self.c.post(
58+
"/api/v1/links/",
59+
{
60+
"from_building": "",
61+
"to_building": self.building_2.id,
62+
"status": "Active",
63+
},
64+
)
65+
code = 400
66+
self.assertEqual(
67+
code,
68+
response.status_code,
69+
f"status code incorrect. Should be {code}, but got {response.status_code}",
70+
)
71+
72+
def test_recursive_get(self):
73+
link = Link(
74+
from_building=self.building_1,
75+
to_building=self.building_2,
76+
status=Link.LinkStatus.ACTIVE,
77+
)
78+
link.save()
79+
80+
response = self.c.get(f"/api/v1/links/{link.id}/")
81+
82+
code = 200
83+
self.assertEqual(
84+
code,
85+
response.status_code,
86+
f"status code incorrect. Should be {code}, but got {response.status_code}",
87+
)
88+
89+
response_obj = json.loads(response.content)
90+
self.assertEqual(response_obj["status"], "Active")
91+
self.assertEqual(
92+
response_obj["from_building"],
93+
{
94+
"id": 1,
95+
"address_truth_sources": "",
96+
"altitude": 0.0,
97+
"bin": None,
98+
"building_status": "Active",
99+
"city": None,
100+
"installs": [],
101+
"sectors": [],
102+
"invalid": True,
103+
"latitude": 0.0,
104+
"longitude": 0.0,
105+
"node_name": None,
106+
"notes": None,
107+
"primary_nn": None,
108+
"state": None,
109+
"street_address": None,
110+
"zip_code": None,
111+
},
112+
)
113+
self.assertEqual(
114+
response_obj["to_building"],
115+
{
116+
"id": 2,
117+
"address_truth_sources": "",
118+
"altitude": 0.0,
119+
"bin": None,
120+
"building_status": "Active",
121+
"city": None,
122+
"installs": [],
123+
"sectors": [],
124+
"invalid": True,
125+
"latitude": 0.0,
126+
"longitude": 0.0,
127+
"node_name": None,
128+
"notes": None,
129+
"primary_nn": None,
130+
"state": None,
131+
"street_address": None,
132+
"zip_code": None,
133+
},
134+
)

‎src/meshapi/tests/test_recursive_views.py

+217-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib.auth.models import Permission, User
44
from django.test import Client, TestCase
55

6-
from meshapi.models import Building, Install, Member
6+
from meshapi.models import Building, Install, Link, Member, Sector
77
from meshapi.tests.sample_data import sample_building, sample_install, sample_member
88

99

@@ -25,6 +25,8 @@ def setup_objects():
2525
install_obj = Install(**inst)
2626
install_obj.save()
2727

28+
return member_obj, building_obj, install_obj
29+
2830

2931
class TestViewsGetLimitedPermissions(TestCase):
3032
c = Client()
@@ -68,6 +70,9 @@ def test_views_get_install(self):
6870
"zip_code": "11111",
6971
"invalid": False,
7072
"address_truth_sources": "['NYCPlanningLabs']",
73+
"links_from": [],
74+
"links_to": [],
75+
"sectors": [],
7176
"latitude": 0.0,
7277
"longitude": 0.0,
7378
"altitude": 0.0,
@@ -186,6 +191,9 @@ def test_views_get_install(self):
186191
"zip_code": "11111",
187192
"invalid": False,
188193
"address_truth_sources": "['NYCPlanningLabs']",
194+
"links_from": [],
195+
"links_to": [],
196+
"sectors": [],
189197
"latitude": 0.0,
190198
"longitude": 0.0,
191199
"altitude": 0.0,
@@ -216,6 +224,9 @@ def test_views_get_member(self):
216224
"zip_code": "11111",
217225
"invalid": False,
218226
"address_truth_sources": "['NYCPlanningLabs']",
227+
"links_from": [],
228+
"links_to": [],
229+
"sectors": [],
219230
"latitude": 0.0,
220231
"longitude": 0.0,
221232
"altitude": 0.0,
@@ -276,6 +287,211 @@ def test_views_get_building(self):
276287
)
277288

278289

290+
class TestMonsterQuery(TestCase):
291+
c = Client()
292+
293+
def setUp(self):
294+
# Create sample data
295+
member_obj_1, building_obj_1, install_obj_1 = setup_objects()
296+
297+
member_obj_2 = Member(id=2, **sample_member)
298+
member_obj_2.name = "Donald Smith"
299+
member_obj_2.save()
300+
301+
building = sample_building.copy()
302+
building["primary_nn"] = None
303+
building_obj_2 = Building(id=2, **building)
304+
building_obj_2.save()
305+
inst = sample_install.copy()
306+
307+
if inst["abandon_date"] == "":
308+
inst["abandon_date"] = None
309+
310+
inst["building"] = building_obj_2
311+
inst["member"] = member_obj_2
312+
inst["install_number"] = 2001
313+
install_obj_2 = Install(**inst)
314+
install_obj_2.save()
315+
316+
sector_1 = Sector(
317+
id=1,
318+
name="Vernon",
319+
device_name="LAP-120",
320+
building=building_obj_1,
321+
status="Active",
322+
azimuth=0,
323+
width=120,
324+
radius=0.3,
325+
)
326+
sector_1.save()
327+
328+
sector_2 = Sector(
329+
id=2,
330+
name="Vernon",
331+
device_name="LAP-120",
332+
building=building_obj_2,
333+
status="Active",
334+
azimuth=0,
335+
width=120,
336+
radius=0.3,
337+
)
338+
sector_2.save()
339+
340+
link = Link(
341+
id=1,
342+
from_building=building_obj_1,
343+
to_building=building_obj_2,
344+
status=Link.LinkStatus.ACTIVE,
345+
)
346+
link.save()
347+
348+
self.admin_user = User.objects.create_superuser(
349+
username="admin", password="admin_password", email="admin@example.com"
350+
)
351+
352+
def test_views_get_link(self):
353+
self.maxDiff = None
354+
self.c.login(username="admin", password="admin_password")
355+
356+
response = self.c.get(f"/api/v1/links/1/")
357+
358+
code = 200
359+
self.assertEqual(
360+
code,
361+
response.status_code,
362+
f"status code incorrect. Should be {code}, but got {response.status_code}",
363+
)
364+
365+
response_obj = json.loads(response.content)
366+
self.assertEqual(response_obj["status"], "Active")
367+
self.assertEqual(
368+
response_obj["from_building"],
369+
{
370+
"id": 1,
371+
"installs": [
372+
{
373+
"install_number": 2000,
374+
"member": {
375+
"id": 1,
376+
"name": "John Smith",
377+
"primary_email_address": "john.smith@example.com",
378+
"stripe_email_address": None,
379+
"additional_email_addresses": [],
380+
"all_email_addresses": ["john.smith@example.com"],
381+
"phone_number": "555-555-5555",
382+
"slack_handle": "@jsmith",
383+
"invalid": False,
384+
"contact_notes": None,
385+
},
386+
"network_number": 2000,
387+
"install_status": "Active",
388+
"ticket_id": 69,
389+
"request_date": "2022-02-27",
390+
"install_date": "2022-03-01",
391+
"abandon_date": "9999-01-01",
392+
"unit": "3",
393+
"roof_access": True,
394+
"referral": None,
395+
"notes": "Referral: Read about it on the internet",
396+
"diy": None,
397+
}
398+
],
399+
"sectors": [
400+
{
401+
"id": 1,
402+
"name": "Vernon",
403+
"radius": 0.3,
404+
"azimuth": 0,
405+
"width": 120,
406+
"status": "Active",
407+
"install_date": None,
408+
"abandon_date": None,
409+
"device_name": "LAP-120",
410+
"ssid": None,
411+
"notes": None,
412+
}
413+
],
414+
"bin": 8888,
415+
"building_status": "Active",
416+
"street_address": "3333 Chom St",
417+
"city": "Brooklyn",
418+
"state": "NY",
419+
"zip_code": "11111",
420+
"invalid": False,
421+
"address_truth_sources": "['NYCPlanningLabs']",
422+
"latitude": 0.0,
423+
"longitude": 0.0,
424+
"altitude": 0.0,
425+
"primary_nn": None,
426+
"node_name": None,
427+
"notes": None,
428+
},
429+
)
430+
self.assertEqual(
431+
response_obj["to_building"],
432+
{
433+
"id": 2,
434+
"installs": [
435+
{
436+
"install_number": 2001,
437+
"member": {
438+
"id": 2,
439+
"name": "Donald Smith",
440+
"primary_email_address": "john.smith@example.com",
441+
"stripe_email_address": None,
442+
"additional_email_addresses": [],
443+
"all_email_addresses": ["john.smith@example.com"],
444+
"phone_number": "555-555-5555",
445+
"slack_handle": "@jsmith",
446+
"invalid": False,
447+
"contact_notes": None,
448+
},
449+
"network_number": 2000,
450+
"install_status": "Active",
451+
"ticket_id": 69,
452+
"request_date": "2022-02-27",
453+
"install_date": "2022-03-01",
454+
"abandon_date": "9999-01-01",
455+
"unit": "3",
456+
"roof_access": True,
457+
"referral": None,
458+
"notes": "Referral: Read about it on the internet",
459+
"diy": None,
460+
}
461+
],
462+
"sectors": [
463+
{
464+
"id": 2,
465+
"name": "Vernon",
466+
"radius": 0.3,
467+
"azimuth": 0,
468+
"width": 120,
469+
"status": "Active",
470+
"install_date": None,
471+
"abandon_date": None,
472+
"device_name": "LAP-120",
473+
"ssid": None,
474+
"notes": None,
475+
}
476+
],
477+
"bin": 8888,
478+
"building_status": "Active",
479+
"street_address": "3333 Chom St",
480+
"city": "Brooklyn",
481+
"state": "NY",
482+
"zip_code": "11111",
483+
"invalid": False,
484+
"address_truth_sources": "['NYCPlanningLabs']",
485+
"latitude": 0.0,
486+
"longitude": 0.0,
487+
"altitude": 0.0,
488+
"primary_nn": None,
489+
"node_name": None,
490+
"notes": None,
491+
},
492+
)
493+
494+
279495
class TestViewsPutAdmin(TestCase):
280496
c = Client()
281497

‎src/meshapi/tests/test_sector.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import json
2+
3+
from django.contrib.auth.models import User
4+
from django.test import Client, TestCase
5+
6+
from ..models import Building, Sector
7+
8+
9+
class TestSector(TestCase):
10+
c = Client()
11+
12+
def setUp(self):
13+
self.admin_user = User.objects.create_superuser(
14+
username="admin", password="admin_password", email="admin@example.com"
15+
)
16+
self.c.login(username="admin", password="admin_password")
17+
18+
self.building_1 = Building(
19+
id=1,
20+
building_status=Building.BuildingStatus.ACTIVE,
21+
address_truth_sources="",
22+
latitude=0,
23+
longitude=0,
24+
altitude=0,
25+
invalid=True,
26+
)
27+
self.building_1.save()
28+
29+
def test_new_sector(self):
30+
response = self.c.post(
31+
"/api/v1/sectors/",
32+
{
33+
"name": "Vernon",
34+
"device_name": "LAP-120",
35+
"building": self.building_1.id,
36+
"status": "Active",
37+
"azimuth": 0,
38+
"width": 120,
39+
"radius": 0.3,
40+
},
41+
)
42+
code = 201
43+
self.assertEqual(
44+
code,
45+
response.status_code,
46+
f"status code incorrect. Should be {code}, but got {response.status_code}",
47+
)
48+
49+
def test_broken_link(self):
50+
response = self.c.post(
51+
"/api/v1/sectors/",
52+
{
53+
"name": "Vernon",
54+
"device_name": "",
55+
"building": "",
56+
"status": "",
57+
"azimuth": 0,
58+
"width": 120,
59+
"radius": 0.3,
60+
},
61+
)
62+
code = 400
63+
self.assertEqual(
64+
code,
65+
response.status_code,
66+
f"status code incorrect. Should be {code}, but got {response.status_code}",
67+
)
68+
69+
def test_recursive_get(self):
70+
sector = Sector(
71+
id=1,
72+
name="Vernon",
73+
device_name="LAP-120",
74+
building=self.building_1,
75+
status="Active",
76+
azimuth=0,
77+
width=120,
78+
radius=0.3,
79+
)
80+
sector.save()
81+
82+
response = self.c.get(f"/api/v1/sectors/{sector.id}/")
83+
84+
code = 200
85+
self.assertEqual(
86+
code,
87+
response.status_code,
88+
f"status code incorrect. Should be {code}, but got {response.status_code}",
89+
)
90+
91+
response_obj = json.loads(response.content)
92+
self.assertEqual(response_obj["status"], "Active")
93+
self.assertEqual(
94+
response_obj["building"],
95+
{
96+
"address_truth_sources": "",
97+
"altitude": 0.0,
98+
"bin": None,
99+
"building_status": "Active",
100+
"city": None,
101+
"id": 1,
102+
"installs": [],
103+
"invalid": True,
104+
"latitude": 0.0,
105+
"links_from": [],
106+
"links_to": [],
107+
"longitude": 0.0,
108+
"node_name": None,
109+
"notes": None,
110+
"primary_nn": None,
111+
"state": None,
112+
"street_address": None,
113+
"zip_code": None,
114+
},
115+
)

‎src/meshapi/urls.py

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
path("members/<int:pk>/", views.MemberDetail.as_view(), name="meshapi-v1-member-detail"),
1515
path("installs/", views.InstallList.as_view(), name="meshapi-v1-install-list"),
1616
path("installs/<int:pk>/", views.InstallDetail.as_view(), name="meshapi-v1-install-detail"),
17+
path("links/", views.LinkList.as_view(), name="meshapi-v1-link-list"),
18+
path("links/<int:pk>/", views.LinkDetail.as_view(), name="meshapi-v1-link-detail"),
19+
path("sectors/", views.SectorList.as_view(), name="meshapi-v1-sector-list"),
20+
path("sectors/<int:pk>/", views.SectorDetail.as_view(), name="meshapi-v1-sector-detail"),
1721
path("join/", views.join_form, name="meshapi-v1-join"),
1822
path("nn-assign/", views.network_number_assignment, name="meshapi-v1-nn-assign"),
1923
path("building/lookup/", views.LookupBuilding.as_view(), name="meshapi-v1-lookup-building"),

‎src/meshapi/views/model_api.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
from rest_framework.decorators import api_view, permission_classes
44
from rest_framework.response import Response
55

6-
from meshapi.models import Building, Install, Member
6+
from meshapi.models import Building, Install, Link, Member, Sector
77
from meshapi.permissions import IsReadOnly
8-
from meshapi.serializers import BuildingSerializer, InstallSerializer, MemberSerializer, UserSerializer
8+
from meshapi.serializers import (
9+
BuildingSerializer,
10+
InstallSerializer,
11+
LinkSerializer,
12+
MemberSerializer,
13+
SectorSerializer,
14+
UserSerializer,
15+
)
916

1017

1118
# Home view
@@ -53,3 +60,23 @@ class InstallList(generics.ListCreateAPIView):
5360
class InstallDetail(generics.RetrieveUpdateDestroyAPIView):
5461
queryset = Install.objects.all()
5562
serializer_class = InstallSerializer
63+
64+
65+
class LinkList(generics.ListCreateAPIView):
66+
queryset = Link.objects.all()
67+
serializer_class = LinkSerializer
68+
69+
70+
class LinkDetail(generics.RetrieveUpdateDestroyAPIView):
71+
queryset = Link.objects.all()
72+
serializer_class = LinkSerializer
73+
74+
75+
class SectorList(generics.ListCreateAPIView):
76+
queryset = Sector.objects.all()
77+
serializer_class = SectorSerializer
78+
79+
80+
class SectorDetail(generics.RetrieveUpdateDestroyAPIView):
81+
queryset = Sector.objects.all()
82+
serializer_class = SectorSerializer

0 commit comments

Comments
 (0)
Please sign in to comment.