This is an automated email from the ASF dual-hosted git repository.

gjm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bloodhound-core.git


The following commit(s) were added to refs/heads/main by this push:
     new f1162c0  Adds components, milestones and versions to api
f1162c0 is described below

commit f1162c0b6f464071d812a856e68a9640262a8f42
Author: Gary Martin <g...@apache.org>
AuthorDate: Thu May 27 22:00:31 2021 +0100

    Adds components, milestones and versions to api
---
 pyproject.toml                         |   1 +
 trackers/api/serializers.py            | 157 ++++++++-
 trackers/api/tests/test_product_api.py |  34 +-
 trackers/api/tests/test_ticket_api.py  | 589 +++++++++++++++++++++++++++++++++
 trackers/api/urls.py                   |   9 +
 trackers/api/views.py                  |  30 ++
 trackers/models.py                     |   3 -
 7 files changed, 792 insertions(+), 31 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index fee9aad..30f3d7f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,6 +11,7 @@ django = "^3.2.2"
 django-rest-framework = "^0.1.0"
 drf-yasg = "^1.20.0"
 drf-nested-routers = "^0.93.3"
+pyyaml = "^5.4.1"
 
 [tool.poetry.dev-dependencies]
 selenium = "^3.141.0"
diff --git a/trackers/api/serializers.py b/trackers/api/serializers.py
index a48abee..58d9248 100644
--- a/trackers/api/serializers.py
+++ b/trackers/api/serializers.py
@@ -1,7 +1,48 @@
 from django.contrib.auth.models import User, Group
 from django.shortcuts import get_object_or_404
 from rest_framework import serializers
-from ..models import Product, Ticket
+from rest_framework.reverse import reverse
+from ..models import Component, Milestone, Product, Ticket, Version
+from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer
+from rest_framework_nested.relations import NestedHyperlinkedIdentityField
+from functools import partial
+
+
+def get_self_url(obj, context, obj_type):
+    keywords = {
+        'product_prefix': obj.product.prefix,
+    }
+    if obj_type == 'ticket':
+        keywords['product_ticket_id'] = obj.product_ticket_id
+    else:
+        keywords['name'] = obj.name
+
+    return reverse(
+        f'product-{obj_type}s-detail',
+        kwargs=keywords,
+        request=context['request'],
+    )
+
+
+class ProductChildSerializer(serializers.HyperlinkedModelSerializer):
+    product_url = serializers.SerializerMethodField()
+
+    def get_product_url(self, obj):
+        keywords = {
+            'prefix': obj.product.prefix,
+        }
+        return reverse(
+            'product-detail',
+            kwargs=keywords,
+            request=self.context['request']
+        )
+
+    def create(self, validated_data):
+        if 'prefix' not in self.context['view'].kwargs.keys():
+            prefix = self.context['view'].kwargs['product_prefix']
+            product = get_object_or_404(Product.objects.all(), prefix=prefix)
+            validated_data['product'] = product
+        return super().create(validated_data)
 
 
 class UserSerializer(serializers.HyperlinkedModelSerializer):
@@ -16,25 +57,113 @@ class 
GroupSerializer(serializers.HyperlinkedModelSerializer):
         fields = ('url', 'name')
 
 
-class ProductSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Product
-        fields = '__all__'
-
+class TicketSerializer(ProductChildSerializer):
+    url = serializers.SerializerMethodField()
 
-class TicketSerializer(serializers.ModelSerializer):
     class Meta:
         model = Ticket
         fields = (
+            'product_url',
+            'url',
             'product_ticket_id',
             'summary',
             'description',
         )
-        extra_kwargs = {'product_ticket_id': {'required': False}}
+        extra_kwargs = {
+            'product_ticket_id': {'required': False},
+        }
 
