Skip to content

Commit

Permalink
Merge pull request #54 from ubcuas/gcom-51/routes-implementation
Browse files Browse the repository at this point in the history
Waypoint Routes Endpoints
  • Loading branch information
Michael-R-Dickinson authored Oct 16, 2024
2 parents d9b6e3d + f1d13c9 commit 8157494
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 17 deletions.
5 changes: 3 additions & 2 deletions src/gcom/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
from django.urls import path
from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
from nav.views import WaypointViewset
from nav.views import RoutesViewset, OrderedWaypointViewset
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r"waypoint", WaypointViewset, basename="waypoint")
router.register(r"route", RoutesViewset, basename="route")
router.register(r"waypoint", OrderedWaypointViewset, basename="waypoint")

urlpatterns = [
# Swagger Docs
Expand Down
6 changes: 6 additions & 0 deletions src/nav/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from django.contrib import admin

from nav.models import Route, Waypoint

# from django.contrib import admin

# Register your models here.
admin.site.register(Waypoint)
admin.site.register(Route)
40 changes: 40 additions & 0 deletions src/nav/migrations/0002_route_waypoint_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 5.0.6 on 2024-10-06 02:03

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("nav", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="Route",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=32)),
],
),
migrations.AddField(
model_name="waypoint",
name="route",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
related_name="waypoints",
to="nav.route",
),
preserve_default=False,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.0.6 on 2024-10-06 03:03

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("nav", "0002_route_waypoint_route"),
]

operations = [
migrations.RemoveField(
model_name="waypoint",
name="route",
),
migrations.AddField(
model_name="orderedwaypoint",
name="route",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
related_name="waypoints",
to="nav.route",
),
preserve_default=False,
),
]
12 changes: 12 additions & 0 deletions src/nav/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
from django.db import models


class Route(models.Model):
"""Describes a route in GCOM
Attributes:
id: A unique identifier for the route
name: The name of the route
"""

name = models.CharField(max_length=32)


class Waypoint(models.Model):
"""Describes a position in GCOM
Expand Down Expand Up @@ -54,3 +65,4 @@ class OrderedWaypoint(Waypoint):
"""

order = models.IntegerField(null=False)
route = models.ForeignKey(Route, related_name="waypoints", on_delete=models.CASCADE)
17 changes: 14 additions & 3 deletions src/nav/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from rest_framework import serializers
from .models import Waypoint
from .models import OrderedWaypoint, Route


class WaypointSerializer(serializers.ModelSerializer):
class OrderedWaypointSerializer(serializers.ModelSerializer):
"""Serializer to convert Waypoint objects to JSON"""

class Meta:
model = Waypoint
model = OrderedWaypoint
ordering = ["order"]
fields = "__all__"


class RouteSerializer(serializers.ModelSerializer):
"""Serializer to convert Route objects to JSON"""

waypoints = OrderedWaypointSerializer(many=True, read_only=True)

class Meta:
model = Route
fields = ["id", "name", "waypoints"]
122 changes: 116 additions & 6 deletions src/nav/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from django.test import TestCase
from .models import Waypoint
from .models import OrderedWaypoint, Route, Waypoint
from rest_framework.test import APITestCase


Expand Down Expand Up @@ -41,8 +42,16 @@ def test_cannot_create_without_altitude(self):

class WaypointEndpointTests(APITestCase):
def setUp(self):
Waypoint.objects.create(
name="Test Waypoint", latitude=0, longitude=0, altitude=0
test_route = Route.objects.create(name="Test Route")
self.test_route_id = test_route.id

OrderedWaypoint.objects.create(
name="Test Waypoint",
latitude=0,
longitude=0,
altitude=0,
order=0,
route=test_route,
)

def test_get_waypoint(self):
Expand All @@ -54,7 +63,14 @@ def test_get_waypoint(self):
def test_create_waypoint(self):
response = self.client.post(
"/api/waypoint/",
{"name": "Test Waypoint 2", "latitude": 0, "longitude": 0, "altitude": 0},
{
"name": "Test Waypoint 2",
"latitude": 0,
"longitude": 0,
"altitude": 0,
"route": self.test_route_id,
"order": 1,
},
)
self.assertEqual(response.status_code, 201)
self.assertEqual(Waypoint.objects.count(), 2)
Expand All @@ -63,10 +79,17 @@ def test_create_waypoint(self):
)

def test_update_waypoint(self):
uuid = Waypoint.objects.get(name="Test Waypoint").id
uuid = OrderedWaypoint.objects.get(name="Test Waypoint").id
response = self.client.put(
f"/api/waypoint/{uuid}/",
{"name": "Updated Waypoint", "latitude": 0, "longitude": 0, "altitude": 0},
{
"name": "Updated Waypoint",
"latitude": 0,
"longitude": 0,
"altitude": 0,
"route": self.test_route_id,
"order": 1,
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Waypoint.objects.get(id=uuid).name, "Updated Waypoint")
Expand All @@ -76,3 +99,90 @@ def test_delete_waypoint(self):
response = self.client.delete(f"/api/waypoint/{uuid}/")
self.assertEqual(response.status_code, 204)
self.assertEqual(Waypoint.objects.count(), 0)


class RouteModelTest(TestCase):
def setUp(self):
Route.objects.create(name="Test Route")

def test_route(self):
route = Route.objects.get(name="Test Route")
self.assertEqual(route.name, "Test Route")

def test_cannot_create_without_name(self):
with self.assertRaises(Exception):
Route.objects.create(name=None)


class RouteEndpointTests(APITestCase):
def setUp(self):
Route.objects.create(name="Test Route")

def test_get_route(self):
response = self.client.get("/api/route/")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "Test Route")

