[PATCH 4/7] api: Add a basic REST API to access Series/Revisions and Patches
Damien Lespiau
damien.lespiau at intel.com
Wed Oct 21 09:40:44 AEDT 2015
v2: Merge commits introducing basic objects
v3: Introduce the SeriesListMixin class
v4: Add the expand get parameter
v5: Introduce /api/1.0/ metadata entry point
Signed-off-by: Damien Lespiau <damien.lespiau at intel.com>
---
docs/api.rst | 363 +++++++++++++++++++++++++++++++++++++++++++++
docs/conf.py | 2 +-
docs/index.rst | 1 +
docs/requirements-base.txt | 3 +
docs/requirements-dev.txt | 1 +
patchwork/models.py | 5 +-
patchwork/serializers.py | 133 +++++++++++++++++
patchwork/settings/base.py | 7 +
patchwork/urls.py | 31 ++++
patchwork/views/api.py | 127 ++++++++++++++++
10 files changed, 671 insertions(+), 2 deletions(-)
create mode 100644 docs/api.rst
create mode 100644 patchwork/serializers.py
create mode 100644 patchwork/views/api.py
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..7979ac4
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,363 @@
+APIs
+===========
+
+REST API
+--------
+
+API metadata
+~~~~~~~~~~~~
+
+.. http:get:: /api/1.0/
+
+ Metadata about the API itself.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/ HTTP/1.1
+ Accept: application/json
+
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: application/json
+
+ {
+ "revision": 0
+ }
+
+ :>json int revision: API revision. This can be used to ensure the server
+ supports a feature introduced from a specific revision.
+
+
+Projects
+~~~~~~~~
+
+A project is merely one of the projects defined for this patchwork instance.
+
+.. http:get:: /api/1.0/projects/
+
+ List of all projects.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/projects/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2,
+ "name": "beignet",
+ "linkname": "beignet",
+ "listemail": "beignet at lists.freedesktop.org",
+ "web_url": "http://www.freedesktop.org/wiki/Software/Beignet/",
+ "scm_url": "git://anongit.freedesktop.org/git/beignet",
+ "webscm_url": "http://cgit.freedesktop.org/beignet/"
+ },
+ {
+ "id": 1,
+ "name": "Cairo",
+ "linkname": "cairo",
+ "listemail": "cairo at cairographics.org",
+ "web_url": "http://www.cairographics.org/",
+ "scm_url": "git://anongit.freedesktop.org/git/cairo",
+ "webscm_url": "http://cgit.freedesktop.org/cairo/"
+ }
+ ]
+ }
+
+.. http:get:: /api/1.0/projects/(string: linkname)/
+.. http:get:: /api/1.0/projects/(int: project_id)/
+
+ .. sourcecode:: http
+
+ GET /api/1.0/projects/intel-gfx/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "id": 1,
+ "name": "intel-gfx",
+ "linkname": "intel-gfx",
+ "listemail": "intel-gfx at lists.freedesktop.org",
+ "web_url": "",
+ "scm_url": "",
+ "webscm_url": ""
+ }
+
+Series
+~~~~~~
+
+A series object represents a lists of patches sent to the mailing-list through
+``git-send-email``. It also includes all subsequent patches that are sent to
+address review comments, both single patch and full new series.
+
+A series has then ``n`` revisions, ``n`` going from ``1`` to ``version``.
+
+.. http:get:: /api/1.0/projects/(string: linkname)/series/
+.. http:get:: /api/1.0/projects/(int: project_id)/series/
+
+ List of all Series belonging to a specific project. The project can be
+ specified using either its ``linkname`` or ``id``.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/projects/intel-gfx/series/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "count": 59,
+ "next": "http://patchwork.freedesktop.org/api/1.0/projects/intel-gfx/series/?page=2",
+ "previous": null,
+ "results": [
+ {
+ "id": 3,
+ "project": 1,
+ "name": "drm/i915: Unwind partial VMA rebinding after failure in set-cache-level",
+ "n_patches": 1,
+ "submitter": 77,
+ "submitted": "2015-10-09T11:51:38",
+ "last_updated": "2015-10-09T11:51:59.013",
+ "version": 1,
+ "reviewer": null
+ },
+ {
+ "id": 5,
+ "project": 1,
+ "name": "RFC drm/i915: Stop the machine whilst capturing the GPU crash dump",
+ "n_patches": 1,
+ "submitter": 77,
+ "submitted": "2015-10-09T12:21:45",
+ "last_updated": "2015-10-09T12:21:58.657",
+ "version": 1,
+ "reviewer": null,
+ }
+ ]
+ }
+
+.. http:get:: /api/1.0/series/
+
+ List of all Series known to patchwork.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/series/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: application/json
+
+ {
+ "count": 344,
+ "next": "http://127.0.0.1:8000/api/1.0/series/?page=2",
+ "previous": null,
+ "results": [
+ {
+ "id": 10,
+ "project": 1,
+ "name": "intel: New libdrm interface to create unbound wc user mappings for objects",
+ "n_patches": 1,
+ "submitter": 10,
+ "submitted": "2015-01-02T11:06:40",
+ "last_updated": "2015-10-09T07:55:18.608",
+ "version": 1,
+ "reviewer": null
+ },
+ {
+ "id": 1,
+ "project": 1,
+ "name": "PMIC based Panel and Backlight Control",
+ "n_patches": 4,
+ "submitter": 1,
+ "submitted": "2014-12-26T10:23:26",
+ "last_updated": "2015-10-09T07:55:01.558",
+ "version": 1,
+ "reviewer": null,
+ },
+ ]
+ }
+
+.. http:get:: /api/1.0/series/(int: series_id)/
+
+ A series (`series_id`). A Series object contains metadata about the series.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/series/47/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, PUT, PATCH, HEAD, OPTIONS
+
+ {
+ "id": 47,
+ "name": "Series without cover letter",
+ "n_patches": 2,
+ "submitter": 21,
+ "submitted": "2015-01-13T09:32:24",
+ "last_updated": "2015-10-09T07:57:23.541",
+ "version": 1,
+ "reviewer": null
+ }
+
+.. http:get:: /api/1.0/series/(int: series_id)/revisions/
+
+ The list of revisions of the series `series_id`.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/series/47/revisions/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ [
+ {
+ "version": 1,
+ "cover_letter": null,
+ "patches": [
+ 120,
+ 121
+ ]
+ }
+ ]
+
+.. http:get:: /api/1.0/series/(int: series_id)/revisions/(int: version)/
+
+ The specific ``version`` of the series `series_id`.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/series/47/revisions/1/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "version": 1,
+ "cover_letter": null,
+ "patches": [
+ 120,
+ 121
+ ]
+ }
+
+Patches
+~~~~~~~
+
+.. http:get:: /api/1.0/patches/
+
+ List of all patches.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/patches/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "count": 1392,
+ "next": "http://127.0.0.1:8000/api/1.0/patches/?page=2",
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "project": 1,
+ "name": "[RFC,1/4] drm/i915: Define a common data structure for Panel Info",
+ "date": "2014-12-26T10:23:27",
+ "submitter": 1,
+ "state": 1,
+ "content": "<diff content>"
+ },
+ {
+ "id": 4,
+ "project": 1,
+ "name": "[RFC,2/4] drm/i915: Add a drm_panel over INTEL_SOC_PMIC",
+ "date": "2014-12-26T10:23:28",
+ "submitter": 1,
+ "state": 1,
+ "content": "<diff content>"
+ }
+ ]
+ }
+
+.. http:get:: /api/1.0/patches/(int: patch_id)/
+
+ A specific patch.
+
+ .. sourcecode:: http
+
+ GET /api/1.0/patches/120/ HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Vary: Accept
+ Allow: GET, HEAD, OPTIONS
+
+ {
+ "id": 120,
+ "name": "[1/2] drm/i915: Balance context pinning on reset cleanup",
+ "date": "2015-01-13T09:32:24",
+ "submitter": 21,
+ "state": 1,
+ "content": "<diff content>"
+ }
+
+API Revisions
+~~~~~~~~~~~~~
+
+**Revision 0**
+
+- Initial revision. Basic objects exposed: api root, projects, series,
+ revisions and patches.
diff --git a/docs/conf.py b/docs/conf.py
index d45985d..0bdc031 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,7 +29,7 @@ import shlex
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = []
+extensions = ['sphinxcontrib.httpdomain']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
diff --git a/docs/index.rst b/docs/index.rst
index 7322a9a..5351e30 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,5 +14,6 @@ Contents:
intro
installation
manual
+ api
development
diff --git a/docs/requirements-base.txt b/docs/requirements-base.txt
index 58b34ec..7314ab3 100644
--- a/docs/requirements-base.txt
+++ b/docs/requirements-base.txt
@@ -1,2 +1,5 @@
MySQL-python==1.2.5
python-dateutil==1.5
+djangorestframework>=2.4.8,<3.0.0
+drf-nested-routers
+enum34
diff --git a/docs/requirements-dev.txt b/docs/requirements-dev.txt
index 19a9b91..6021175 100644
--- a/docs/requirements-dev.txt
+++ b/docs/requirements-dev.txt
@@ -1,3 +1,4 @@
-r requirements-base.txt
selenium
sphinx
+sphinxcontrib-httpdomain
diff --git a/patchwork/models.py b/patchwork/models.py
index 3ede65c..24fac6c 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -39,12 +39,15 @@ class Person(models.Model):
user = models.ForeignKey(User, null = True, blank = True,
on_delete = models.SET_NULL)
- def __unicode__(self):
+ def display_name(self):
if self.name:
return self.name
else:
return self.email
+ def __unicode__(self):
+ return self.display_name()
+
def link_to_user(self, user):
self.name = user.profile.name()
self.user = user
diff --git a/patchwork/serializers.py b/patchwork/serializers.py
new file mode 100644
index 0000000..418a140
--- /dev/null
+++ b/patchwork/serializers.py
@@ -0,0 +1,133 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2014 Intel Corporation
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from django.contrib.auth.models import User
+from patchwork.models import Project, Series, SeriesRevision, Patch, Person, \
+ State
+from rest_framework import serializers
+from enum import Enum
+
+class RelatedMode(Enum):
+ """Select how to show related fields in the JSON responses."""
+ primary_key = 1
+ expand = 2
+
+class PatchworkModelSerializerOptions(serializers.ModelSerializerOptions):
+ """Meta class options for PatchworkModelSerializer"""
+ def __init__(self, meta):
+ super(PatchworkModelSerializerOptions, self).__init__(meta)
+ self.expand_serializers = getattr(meta, 'expand_serializers', {})
+
+class PatchworkModelSerializer(serializers.ModelSerializer):
+ """A model serializer with configurable related fields.
+
+ PatchworkModelSerializer can either show related fields as a integer
+ or expand them to include the related full JSON object.
+ This behaviour is selectable through the 'related' GET parameter. Adding
+ 'related=expand' to the GET request will expand related fields.
+ """
+
+ _options_class = PatchworkModelSerializerOptions
+
+ def __init__(self, *args, **kwargs):
+ super(PatchworkModelSerializer, self).__init__(*args, **kwargs)
+
+ self._pw_related = RelatedMode.primary_key
+ related = self.context['request'].QUERY_PARAMS.get('related')
+ if not related:
+ return
+
+ try:
+ self._pw_related = RelatedMode[related]
+ except KeyError:
+ pass
+
+ def _pw_get_nested_field(self, model_field, related_model, to_many):
+ class NestedModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = related_model
+
+ if model_field.name in self.opts.expand_serializers:
+ serializer_class = self.opts.expand_serializers[model_field.name]
+ return serializer_class(context=self.context, many=to_many)
+ return NestedModelSerializer(many=to_many)
+
+ def get_related_field(self, model_field, related_model, to_many):
+ if self._pw_related == RelatedMode.expand:
+ return self._pw_get_nested_field(model_field, related_model, to_many)
+ else:
+ return super(PatchworkModelSerializer, self). \
+ get_related_field(model_field, related_model, to_many)
+
+class UserSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = User
+ fields = ('id', 'username', 'first_name', 'last_name', )
+
+class PersonSerializer(serializers.ModelSerializer):
+ name = serializers.CharField(source='display_name', read_only=True)
+ class Meta:
+ model = Person
+ fields = ('id', 'name', )
+
+class ProjectSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Project
+ fields = ('id', 'name', 'linkname', 'listemail', 'web_url', 'scm_url',
+ 'webscm_url')
+
+class StateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = State
+ fields = ('id', 'name')
+
+class SeriesSerializer(PatchworkModelSerializer):
+ class Meta:
+ model = Series
+ fields = ('id', 'project', 'name', 'n_patches', 'submitter',
+ 'submitted', 'last_updated', 'version', 'reviewer')
+ read_only_fields = ('project', 'n_patches', 'submitter', 'submitted',
+ 'last_updated', 'version')
+ expand_serializers = {
+ 'project': ProjectSerializer,
+ 'submitter': PersonSerializer,
+ 'reviewer': UserSerializer,
+ }
+
+class PatchSerializer(PatchworkModelSerializer):
+ class Meta:
+ model = Patch
+ fields = ('id', 'project', 'name', 'date', 'submitter', 'state',
+ 'content')
+ read_only_fields = ('id', 'project', 'name', 'date', 'submitter',
+ 'content')
+ expand_serializers = {
+ 'project': ProjectSerializer,
+ 'submitter': PersonSerializer,
+ 'state': StateSerializer,
+ }
+
+class RevisionSerializer(PatchworkModelSerializer):
+ class Meta:
+ model = SeriesRevision
+ fields = ('version', 'cover_letter', 'patches')
+ read_only_fields = ('version', 'cover_letter')
+ expand_serializers = {
+ 'patches': PatchSerializer,
+ }
diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py
index ab03814..6edf7c6 100644
--- a/patchwork/settings/base.py
+++ b/patchwork/settings/base.py
@@ -24,6 +24,7 @@ INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.staticfiles',
'patchwork',
+ 'rest_framework',
]
# HTTP
@@ -97,6 +98,12 @@ STATICFILES_DIRS = [
os.path.join(ROOT_DIR, 'htdocs'),
]
+#
+# REST framework
+
+REST_FRAMEWORK = {
+}
+
#
# Patchwork settings
diff --git a/patchwork/urls.py b/patchwork/urls.py
index bc792d0..3ba734e 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -21,12 +21,43 @@ from django.conf.urls import patterns, url, include
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
+from rest_framework_nested import routers
+import patchwork.views.api as api
+
+# API
+
+# /projects/$project/
+project_router = routers.SimpleRouter()
+project_router.register('projects', api.ProjectViewSet)
+# /projects/$project/series/
+series_list_router = routers.NestedSimpleRouter(project_router, 'projects',
+ lookup='project')
+series_list_router.register(r'series', api.SeriesListViewSet)
+# /series/$id/
+series_router = routers.SimpleRouter()
+series_router.register(r'series', api.SeriesViewSet)
+# /series/$id/revisions/$rev
+revisions_router = routers.NestedSimpleRouter(series_router, 'series',
+ lookup='series')
+revisions_router.register(r'revisions', api.RevisionViewSet)
+# /patches/$id/
+patches_router = routers.SimpleRouter()
+patches_router.register(r'patches', api.PatchViewSet)
admin.autodiscover()
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
+ # API
+ (r'^api/1.0/$', api.API.as_view()),
+ (r'^api/1.0/', include(project_router.urls)),
+ (r'^api/1.0/', include(series_list_router.urls)),
+ (r'^api/1.0/', include(series_router.urls)),
+ (r'^api/1.0/', include(revisions_router.urls)),
+ (r'^api/1.0/', include(patches_router.urls)),
+
+ # project view:
(r'^$', 'patchwork.views.projects'),
(r'^project/(?P<project_id>[^/]+)/list/$', 'patchwork.views.patch.list'),
(r'^project/(?P<project_id>[^/]+)/$', 'patchwork.views.project.project'),
diff --git a/patchwork/views/api.py b/patchwork/views/api.py
new file mode 100644
index 0000000..01539b6
--- /dev/null
+++ b/patchwork/views/api.py
@@ -0,0 +1,127 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2014 Intel Corporation
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from patchwork.models import Project, Series, SeriesRevision, Patch
+from rest_framework import views, viewsets, mixins, generics, filters, permissions
+from rest_framework.decorators import api_view, renderer_classes, \
+ permission_classes
+from rest_framework.renderers import JSONRenderer
+from rest_framework.response import Response
+from rest_framework.generics import get_object_or_404
+from patchwork.serializers import ProjectSerializer, SeriesSerializer, \
+ RevisionSerializer, PatchSerializer
+
+
+API_REVISION = 0
+
+class MaintainerPermission(permissions.BasePermission):
+ def has_object_permission(self, request, view, obj):
+ # read only for everyone
+ if request.method in permissions.SAFE_METHODS:
+ return True
+
+ # editable for maintainers
+ user = request.user
+ if not user.is_authenticated():
+ return False
+ return obj.project.is_editable(user)
+
+class API(views.APIView):
+ permission_classes = (permissions.AllowAny,)
+
+ def get(self, request, format=None):
+ return Response({ 'revision': API_REVISION })
+
+class ListMixin(object):
+ paginate_by = 20
+ paginate_by_param = 'perpage'
+ max_paginate_by = 100
+
+class SeriesListMixin(ListMixin):
+ queryset = Series.objects.all()
+ serializer_class = SeriesSerializer
+ filter_backends = (filters.OrderingFilter, )
+ ordering_fields = ('name', 'n_patches', 'submitter__name', 'reviewer__name',
+ 'submitted', 'last_updated')
+
+def is_integer(s):
+ try:
+ int(s)
+ return True
+ except ValueError:
+ return False
+
+class ProjectViewSet(mixins.ListModelMixin, ListMixin, viewsets.GenericViewSet):
+ permission_classes = (MaintainerPermission, )
+ queryset = Project.objects.all()
+ serializer_class = ProjectSerializer
+
+ def retrieve(self, request, pk=None):
+ if is_integer(pk):
+ queryset = get_object_or_404(Project, pk=pk)
+ else:
+ queryset = get_object_or_404(Project, linkname=pk)
+ serializer = ProjectSerializer(queryset)
+ return Response(serializer.data)
+
+class SeriesListViewSet(mixins.ListModelMixin,
+ SeriesListMixin,
+ viewsets.GenericViewSet):
+ permission_classes = (MaintainerPermission, )
+
+ def get_queryset(self):
+
+ pk = self.kwargs['project_pk']
+ if is_integer(pk):
+ queryset = self.queryset.filter(project__pk=pk)
+ else:
+ queryset = self.queryset.filter(project__linkname=pk)
+ return queryset
+
+class SeriesViewSet(mixins.ListModelMixin,
+ mixins.RetrieveModelMixin,
+ SeriesListMixin,
+ viewsets.GenericViewSet):
+ permission_classes = (MaintainerPermission, )
+ queryset = Series.objects.all()
+
+class RevisionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
+ permission_classes = (MaintainerPermission, )
+ queryset = SeriesRevision.objects.all()
+ serializer_class = RevisionSerializer
+
+ def get_queryset(self):
+
+ series_pk = self.kwargs['series_pk']
+ return self.queryset.filter(series=series_pk)
+
+ def retrieve(self, request, series_pk=None, pk=None):
+ rev = get_object_or_404(SeriesRevision, series=series_pk, version=pk)
+ print(self.get_serializer_context())
+ serializer = RevisionSerializer(rev,
+ context=self.get_serializer_context())
+ return Response(serializer.data)
+
+class PatchViewSet(mixins.ListModelMixin,
+ mixins.RetrieveModelMixin,
+ ListMixin,
+ viewsets.GenericViewSet):
+ permission_classes = (MaintainerPermission, )
+ queryset = Patch.objects.all()
+ serializer_class = PatchSerializer
--
2.4.3
More information about the Patchwork
mailing list