-    def create(self, validated_data):
-        if 'prefix' not in self.context['view'].kwargs.keys():
-            prefix = self.context['view'].kwargs['product_prefix']
-            product = get_object_or_404(Product.objects.all(), prefix=prefix)
-            validated_data['product'] = product
-        return super().create(validated_data)
+    def get_url(self, obj):
+        return get_self_url(obj, self.context, 'ticket')
+
+
+class ComponentSerializer(ProductChildSerializer):
+    url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Component
+        fields = (
+            'product_url',
+            'url',
+            'name',
+            'description',
+            'owner',
+        )
+
+    def get_url(self, obj):
+        return get_self_url(obj, self.context, 'component')
+
+
+class MilestoneSerializer(ProductChildSerializer):
+    url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Milestone
+        fields = (
+            'product_url',
+            'url',
+            'name',
+            'description',
+            'due',
+            'completed',
+        )
+
+    def get_url(self, obj):
+        return get_self_url(obj, self.context, 'milestone')
+
+
+class VersionSerializer(ProductChildSerializer):
+    url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Version
+        fields = (
+            'product_url',
+            'url',
+            'name',
+            'description',
+            'time',
+        )
+
+    def get_url(self, obj):
+        return get_self_url(obj, self.context, 'version')
+
+
+ProductHyperlinkedModelSerializer = partial(
+    serializers.HyperlinkedIdentityField,
+    lookup_field='prefix',
+    lookup_url_kwarg='product_prefix',
+)
+
+
+class ProductSerializer(serializers.HyperlinkedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='product-detail',
+        lookup_field='prefix',
+    )
+    tickets_url = ProductHyperlinkedModelSerializer(
+        view_name='product-tickets-list',
+    )
+    components_url = ProductHyperlinkedModelSerializer(
+        view_name='product-components-list',
+    )
+    milestones_url = ProductHyperlinkedModelSerializer(
+        view_name='product-milestones-list',
+    )
+    versions_url = ProductHyperlinkedModelSerializer(
+        view_name='product-versions-list',
+    )
+
+    class Meta:
+        model = Product
+        fields = (
+            'url',
+            'prefix',
+            'name',
+            'description',
+            'owner',
+            'tickets_url',
+            'components_url',
+            'milestones_url',
+            'versions_url',
+        )
diff --git a/trackers/api/tests/test_product_api.py 
b/trackers/api/tests/test_product_api.py
index 39b9243..0a43226 100644
--- a/trackers/api/tests/test_product_api.py
+++ b/trackers/api/tests/test_product_api.py
@@ -15,17 +15,16 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-from django.contrib.auth.models import User
 from django.urls import reverse
 from rest_framework.test import APITestCase
 from rest_framework import status
 
 from ...models import Product
-from ..serializers import ProductSerializer
 
 
 class ProductsApiTest(APITestCase):
-    """Test for GET all products API """
+    """Test for GET all products API"""
+
     def setUp(self):
         self.ally = Product.objects.create(prefix='ALY', name='Project Alice')
         self.bob = Product.objects.create(prefix='BOB', name='Project Robert')
@@ -47,24 +46,27 @@ class ProductsApiTest(APITestCase):
 
     def test_get_all_products(self):
         response = self.client.get(reverse('product-list'))
-        products = Product.objects.all()
-        serializer = ProductSerializer(products, many=True)
-        self.assertEqual(response.data, serializer.data)
+
         self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            len(response.data),
+            Product.objects.count(),
+        )
 
     def test_get_product(self):
         response = self.client.get(
-            reverse('product-detail', args=[self.ally.prefix])
+            reverse('product-detail', args=[self.ally.prefix]),
         )
-        product = Product.objects.get(prefix=self.ally.prefix)
-        serializer = ProductSerializer(product)
-        self.assertEqual(response.data, serializer.data)
+
         self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['prefix'], self.ally.prefix)
+        self.assertEqual(response.data['name'], self.ally.name)
 
     def test_get_invalid_product(self):
         response = self.client.get(
             reverse('product-detail', args=['randomnonsense'])
         )
