Skip to content

Commit

Permalink
Refactor query view to use Serializer instead of dataclass (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew-Dickinson authored Feb 19, 2024
1 parent 32b55b8 commit e836ea5
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 137 deletions.
7 changes: 4 additions & 3 deletions src/meshapi/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ class HasNNAssignPermission(HasDjangoPermission):
# Janky
class LegacyMeshQueryPassword(permissions.BasePermission):
def has_permission(self, request, view):
if request.query_params["password"] != os.environ.get("QUERY_PSK"):
raise PermissionDenied("Authentication Failed.")
return True
if "password" in request.query_params and request.query_params["password"] == os.environ.get("QUERY_PSK"):
return True

raise PermissionDenied("Authentication Failed.")


class LegacyNNAssignmentPassword(permissions.BasePermission):
Expand Down
42 changes: 42 additions & 0 deletions src/meshapi/serializers/query_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from rest_framework import serializers

from meshapi.models import Install


class QueryFormSerializer(serializers.ModelSerializer):
class Meta:
model = Install
fields = (
"install_number",
"street_address",
"unit",
"city",
"state",
"zip_code",
"name",
"primary_email_address",
"stripe_email_address",
"additional_email_addresses",
"notes",
"network_number",
"install_status",
)

street_address = serializers.CharField(source="building.street_address")
city = serializers.CharField(source="building.city")
state = serializers.CharField(source="building.state")
zip_code = serializers.CharField(source="building.zip_code")

name = serializers.CharField(source="member.name")
primary_email_address = serializers.CharField(source="member.primary_email_address")
stripe_email_address = serializers.CharField(source="member.stripe_email_address")
additional_email_addresses = serializers.ListField(
source="member.additional_email_addresses", child=serializers.CharField()
)

notes = serializers.SerializerMethodField("concat_all_notes")

def concat_all_notes(self, install):
return "\n".join(
[notes for notes in [install.notes, install.building.notes, install.member.contact_notes] if notes]
)
14 changes: 7 additions & 7 deletions src/meshapi/tests/test_lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def setUp(self):
m2.save()

def test_member_name_search(self):
response = self.c.get("/api/v1/member/lookup/?name=Joh")
response = self.c.get("/api/v1/members/lookup/?name=Joh")
code = 200
self.assertEqual(
code,
Expand All @@ -41,7 +41,7 @@ def test_member_name_search(self):
self.assertEqual(response_objs[0]["name"], "John Smith")

def test_member_email_search(self):
response = self.c.get("/api/v1/member/lookup/?email_address=donald")
response = self.c.get("/api/v1/members/lookup/?email_address=donald")
code = 200
self.assertEqual(
code,
Expand All @@ -53,7 +53,7 @@ def test_member_email_search(self):
self.assertEqual(len(response_objs), 1)
self.assertEqual(response_objs[0]["name"], "Donald Smith")

response = self.c.get("/api/v1/member/lookup/?email_address=smith")
response = self.c.get("/api/v1/members/lookup/?email_address=smith")
code = 200
self.assertEqual(
code,
Expand All @@ -67,7 +67,7 @@ def test_member_email_search(self):
self.assertEqual(response_objs[1]["name"], "Donald Smith")

def test_member_alt_email_search(self):
response = self.c.get("/api/v1/member/lookup/?email_address=stripe")
response = self.c.get("/api/v1/members/lookup/?email_address=stripe")
code = 200
self.assertEqual(
code,
Expand All @@ -79,7 +79,7 @@ def test_member_alt_email_search(self):
self.assertEqual(len(response_objs), 1)
self.assertEqual(response_objs[0]["name"], "Donald Smith")

response = self.c.get("/api/v1/member/lookup/?email_address=addl")
response = self.c.get("/api/v1/members/lookup/?email_address=addl")
code = 200
self.assertEqual(
code,
Expand All @@ -92,7 +92,7 @@ def test_member_alt_email_search(self):
self.assertEqual(response_objs[0]["name"], "Donald Smith")

def test_member_phone_search(self):
response = self.c.get("/api/v1/member/lookup/?phone_number=6666")
response = self.c.get("/api/v1/members/lookup/?phone_number=6666")
code = 200
self.assertEqual(
code,
Expand All @@ -105,7 +105,7 @@ def test_member_phone_search(self):
self.assertEqual(response_objs[0]["name"], "Donald Smith")

def test_member_combined_search(self):
response = self.c.get("/api/v1/member/lookup/?phone_number=555&email_address=smith&name=don")
response = self.c.get("/api/v1/members/lookup/?phone_number=555&email_address=smith&name=don")
code = 200
self.assertEqual(
code,
Expand Down
6 changes: 3 additions & 3 deletions src/meshapi/tests/test_query_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ def query(self, route, field, data):
self.assertEqual(len(resp_json), 1)

def test_query_address(self):
self.query("building", "street_address", self.install.building.street_address)
self.query("buildings", "street_address", self.install.building.street_address)

def test_query_email(self):
self.query("member", "email_address", self.install.member.primary_email_address)
self.query("members", "email_address", self.install.member.primary_email_address)

def test_query_nn(self):
self.query("install", "network_number", self.install.network_number)
self.query("installs", "network_number", self.install.network_number)
12 changes: 6 additions & 6 deletions src/meshapi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
path("sectors/<int:pk>/", views.SectorDetail.as_view(), name="meshapi-v1-sector-detail"),
path("join/", views.join_form, name="meshapi-v1-join"),
path("nn-assign/", views.network_number_assignment, name="meshapi-v1-nn-assign"),
path("building/lookup/", views.LookupBuilding.as_view(), name="meshapi-v1-lookup-building"),
path("member/lookup/", views.LookupMember.as_view(), name="meshapi-v1-lookup-member"),
path("install/lookup/", views.LookupInstall.as_view(), name="meshapi-v1-lookup-install"),
path("query/building/", views.QueryBuilding.as_view(), name="meshapi-v1-query-building"),
path("query/member/", views.QueryMember.as_view(), name="meshapi-v1-query-member"),
path("query/install/", views.QueryInstall.as_view(), name="meshapi-v1-query-install"),
path("buildings/lookup/", views.LookupBuilding.as_view(), name="meshapi-v1-lookup-building"),
path("members/lookup/", views.LookupMember.as_view(), name="meshapi-v1-lookup-member"),
path("installs/lookup/", views.LookupInstall.as_view(), name="meshapi-v1-lookup-install"),
path("query/buildings/", views.QueryBuilding.as_view(), name="meshapi-v1-query-building"),
path("query/members/", views.QueryMember.as_view(), name="meshapi-v1-query-member"),
path("query/installs/", views.QueryInstall.as_view(), name="meshapi-v1-query-install"),
path("mapdata/installs/", views.MapDataInstallList.as_view(), name="meshapi-v1-map-data-installs"),
path("mapdata/links/", views.MapDataLinkList.as_view(), name="meshapi-v1-map-data-links"),
path("mapdata/sectors/", views.MapDataSectorList.as_view(), name="meshapi-v1-map-data-sectors"),
Expand Down
195 changes: 77 additions & 118 deletions src/meshapi/views/query_api.py
Original file line number Diff line number Diff line change
@@ -1,156 +1,115 @@
from dataclasses import asdict, dataclass
from typing import Dict
from typing import Any, Dict, Optional

from rest_framework.views import APIView, Response, models, status
from rest_framework import generics
from rest_framework.views import models

from meshapi import permissions
from meshapi.models import Building, Install, Member
from meshapi.serializers.query_api import QueryFormSerializer

"""
Re-implements https://docs.nycmesh.net/installs/query/
Search by address, email, nn, install_number, or bin
Guarded by PSK
Returns:
<Query>:<Query Data>
<Install Number>, <Addy>, <Unit #>, <Name>, <Email>, <Notes>, <NN>, <Install Status>
<Install Number>, <Addy>, <Unit #>, <Name>, <Email>, <Notes>, <NN>, <Install Status>
<Install Number>, <Addy>, <Unit #>, <Name>, <Email>, <Notes>, <NN>, <Install Status>
...
Line 2 is the same no matter what(tm)
However, we return a JSON array, rather than a CSV file
"""


@dataclass
class QueryResponse:
install_number: int
street_address: str
unit: str
city: str
state: str
zip_code: int
name: str
primary_email_address: str
stripe_email_address: str
additional_email_addresses: list[str]
notes: str
network_number: int
install_status: str

@staticmethod
def from_install(install):
return QueryResponse(
install_number=install.install_number,
street_address=install.building.street_address,
city=install.building.city,
state=install.building.state,
zip_code=install.building.zip_code,
unit=install.unit,
name=install.member.name,
primary_email_address=install.member.primary_email_address,
stripe_email_address=install.member.stripe_email_address,
additional_email_addresses=install.member.additional_email_addresses,
notes=f"{install.notes}\n{install.building.notes}\n{install.member.contact_notes}",
network_number=install.network_number,
install_status=install.install_status,
)


class QueryView(APIView):
def filter_on(
self,
model: type[models.Model],
permitted_filters: Dict[str, str],
subsitutions: Dict[str, str] = None,
):
query_dict = {}
for k, v in self.request.query_params.items():
if v:
if subsitutions and k in subsitutions:
query_dict[subsitutions[k]] = v
else:
query_dict[k] = v

filter_args = {}
for k, v in query_dict.items():
if k in permitted_filters.keys():
if permitted_filters[k]:
filter_args[f"{k}__{permitted_filters[k]}"] = v
else:
filter_args[f"{k}"] = v

return model.objects.filter(**filter_args)


class QueryBuilding(QueryView):
def filter_model_on_query_params(
query_params: Dict[str, Any],
model: type[models.Model],
permitted_filters: Dict[str, Optional[str]],
subsitutions: Dict[str, str] = None,
):
query_dict = {}
for k, v in query_params.items():
if v:
if subsitutions and k in subsitutions:
query_dict[subsitutions[k]] = v
else:
query_dict[k] = v

filter_args = {}
for k, v in query_dict.items():
if k in permitted_filters.keys():
if permitted_filters[k]:
filter_args[f"{k}__{permitted_filters[k]}"] = v
else:
filter_args[f"{k}"] = v

return model.objects.filter(**filter_args)


BUILDING_FILTERS = {
"street_address": "icontains",
"zip_code": "iexact",
"city": "icontains",
"state": "icontains",
"bin": "iexact",
}


class QueryBuilding(generics.ListAPIView):
serializer_class = QueryFormSerializer
pagination_class = None
permission_classes = [permissions.LegacyMeshQueryPassword]

def get(self, request, format=None):
buildings = self.filter_on(
def get_queryset(self):
buildings = filter_model_on_query_params(
self.request.query_params,
Building,
{
"street_address": "icontains",
"zip_code": "iexact",
"city": "icontains",
"state": "icontains",
"bin": "iexact",
},
BUILDING_FILTERS,
)

responses = []
output_qs = []
for building in buildings:
for install in building.installs.all():
responses.append(asdict(QueryResponse.from_install(install)))
output_qs.extend(building.installs.all())

return output_qs

return Response(
responses,
status=status.HTTP_200_OK,
)

MEMBER_FILTERS = {
"email_address": None,
}

class QueryMember(QueryView):

class QueryMember(generics.ListAPIView):
serializer_class = QueryFormSerializer
pagination_class = None
permission_classes = [permissions.LegacyMeshQueryPassword]

def get(self, request, format=None):
members = self.filter_on(
def get_queryset(self):
members = filter_model_on_query_params(
self.request.query_params,
Member,
{
"email_address": None,
},
MEMBER_FILTERS,
{
"email_address": "primary_email_address",
},
)

responses = []
output_qs = []
for member in members:
for install in member.installs.all():
responses.append(asdict(QueryResponse.from_install(install)))
output_qs.extend(member.installs.all())

return Response(
responses,
status=status.HTTP_200_OK,
)
return output_qs


class QueryInstall(QueryView):
permission_classes = [permissions.LegacyMeshQueryPassword]
INSTALL_FILTERS = {
"network_number": None,
"install_number": None,
}

def get(self, request, format=None):
installs = self.filter_on(
Install,
{
"network_number": None,
"install_number": None,
},
)

responses = []
for install in installs:
responses.append(asdict(QueryResponse.from_install(install)))
class QueryInstall(generics.ListAPIView):
serializer_class = QueryFormSerializer
pagination_class = None
permission_classes = [permissions.LegacyMeshQueryPassword]

return Response(
responses,
status=status.HTTP_200_OK,
def get_queryset(self):
return filter_model_on_query_params(
self.request.query_params,
Install,
INSTALL_FILTERS,
)

0 comments on commit e836ea5

Please sign in to comment.