Skip to content

Commit

Permalink
Using UUIDs in URLs for users, publishers, importers, distros, and wo…
Browse files Browse the repository at this point in the history
  • Loading branch information
David Davis authored and daviddavis committed Dec 11, 2017
1 parent dcb4838 commit 9d9c6a6
Show file tree
Hide file tree
Showing 7 changed files with 20 additions and 67 deletions.
48 changes: 7 additions & 41 deletions docs/contributing/3.0-development/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ Serializer Notes
be explicit.

* Serialized objects will provide thier own URL as the value of the "_href" field on the serializer.
If your API endpoint does not use the primary key as its lookup field (such as repository being
looked up by name), you will need to use a ``rest_framework.serializers.HyperlinkedIdentifyField``
to generate the ``_href`` field value correctly, by specifying its ``lookup_field`` and
``view_name``.
You will need to use a ``rest_framework.serializers.HyperlinkedIdentifyField`` to generate the
``_href`` field value by specifying its ``view_name``. If this object is referenced in the url by
a field other than the pk, you will also need to specify a ``lookup_field``.

* When subclassing serializers, you should also explicitly inherit properties that would normally
be overridden in the parent Serializer's Meta class.
Expand Down Expand Up @@ -198,37 +197,6 @@ name of that model when using a related field.
This is enough of a tricky problem that it has its own section in the docs a little bit below,
called "Master/Detail Relationships Overview".

lookup_field annoyances
^^^^^^^^^^^^^^^^^^^^^^^

When defining a ViewSet, the "lookup_field" setting can be used to specify a field other than
the Primary Key of that objects to use. This can result in better usability for objects that
have a single field other than their primary key that uniquely identifies them. For example,
Repository objects have a "name" field that uniquely identify them, so we can use that field
as our lookup_field.

This can get a little annoying when developing new API endpoints that relate to Repository,
because serializers relating to the object using "lookup_field" must also pass that keyword
argument so the DRF knows how to build the correct URL for Repository's ViewSet.

So, given this ViewSet::

class ExampleViewSet(viewsets.ViewSet):
lookup_field = 'name'
queryset = ExampleModel.objects.all()

Any serializer declaring a relationship to the model represented at that ViewSet must
also include lookup_field in the arguments to the related field in addition to the other
required fields::

class RelatedSerializer(serializers.HyperlinkedModelSerializer):
examples = HyperlinkedRelatedField(
many=True,
read_only=True,
view_name='examplemodel-detail',
lookup_field='name'
)

Building Explicit Serializers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -240,7 +208,7 @@ explicit Serializer class, DRF provides an excellent bit of functionality::
>>> from serializers import RepositorySerializer
>>> RepositorySerializer()
RepositorySerializer():
_href = HyperlinkedIdentityField(lookup_field='name', view_name='repository-detail')
_href = HyperlinkedIdentityField(view_name='repositories-detail')
name = CharField(style={'base_template': 'textarea.html'}, validators=[<UniqueValidator(queryset=Repository.objects.all())>])
description = CharField(allow_blank=True, required=False, style={'base_template': 'textarea.html'})
last_content_added = DateTimeField(allow_null=True, required=False)
Expand Down Expand Up @@ -389,7 +357,6 @@ This is what the ViewSet should look like:
.. code-block:: python
class RepositoryViewSet(viewsets.ModelViewSet):
lookup_field = 'name'
queryset = models.Repository.objects.all()
serializer_class = serializers.RepositorySerializer
filter_fields = ('name',)
Expand All @@ -416,10 +383,9 @@ that allows the same request:
fields = ['name']
class RepositoryViewSet(viewsets.ModelViewSet):
lookup_field = 'name'
queryset = models.Repository.objects.all()
serializer_class = serializers.RepositorySerializer
filter_class = RepositoryFilter
queryset = models.Repository.objects.all()
serializer_class = serializers.RepositorySerializer
filter_class = RepositoryFilter
Beyond Equality
Expand Down
11 changes: 4 additions & 7 deletions pulpcore/pulpcore/app/serializers/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class RepositorySerializer(ModelSerializer):
help_text=_('A mapping of string keys to string values, for storing notes on this object.'),
required=False
)
importers = DetailRelatedField(many=True, read_only=True, lookup_field='name')
publishers = DetailRelatedField(many=True, read_only=True, lookup_field='name')
importers = DetailRelatedField(many=True, read_only=True)
publishers = DetailRelatedField(many=True, read_only=True)
content = serializers.HyperlinkedIdentityField(
view_name='repositories-content',
)
Expand All @@ -68,7 +68,7 @@ class ImporterSerializer(MasterModelSerializer):
Every importer defined by a plugin should have an Importer serializer that inherits from this
class. Please import from `pulpcore.plugin.serializers` rather than from this module directly.
"""
_href = DetailIdentityField(lookup_field='name')
_href = DetailIdentityField()
name = serializers.CharField(
help_text=_('A name for this importer, unique within the associated repository.')
)
Expand Down Expand Up @@ -159,7 +159,7 @@ class PublisherSerializer(MasterModelSerializer):
Every publisher defined by a plugin should have an Publisher serializer that inherits from this
class. Please import from `pulpcore.plugin.serializers` rather than from this module directly.
"""
_href = DetailIdentityField(lookup_field='name')
_href = DetailIdentityField()
name = serializers.CharField(
help_text=_('A name for this publisher, unique within the associated repository.')
)
Expand All @@ -183,7 +183,6 @@ class PublisherSerializer(MasterModelSerializer):
distributions = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
lookup_field='name',
view_name='distributions-detail',
)

