[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#824912: tracker.d.o: add an API for action items



Hi!

sorry for delayed answer. my stomach made strange things these
days. long bed days basically. :)

so, thx for the extensive review, let see what changed:

Raphael Hertzog <hertzog@debian.org> writes:

> stretch is coming in a few months and we are not using Django from stable
> either, so I would suggest to use directly DRF 3.4.

Done.

>> > Reported-by: Paul Wise <pabs@debian.org>
>> 
>> Done.
>
> You are still missing: s/_/-/

Done :)

> What about using the API self-documentation feature?
> http://www.django-rest-framework.org/topics/documenting-your-api/#self-describing-apis

I agree.

> That will also force you to choose good names to your views.

I'm not really good choosing names, i went for minimalist approach this
time :)

> Copyright 2016 only for new code. :) Applies on all files.

I didn't get this bit, so i just dropped it. It can be an additional
commit if necessary.


> Please don't use the same "name" for both URL. Use something like
> "...-action-item-list" and "...-action-item-detail".
>
> Also, after having looked at the DRF doc, we should not hardcode the
> versioning at this level, we should rather use NamespaceVersioning:
> http://www.django-rest-framework.org/api-guide/versioning/#namespaceversioning

Implemented it.

>> +class ActionItemListAPIView(generics.ListAPIView):
>
> Shouldn't we merge both objects with a single ReadOnlyModelViewSet?
> http://www.django-rest-framework.org/api-guide/viewsets/#readonlymodelviewset

I'm not a really great fan of viewsets. i think we cannot split hairs
using viewsets. But i took the idea from them to use the mixins + a
generic view.

>> +    """
>> +    List all ActionItem instances.
>> +    """
>
> Here you must explain the GET query parameter that we support.

Now there's a hopefully more verbose explanation.

>> +    def get_queryset(self):
>> +        queryset = ActionItem.objects.all()
>> +        package_name = self.request.GET.get('package_name', None)
>
> package_name is a required abstraction at the database level but it makes
> no sense for end users. I would just use "package" as the query parameter.

done.

>
> Also, one of the early design decision seems to be whether we use an
> hyperlinked serializer or not. I don't have a strong opinion on that but it
> makes it easier to navigate interactively in the data through the browser. So
> unless we have a reason not to, I believe it would be nice to use it:
> http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/#hyperlinking-our-api

Well imho, it makes sense if we are serializing also other models with a
correspondant URL. So i'm happy to implement this behaviour as the API
grows. But i guess for this specific issue it is not really a must.

> But even if we use the hyperlinked version of the serializer, I would still
> include the "id" attribute everywhere.
>
> Cheers,


> -- 
> Raphaël Hertzog ◈ Debian Developer
>
> Support Debian LTS: http://www.freexian.com/services/debian-lts.html
> Learn to master Debian: http://debian-handbook.info/get/

Cheers,

-- 
efkin.

>From 55c077611db6a91b1cfbae45e2f809c12b1b6eb4 Mon Sep 17 00:00:00 2001
From: efkin <efkin@riseup.net>
Date: Wed, 14 Dec 2016 13:04:52 +0100
Subject: [PATCH 1/3] Include rest_framework dependency

---
 distro_tracker/project/settings/defaults.py | 1 +
 docs/setup/setup.rst                        | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/distro_tracker/project/settings/defaults.py b/distro_tracker/project/settings/defaults.py