+
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_create_product(self):
@@ -72,11 +74,12 @@ class ProductsApiTest(APITestCase):
             reverse('product-list'),
             self.new_product_data,
         )
+
         product = Product.objects.get(prefix=self.new_product_data['prefix'])
-        serializer = ProductSerializer(product)
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(response.data, serializer.data)
+        self.assertEqual(product.prefix, self.new_product_data['prefix'])
+        self.assertEqual(product.name, self.new_product_data['name'])
 
     def test_create_bad_product(self):
         response = self.client.post(
@@ -91,11 +94,13 @@ class ProductsApiTest(APITestCase):
             reverse('product-detail', args=[self.ally.prefix]),
             self.product_data,
         )
+
         product = Product.objects.get(prefix=self.product_data['prefix'])
-        serializer = ProductSerializer(product)
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, serializer.data)
+        self.assertNotEqual(self.ally.name, product.name)
+        self.assertEqual(self.product_data['prefix'], product.prefix)
+        self.assertEqual(self.product_data['name'], product.name)
 
     def test_update_product_bad_data(self):
         response = self.client.put(
@@ -109,4 +114,5 @@ class ProductsApiTest(APITestCase):
         response = self.client.delete(
             reverse('product-detail', args=[self.ally.prefix]),
         )
+
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
diff --git a/trackers/api/tests/test_ticket_api.py 
b/trackers/api/tests/test_ticket_api.py
new file mode 100644
index 0000000..b8afb0a
--- /dev/null
+++ b/trackers/api/tests/test_ticket_api.py
@@ -0,0 +1,589 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+from django.contrib.auth.models import User
+from django.urls import reverse
+from rest_framework.test import APITestCase
+from rest_framework import status
+
+from ...models import (
+    Component,
+    Milestone,
+    Product,
+    Ticket,
+    Version,
+)
+from ..serializers import (
+    ComponentSerializer,
+    MilestoneSerializer,
+    TicketSerializer,
+    VersionSerializer,
+)
+
+class TicketApiTest(APITestCase):
+    """Tests for ticket API"""
+
+    def setUp(self):
+        self.product = Product.objects.create(prefix="BH", name="Bloodhound")
+        self.record1 = Ticket.objects.create(product=self.product, summary="BH 
#1")
+        self.record2 = Ticket.objects.create(product=self.product, summary="BH 
#2")
+
+        self.request_data = {
+            "summary": "Example Summary",
+        }
+
+        self.bad_request_data = {
+            "summary": "",
+        }
+
+    def test_get_all_tickets(self):
+        response = self.client.get(
+            reverse("product-tickets-list", kwargs={"product_prefix": "BH"})
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            len(response.data),
+            Ticket.objects.count(),
+        )
+
+    def test_get_ticket(self):
+        response = self.client.get(
+            reverse(
+                "product-tickets-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "product_ticket_id": self.record1.product_ticket_id,
+                },
+            )
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['summary'], self.record1.summary)
+
+    def test_get_invalid_ticket(self):
+        response = self.client.get(
+            reverse(
+                "product-tickets-detail",
+                kwargs={
+                    "product_prefix": "BH",
+                    "product_ticket_id": 9999,
+                },
+            )
+        )
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_create_ticket(self):
+        response = self.client.post(
+            reverse("product-tickets-list", kwargs={"product_prefix": "BH"}),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        record = Ticket.objects.get(
+            product=self.product,
+            product_ticket_id=response.data['product_ticket_id']
+        )
+
+        self.assertEqual(response.data['summary'], record.summary)
+
+    def test_create_invalid_product(self):
+        response = self.client.post(
+            reverse(
+                'product-tickets-list',
+                kwargs={"product_prefix": "INVALID"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_create_missing_summary(self):
+        response = self.client.post(
+            reverse('product-tickets-list', kwargs={"product_prefix": "BH"}),
+            self.bad_request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_update_ticket(self):
+        response = self.client.put(
+            reverse(
+                "product-tickets-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "product_ticket_id": self.record1.product_ticket_id,
+                },
+            ),
+            {"summary": "new summary"},
+        )
+
+        old_summary = self.record1.summary
+
+        record = Ticket.objects.get(
+            product=self.product,
+            product_ticket_id=response.data['product_ticket_id']
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['summary'], record.summary)
+        self.assertNotEqual(old_summary, record.summary)
+
+    def test_update_ticket_bad_data(self):
+        response = self.client.put(
+            reverse(
+                "product-tickets-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "product_ticket_id": self.record1.product_ticket_id,
+                },
+            ),
+            {"summary": ""},
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_delete_ticket(self):
+        response = self.client.delete(
+            reverse(
+                'product-tickets-detail',
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "product_ticket_id": self.record1.product_ticket_id,
+                }
+            ),
+        )
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        with self.assertRaises(Ticket.DoesNotExist):
+            Ticket.objects.get(
+                product=self.record1.product.prefix,
+                product_ticket_id=self.record1.product_ticket_id,
+            )
+
+
+class ComponentApiTest(APITestCase):
+    """Tests for component API"""
+
+    def setUp(self):
+        self.product = Product.objects.create(prefix="BH", name="Bloodhound")
+        self.record1 = Component.objects.create(product=self.product, 
name="Component 1")
+        self.record2 = Component.objects.create(product=self.product, 
name="Component 2")
+
+        self.request_data = {
+            "name": "Example Name",
+        }
+
+        self.bad_request_data = {
+            "summary": "",
+        }
+
+    def test_get_all_components(self):
+        response = self.client.get(
+            reverse("product-components-list", kwargs={"product_prefix": "BH"})
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            len(response.data),
+            Component.objects.count(),
+        )
+
+    def test_get_component(self):
+        response = self.client.get(
+            reverse(
+                "product-components-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            )
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['name'], self.record1.name)
+
+    def test_get_missing_component(self):
+        response = self.client.get(
+            reverse(
+                "product-components-detail",
+                kwargs={
+                    "product_prefix": "BH",
+                    "name": "not here",
+                },
+            )
+        )
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_create_component(self):
+        response = self.client.post(
+            reverse(
+                "product-components-list",
+                kwargs={"product_prefix": "BH"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        record = Component.objects.get(
+            product=self.product,
+            name=response.data['name']
+        )
+
+        self.assertEqual(response.data['name'], record.name)
+
+    def test_create_component_with_invalid_product(self):
+        response = self.client.post(
+            reverse(
+                'product-components-list',
+                kwargs={"product_prefix": "INVALID"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_update_component(self):
+        new_name = "new name"
+        response = self.client.put(
+            reverse(
+                "product-components-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"name": new_name},
+        )
+
+        record = Component.objects.get(
+            product=self.product,
+            name=new_name,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertNotEqual(new_name, self.record1.name)
+        self.assertEqual(record.name, new_name)
+
+    def test_update_component_bad_data(self):
+        response = self.client.put(
+            reverse(
+                "product-components-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"summary": ""},
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_delete_component(self):
+        response = self.client.delete(
+            reverse(
+                'product-components-detail',
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                }
+            ),
+        )
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        with self.assertRaises(Component.DoesNotExist):
+            Component.objects.get(
+                product=self.record1.product.prefix,
+                name=self.record1.name,
+            )
+
+
+class MilestoneApiTest(APITestCase):
+    """Tests for component API"""
+
+    def setUp(self):
+        self.product = Product.objects.create(prefix="BH", name="Bloodhound")
+        self.record1 = Milestone.objects.create(product=self.product, 
name="Milestone 1")
+        self.record2 = Milestone.objects.create(product=self.product, 
name="Milestone 2")
+
+        self.request_data = {
+            "name": "Example Name",
+            "description": "Example Description",
+        }
+
+        self.bad_request_data = {
+            "summary": "",
+        }
+
+    def test_get_all_milestones(self):
+        response = self.client.get(
+            reverse("product-milestones-list", kwargs={"product_prefix": "BH"})
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            len(response.data),
+            Milestone.objects.count(),
+        )
+
+    def test_get_milestone(self):
+        response = self.client.get(
+            reverse(
+                "product-milestones-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            )
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['name'], self.record1.name)
+
+    def test_get_missing_milestone(self):
+        response = self.client.get(
+            reverse(
+                "product-milestones-detail",
+                kwargs={
+                    "product_prefix": "BH",
+                    "name": "not here",
+                },
+            )
+        )
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_create_milestone(self):
+        response = self.client.post(
+            reverse(
+                "product-milestones-list",
+                kwargs={"product_prefix": "BH"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        record = Milestone.objects.get(
+            product=self.product,
+            name=response.data['name']
+        )
+
+        self.assertEqual(response.data['description'], record.description)
+
+    def test_create_milestone_with_invalid_product(self):
+        response = self.client.post(
+            reverse(
+                'product-milestones-list',
+                kwargs={"product_prefix": "INVALID"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_update_component(self):
+        response = self.client.put(
+            reverse(
+                "product-milestones-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"name": "new name"},
+        )
+
+        old_name = self.record1.name
+
+        record = Milestone.objects.get(
+            product=self.product,
+            name=response.data['name']
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['description'], record.description)
+        self.assertNotEqual(old_name, record.name)
+
+    def test_update_milestone_bad_data(self):
+        response = self.client.put(
+            reverse(
+                "product-milestones-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"summary": ""},
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_delete_milestone(self):
+        response = self.client.delete(
+            reverse(
+                'product-milestones-detail',
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                }
+            ),
+        )
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        with self.assertRaises(Milestone.DoesNotExist):
+            Milestone.objects.get(
+                product=self.record1.product.prefix,
+                name=self.record1.name,
+            )
+
+
+class VersionApiTest(APITestCase):
+    """Tests for component API"""
+
+    def setUp(self):
+        self.product = Product.objects.create(prefix="BH", name="Bloodhound")
+        self.record1 = Version.objects.create(product=self.product, 
name="Version 1")
+        self.record2 = Version.objects.create(product=self.product, 
name="Version 2")
+
+        self.request_data = {
+            "name": "Example Name",
+            "description": "Example description",
+        }
+
+        self.bad_request_data = {
+            "summary": "",
+        }
+
+    def test_get_all_versions(self):
+        response = self.client.get(
+            reverse("product-versions-list", kwargs={"product_prefix": "BH"})
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            len(response.data),
+            Version.objects.count(),
+        )
+
+    def test_get_version(self):
+        response = self.client.get(
+            reverse(
+                "product-versions-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            )
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data['name'], self.record1.name)
+
+    def test_get_missing_version(self):
+        response = self.client.get(
+            reverse(
+                "product-versions-detail",
+                kwargs={
+                    "product_prefix": "BH",
+                    "name": "not here",
+                },
+            )
+        )
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_create_version(self):
+        response = self.client.post(
+            reverse(
+                "product-versions-list",
+                kwargs={"product_prefix": "BH"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        record = Version.objects.get(
+            product=self.product,
+            name=response.data['name']
+        )
+
+        self.assertEqual(response.data['description'], record.description)
+
+    def test_create_version_with_invalid_product(self):
+        response = self.client.post(
+            reverse(
+                'product-versions-list',
+                kwargs={"product_prefix": "INVALID"}
+            ),
+            self.request_data,
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+    def test_update_version(self):
+        new_name = "new name"
+        response = self.client.put(
+            reverse(
+                "product-versions-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"name": new_name},
+        )
+
+        old_name = self.record1.name
+
+        record = Version.objects.get(
+            product=self.product,
+            name=response.data['name']
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertNotEqual(new_name, old_name)
+        self.assertEqual(record.name, new_name)
+
+    def test_update_version_bad_data(self):
+        response = self.client.put(
+            reverse(
+                "product-versions-detail",
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                },
+            ),
+            {"summary": ""},
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_delete_version(self):
+        response = self.client.delete(
+            reverse(
+                'product-versions-detail',
+                kwargs={
+                    "product_prefix": self.record1.product.prefix,
+                    "name": self.record1.name,
+                }
+            ),
+        )
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+        with self.assertRaises(Version.DoesNotExist):
+            Version.objects.get(
+                product=self.record1.product.prefix,
+                name=self.record1.name,
+            )
diff --git a/trackers/api/urls.py b/trackers/api/urls.py
index 1ca3741..7dad774 100644
--- a/trackers/api/urls.py
+++ b/trackers/api/urls.py
@@ -17,6 +17,8 @@
 
 from django.urls import path
 from django.conf.urls import include
+from rest_framework.schemas import get_schema_view
+from rest_framework.renderers import JSONOpenAPIRenderer
 from rest_framework_nested import routers
 from . import views
 
@@ -27,10 +29,17 @@ router.register('products', views.ProductViewSet)
 
 products_router = routers.NestedDefaultRouter(router, 'products', 
lookup='product')
 products_router.register('tickets', views.TicketViewSet, 
basename='product-tickets')
+products_router.register('components', views.ComponentViewSet, 
basename='product-components')
+products_router.register('milestones', views.MilestoneViewSet, 
basename='product-milestones')
+products_router.register('versions', views.VersionViewSet, 
basename='product-versions')
 
 urlpatterns = [
     path('', include(router.urls)),
     path('', include(products_router.urls)),
+    path('openapi', get_schema_view(
+        title="Apache Bloodhound",
+        version="0.1.0",
+    ), name='openapi-schema'),
     path(
         'swagger<str:format>',
         views.schema_view.without_ui(cache_timeout=0),
diff --git a/trackers/api/views.py b/trackers/api/views.py
index a1d439a..7995b5f 100644
--- a/trackers/api/views.py
+++ b/trackers/api/views.py
@@ -59,3 +59,33 @@ class TicketViewSet(viewsets.ModelViewSet):
     def get_queryset(self, *args, **kwargs):
         prefix = self.kwargs['product_prefix']
         return models.Ticket.objects.filter(product=prefix)
+
+
+class ComponentViewSet(viewsets.ModelViewSet):
+    queryset = models.Component.objects.all()
+    serializer_class = serializers.ComponentSerializer
+    lookup_field = 'name'
+
+    def get_queryset(self, *args, **kwargs):
+        prefix = self.kwargs['product_prefix']
+        return models.Component.objects.filter(product=prefix)
+
+
+class MilestoneViewSet(viewsets.ModelViewSet):
+    queryset = models.Milestone.objects.all()
+    serializer_class = serializers.MilestoneSerializer
+    lookup_field = 'name'
+
+    def get_queryset(self, *args, **kwargs):
+        prefix = self.kwargs['product_prefix']
+        return models.Milestone.objects.filter(product=prefix)
+
+
+class VersionViewSet(viewsets.ModelViewSet):
+    queryset = models.Version.objects.all()
+    serializer_class = serializers.VersionSerializer
+    lookup_field = 'name'
+
+    def get_queryset(self, *args, **kwargs):
+        prefix = self.kwargs['product_prefix']
+        return models.Version.objects.filter(product=prefix)
diff --git a/trackers/models.py b/trackers/models.py
index 6b48f0e..989e359 100644
--- a/trackers/models.py
+++ b/trackers/models.py
@@ -15,10 +15,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-import difflib
-import functools
 import logging
-import uuid
 
 from django.db import models
 from django.urls import reverse

Reply via email to