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 <[email protected]>
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