index ad7defd..0f27309 100644
--- a/distro_tracker/project/settings/defaults.py
+++ b/distro_tracker/project/settings/defaults.py
@@ -249,6 +249,7 @@ INSTALLED_APPS = (
     'django.contrib.staticfiles',
     'django.contrib.admin',
     'django_email_accounts',
+    'rest_framework',
     'distro_tracker.html',
     'distro_tracker.core',
     'distro_tracker.accounts',
diff --git a/docs/setup/setup.rst b/docs/setup/setup.rst
index dad9ac0..1898528 100644
--- a/docs/setup/setup.rst
+++ b/docs/setup/setup.rst
@@ -15,6 +15,7 @@ Distro Tracker currently depends on the following Debian packages:
 - python-django-jsonfield (>= 1.0.0)
 - python-django-debug-toolbar (in development mode only)
 - python-django-captcha (optional)
+- python-djangorestframework (= 3.4.X from Stretch)
 - python-debian
 - python-apt
 - python-gpgme
@@ -37,6 +38,7 @@ Here is the list of required packages for development on Debian Jessie::
 
  $ sudo apt install python-django python-requests python-django-jsonfield python-django-debug-toolbar python-debian python-apt python-gpgme python-yaml python-bs4 python-soappy python-ldap python-pyinotify python-tox python-mock python-lzma python-selenium python3-django python3-requests python3-django-jsonfield python3-django-debug-toolbar python3-debian python3-apt python3-gpgme python3-yaml python3-bs4 python3-pyinotify python3-selenium chromium chromedriver
 
+
 .. _database_setup:
 
 Database
-- 
2.1.4

>From 64e781ade6ef6226f313c03944c91936c8de79f6 Mon Sep 17 00:00:00 2001
From: efkin <efkin@riseup.net>
Date: Wed, 14 Dec 2016 13:14:35 +0100
Subject: [PATCH 2/3] Create distro_tracker submodule for API development

---
 distro_tracker/api/__init__.py              | 0
 distro_tracker/api/tests.py                 | 3 +++
 distro_tracker/api/views.py                 | 3 +++
 distro_tracker/project/settings/defaults.py | 1 +
 4 files changed, 7 insertions(+)
 create mode 100644 distro_tracker/api/__init__.py
 create mode 100644 distro_tracker/api/tests.py
 create mode 100644 distro_tracker/api/views.py

diff --git a/distro_tracker/api/__init__.py b/distro_tracker/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/distro_tracker/api/tests.py b/distro_tracker/api/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/distro_tracker/api/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/distro_tracker/api/views.py b/distro_tracker/api/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/distro_tracker/api/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/distro_tracker/project/settings/defaults.py b/distro_tracker/project/settings/defaults.py
index 0f27309..0d42a37 100644
--- a/distro_tracker/project/settings/defaults.py
+++ b/distro_tracker/project/settings/defaults.py
@@ -250,6 +250,7 @@ INSTALLED_APPS = (
     'django.contrib.admin',
     'django_email_accounts',
     'rest_framework',
+    'distro_tracker.api',
     'distro_tracker.html',
     'distro_tracker.core',
     'distro_tracker.accounts',
-- 
2.1.4

>From 532cacd4cb085b39544fccdc43fca764d804763b Mon Sep 17 00:00:00 2001
From: efkin <efkin@riseup.net>
Date: Wed, 14 Dec 2016 18:45:09 +0100
Subject: [PATCH] Create basic API list/detail endpoint for ActionItem model
 instances

Reported-by: Paul Wise <pabs@debian.org>
---
 distro_tracker/api/paginators.py   |  7 +++
 distro_tracker/api/serializers.py  | 27 +++++++++++
 distro_tracker/api/tests.py        | 95 +++++++++++++++++++++++++++++++++++++-
 distro_tracker/api/tracker_urls.py | 18 ++++++++
 distro_tracker/api/views.py        | 47 ++++++++++++++++++-
 5 files changed, 190 insertions(+), 4 deletions(-)
 create mode 100644 distro_tracker/api/paginators.py
 create mode 100644 distro_tracker/api/serializers.py
 create mode 100644 distro_tracker/api/tracker_urls.py

diff --git a/distro_tracker/api/paginators.py b/distro_tracker/api/paginators.py
new file mode 100644
index 0000000..7b4f52f
--- /dev/null
+++ b/distro_tracker/api/paginators.py
@@ -0,0 +1,7 @@
+from rest_framework.pagination import LimitOffsetPagination
+
+
+class StandardResultsSetPaignation(LimitOffsetPagination):
+    default_limit = 100
+    max_limit = 500
+    
diff --git a/distro_tracker/api/serializers.py b/distro_tracker/api/serializers.py
new file mode 100644
index 0000000..1f1d098
--- /dev/null
+++ b/distro_tracker/api/serializers.py
@@ -0,0 +1,27 @@
+from rest_framework import serializers
+
+from distro_tracker.core.models import ActionItem
+from distro_tracker.core.models import PackageName
+
+
+class ActionItemSerializer(serializers.ModelSerializer):
+
+    package_name = serializers.ReadOnlyField(source="package.name")
+    item_type_name = serializers.ReadOnlyField(source="item_type.type_name")
+    
+    class Meta:
+        model = ActionItem
+        fields = (
+            'id',
+            'package_name',
+            'item_type_name',
+            'short_description',
+            'severity',
+            'created_timestamp',
+            'last_updated_timestamp',
+            'extra_data',
+        )
+
+
+
+
diff --git a/distro_tracker/api/tests.py b/distro_tracker/api/tests.py
index 7ce503c..7c53110 100644
--- a/distro_tracker/api/tests.py
+++ b/distro_tracker/api/tests.py
@@ -1,3 +1,94 @@
-from django.test import TestCase
+from django.core.urlresolvers import reverse
 
-# Create your tests here.
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from distro_tracker.core.models import ActionItem
+from distro_tracker.core.models import ActionItemType
+from distro_tracker.core.models import SourcePackageName
+
+
+class ActionItemListAPIViewTest(APITestCase):
+    """
+    Test for the :class:`distro_tracker.api.views.ActionItemListAPIView`.
+    """
+
+    def setUp(self):
+        self.url = reverse('dtracker-api-v1-action-items')
+        
+    def test_empty_list(self):
+        """
+        Test when the queryset is empty.
+        """
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            0,
+            response.data['count'],
+        )
+
+    def test_account_item(self):
+        """
+        Test with actual content.
+        """
+        package = SourcePackageName.objects.create(name='dummy-package')
+        action_type = ActionItemType.objects.create(
+            type_name='test',
+            full_description_template='action-item-test.html',
+        )
+        action_item = ActionItem.objects.create(
+            package=package,
+            item_type=action_type,
+            short_description="Short description of item",
+        )
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            1,
+            response.data['count'],
+        )
+        result = response.data['results'][0]
+        self.assertEqual(
+            'dummy-package',
+            result['package_name'],
+        )
+
+        
+class ActionItemDetailAPIViewTest(APITestCase):
+    """
+    Test for the :class:`distro_tracker.api.views.ActionItemDetailAPIView`.
+    """
+
+    def setUp(self):
+        self.url = reverse('dtracker-api-v1-action-items', kwargs={'pk':1})
+
+    def test_404_on_non_existing_pk(self):
+        """
+        Test when the pk does not return any instance.
+        """
+        response = self.client.get(self.url)
+
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        
+    def test_existing_instance(self):
+        """
+        Test when the pk does return an actual instance.
+        """
+        package = SourcePackageName.objects.create(name='dummy-package')
+        action_type = ActionItemType.objects.create(
+            type_name='test',
+            full_description_template='action-item-test.html',
+        )
+        action_item = ActionItem.objects.create(
+            package=package,
+            item_type=action_type,
+            short_description="Short description of item",
+        )
+        response = self.client.get(self.url)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(
+            'dummy-package',
+            response.data['package_name'],
+        )
+
+        
diff --git a/distro_tracker/api/tracker_urls.py b/distro_tracker/api/tracker_urls.py
new file mode 100644
index 0000000..3febcac
--- /dev/null
+++ b/distro_tracker/api/tracker_urls.py
@@ -0,0 +1,18 @@
+from django.conf.urls import url, include
+
+from .views import ActionItemView
+
+
+v1_patterns = [
+    url(r'^action-items/?$',
+        ActionItemView.as_view(),
+        name='dtracker-api-v1-action-item-list'),
+    url(r'^action-items/(?P<pk>[0-9]+)/?$',
+        ActionItemView.as_view(),
+        name='dtracker-api-v1-action-item-detail'),
+]
+
+
+urlpatterns = [
+    url(r'^api/v1/', include(v1_patterns, namespace='v1')),
+]
diff --git a/distro_tracker/api/views.py b/distro_tracker/api/views.py
index 91ea44a..7c791b6 100644
--- a/distro_tracker/api/views.py
+++ b/distro_tracker/api/views.py
@@ -1,3 +1,46 @@
-from django.shortcuts import render
+from django.http import Http404
 