Expand All @@ -203,7 +202,6 @@ class Meta:

class DistributionSerializer(ModelSerializer):
_href = serializers.HyperlinkedIdentityField(
lookup_field='name',
view_name='distributions-detail'
)
name = serializers.CharField(
Expand Down Expand Up @@ -236,7 +234,6 @@ class DistributionSerializer(ModelSerializer):
)
publisher = DetailRelatedField(
queryset=models.Publisher.objects.all(),
lookup_field='name',
)

class Meta:
Expand Down
8 changes: 2 additions & 6 deletions pulpcore/pulpcore/app/serializers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ class TaskSerializer(ModelSerializer):
help_text=_("The worker associated with this task."
" This field is empty if a worker is not yet assigned."),
read_only=True,
view_name='workers-detail',
lookup_field='name'
view_name='workers-detail'
)

parent = serializers.HyperlinkedRelatedField(
Expand Down Expand Up @@ -94,10 +93,7 @@ class Meta:


class WorkerSerializer(ModelSerializer):
_href = serializers.HyperlinkedIdentityField(
view_name='workers-detail',
lookup_field='name'
)
_href = serializers.HyperlinkedIdentityField(view_name='workers-detail')

name = serializers.CharField(
help_text=_('The name of the worker.'),
Expand Down
3 changes: 1 addition & 2 deletions pulpcore/pulpcore/app/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ def to_internal_value(self, data):


class UserSerializer(ModelSerializer):
_href = serializers.HyperlinkedIdentityField(view_name='users-detail',
lookup_field='username')
_href = serializers.HyperlinkedIdentityField(view_name='users-detail')

username = serializers.CharField(
help_text=_("Required. {} characters or fewer. Letters, digits and @/./+/-/_ only.").format(
Expand Down
15 changes: 6 additions & 9 deletions pulpcore/pulpcore/app/viewsets/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,11 @@ class Meta:

class ImporterViewSet(NamedModelViewSet):
endpoint_name = 'importers'
lookup_field = 'name'
serializer_class = ImporterSerializer
queryset = Importer.objects.all()
filter_class = ImporterFilter

def update(self, request, name, partial=False):
def update(self, request, pk, partial=False):
importer = self.get_object()
serializer = self.get_serializer(importer, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
Expand All @@ -135,7 +134,7 @@ def update(self, request, name, partial=False):
)
return OperationPostponedResponse([async_result], request)

def destroy(self, request, name):
def destroy(self, request, pk):
importer = self.get_object()
async_result = tasks.importer.delete.apply_async_with_reservation(
tags.RESOURCE_REPOSITORY_TYPE, str(importer.repository.pk),
Expand All @@ -144,7 +143,7 @@ def destroy(self, request, name):
return OperationPostponedResponse([async_result], request)

@decorators.detail_route(methods=('post',))
def sync(self, request, name):
def sync(self, request, pk):
importer = self.get_object()
async_result = tasks.importer.sync.apply_async_with_reservation(
tags.RESOURCE_REPOSITORY_TYPE, str(importer.repository.pk),
Expand All @@ -155,12 +154,11 @@ def sync(self, request, name):

class PublisherViewSet(NamedModelViewSet):
endpoint_name = 'publishers'
lookup_field = 'name'
serializer_class = PublisherSerializer
queryset = Publisher.objects.all()
filter_class = PublisherFilter

def update(self, request, name, partial=False):
def update(self, request, pk, partial=False):
publisher = self.get_object()
serializer = self.get_serializer(publisher, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
Expand All @@ -175,7 +173,7 @@ def update(self, request, name, partial=False):
)
return OperationPostponedResponse([async_result], request)

def destroy(self, request, name):
def destroy(self, request, pk):
publisher = self.get_object()
async_result = tasks.publisher.delete.apply_async_with_reservation(
tags.RESOURCE_REPOSITORY_TYPE, str(publisher.repository.pk),
Expand All @@ -185,7 +183,7 @@ def destroy(self, request, name):
return OperationPostponedResponse([async_result], request)

@decorators.detail_route(methods=('post',))
def publish(self, request, name):
def publish(self, request, pk):
publisher = self.get_object()
async_result = tasks.publisher.publish.apply_async_with_reservation(
tags.RESOURCE_REPOSITORY_TYPE, str(publisher.repository.pk),
Expand All @@ -198,7 +196,6 @@ class DistributionViewSet(NamedModelViewSet):
endpoint_name = 'distributions'
queryset = Distribution.objects.all()
serializer_class = DistributionSerializer
lookup_field = 'name'


class RepositoryContentViewSet(NamedModelViewSet):
Expand Down
1 change: 0 additions & 1 deletion pulpcore/pulpcore/app/viewsets/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ class WorkerViewSet(NamedModelViewSet):
queryset = Worker.objects.all()
serializer_class = WorkerSerializer
endpoint_name = 'workers'
lookup_field = 'name'
http_method_names = ['get', 'options']
lookup_value_regex = '[^/]+'
1 change: 0 additions & 1 deletion pulpcore/pulpcore/app/viewsets/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class UserViewSet(NamedModelViewSet):
endpoint_name = 'users'
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_field = 'username'

@decorators.detail_route(methods=('post',))
def jwt_reset(self, request, username):
Expand Down

0 comments on commit 9d9c6a6

Please sign in to comment.