def test_create_route(self):
response = self.client.post("/api/route/", {"name": "Test Route 2"})
self.assertEqual(response.status_code, 201)
self.assertEqual(Route.objects.count(), 2)
self.assertEqual(Route.objects.get(name="Test Route 2").name, "Test Route 2")

def test_update_route(self):
uuid = Route.objects.get(name="Test Route").id
response = self.client.put(f"/api/route/{uuid}/", {"name": "Updated Route"})
self.assertEqual(response.status_code, 200)
self.assertEqual(Route.objects.get(id=uuid).name, "Updated Route")

def test_delete_route(self):
uuid = Route.objects.get(name="Test Route").id
response = self.client.delete(f"/api/route/{uuid}/")
self.assertEqual(response.status_code, 204)
self.assertEqual(Route.objects.count(), 0)

def test_reorder_waypoints(self):
route = Route.objects.get(name="Test Route")
OrderedWaypoint.objects.create(
name="Test Waypoint 1",
latitude=0,
longitude=0,
altitude=0,
order=1,
route=route,
)
OrderedWaypoint.objects.create(
name="Test Waypoint 2",
latitude=0,
longitude=0,
altitude=0,
order=1,
route=route,
)

OrderedWaypoint.objects.create(
name="Test Waypoint 3",
latitude=0,
longitude=0,
altitude=0,
order=1,
route=route,
)

response = self.client.post(
f"/api/route/{route.id}/reorder-waypoints/",
content_type="application/json",
data=json.dumps(
[
str(OrderedWaypoint.objects.get(name="Test Waypoint 2").id),
str(OrderedWaypoint.objects.get(name="Test Waypoint 3").id),
str(OrderedWaypoint.objects.get(name="Test Waypoint 1").id),
]
),
)

self.assertEqual(response.status_code, 200)
self.assertEqual(OrderedWaypoint.objects.get(name="Test Waypoint 2").order, 0)
self.assertEqual(OrderedWaypoint.objects.get(name="Test Waypoint 3").order, 1)
self.assertEqual(OrderedWaypoint.objects.get(name="Test Waypoint 1").order, 2)
60 changes: 54 additions & 6 deletions src/nav/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,65 @@
from rest_framework import viewsets
from .models import Waypoint
from .serializers import WaypointSerializer
from .models import Route, OrderedWaypoint
from .serializers import RouteSerializer, OrderedWaypointSerializer
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status


# Create your views here.
class WaypointViewset(viewsets.ModelViewSet):
class OrderedWaypointViewset(viewsets.ModelViewSet):
"""Viewset for CRUD operations on Waypoints"""

queryset = Waypoint.objects.all()
serializer_class = WaypointSerializer
queryset = OrderedWaypoint.objects.all()
serializer_class = OrderedWaypointSerializer

def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get("data", {}), list):
kwargs["many"] = True

return super(WaypointViewset, self).get_serializer(*args, **kwargs)
return super(OrderedWaypointViewset, self).get_serializer(*args, **kwargs)


class RoutesViewset(viewsets.ModelViewSet):
"""Viewset for CRUD operations on Routes"""

queryset = Route.objects.all().prefetch_related("waypoints")
serializer_class = RouteSerializer

@action(detail=True, methods=["post"], url_path="reorder-waypoints")
def reorder_waypoints(self, request, pk=None):
"""
Action to reorder waypoints in a route
Args:
request: The request object - contains the reordered waypoint ids
pk: The primary key of the route whose waypoints we're reordering
"""
route = self.get_object()

waypoints = OrderedWaypoint.objects.filter(route=route)

if (
not request.data
or not isinstance(request.data, list)
or len(request.data) != len(waypoints)
):
return Response(
{"error": "Please provide a list of all waypoint IDs"},
status=status.HTTP_400_BAD_REQUEST,
)

reordered_waypoint_ids = request.data
for idx, waypoint_id in enumerate(reordered_waypoint_ids):
try:
waypoint = waypoints.get(id=waypoint_id)
waypoint.order = idx
waypoint.save()
except OrderedWaypoint.DoesNotExist:
return Response(
{"error": f"Waypoint with not found with id: {waypoint_id}"},
status=status.HTTP_400_BAD_REQUEST,
)

return Response(
{"success": "Waypoints reordered successfully"}, status=status.HTTP_200_OK
)

0 comments on commit 8157494

Please sign in to comment.