-# Create your views here.
+from rest_framework import status
+from rest_framework import generics
+from rest_framework.mixins import ListModelMixin
+from rest_framework.mixins import RetrieveModelMixin
+from rest_framework.response import Response
+
+from distro_tracker.api.serializers import ActionItemSerializer
+from distro_tracker.api.paginators import StandardResultsSetPaignation
+from distro_tracker.core.models import ActionItem
+
+
+class ActionItemView(RetrieveModelMixin,
+                     ListModelMixin,
+                     generics.GenericAPIView):
+    """
+    This is a readonly endpoint. It retrieves the list of the
+    `ActionItem` instances or a single instance if the `pk` is
+    provided in the URL in the form `/api/v1/action-items/<pk>/`.
+
+    Eventually it accepts also the following query parameters:
+
+    * `package`: a string containing the name of the package
+    * `limit`: an integer indicating the max number of items per query
+    * `offset`: an integer indicating the starting position of the query 
+    in relation to the complete set of unpaginated items.
+    
+    """
+
+    serializer_class = ActionItemSerializer
+    pagination_class = StandardResultsSetPaignation
+    
+    def get_queryset(self):
+        queryset = ActionItem.objects.all()
+        package_name = self.request.query_params.get('package', None)
+        if package_name is not None:
+            queryset = queryset.filter(package__name=package_name)
+        return queryset
+
+    def get(self, request, *args, **kwargs):
+        if 'pk' in kwargs:
+            return self.retrieve(request, *args, **kwargs)
+        else:
+            return self.list(request, *args, **kwargs)
+        
-- 
2.1.4


Reply to: