Generate schema for Django rest framework viewset actions - django

As per the DRF documentation I started using ViewSet and have implemented list, retrieve, create, update and destroyactions. I have another APIView for which I was able to write schema (ManualSchema) and when I navigate to /docs/ I am able to the documentation as well as live endpoint for interaction.
I wish to create separate schema for each of the viewset action. I tried writing one but it doesn't show up so I think I am missing something.
Here is the code:
class Clients(viewsets.ViewSet):
'''
Clients is DRF viewset which implements `create`, `update`, `read` actions by implementing create, update, list and retrieve functions respectively.
'''
list_schema = schemas.ManualSchema(fields=[
coreapi.Field(
'status',
required=False,
location='query',
description='Accepted values are `active`, `inactive`'
),
],
description='Clients list',
encoding='application/x-www-form-urlencoded')
#action(detail=True, schema=list_schema)
def list(self, request):
'''Logic for listing'''
def retrieve(self, request, oid=None):
'''Logic for retrieval'''
create_schema = schemas.ManualSchema(fields=[
coreapi.Field(
'name',
required=False,
location='body',
),
coreapi.Field(
'location',
required=False,
location='body',
),
],
description='Clients list',
encoding='application/x-www-form-urlencoded')
#action(detail=True, schema=create_schema)
def create(self, request):
'''Logic for creation'''

So I will answer my own question. I took a look at DRF source code for schema generation. I came up with the plan and performed following steps.
I subclassed SchemaGenerator class defined in rest_framework.schemas module. Below is the code.
class CoreAPISchemaGenerator(SchemaGenerator):
def get_links(self, request=None, **kwargs):
links = LinkNode()
paths = list()
view_endpoints = list()
for path, method, callback in self.endpoints:
view = self.create_view(callback, method, request)
path = self.coerce_path(path, method, view)
paths.append(path)
view_endpoints.append((path, method, view))
if not paths:
return None
prefix = self.determine_path_prefix(paths)
for path, method, view in view_endpoints:
if not self.has_view_permissions(path, method, view):
continue
actions = getattr(view, 'actions', None)
schemas = getattr(view, 'schemas', None)
if not schemas:
link = view.schema.get_link(path, method, base_url=self.url)
subpath = path[len(prefix):]
keys = self.get_keys(subpath, method, view, view.schema)
insert_into(links, keys, link)
else:
action_map = getattr(view, 'action_map', None)
method_name = action_map.get(method.lower())
schema = schemas.get(method_name)
link = schema.get_link(path, method, base_url=self.url)
subpath = path[len(prefix):]
keys = self.get_keys(subpath, method, view, schema)
insert_into(links, keys, link)
return links
def get_keys(self, subpath, method, view, schema=None):
if schema and hasattr(schema, 'endpoint_name'):
return [schema.endpoint_name]
else:
if hasattr(view, 'action'):
action = view.action
else:
if is_list_view(subpath, method, view):
action = 'list'
else:
action = self.default_mapping[method.lower()]
named_path_components = [
component for component
in subpath.strip('/').split('/')
if '{' not in component
]
if is_custom_action(action):
if len(view.action_map) > 1:
action = self.default_mapping[method.lower()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
else:
return named_path_components[:-1] + [action]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
I specifically modified two functions get_links and get_keys as that allow me to achieve what I wanted.
Further, for all the functions in viewsets that I was writing I dedicated an individual schema for it. I simply created a dictionary to keep mappings of function name to schema instance. For better approach I created a separate file to store schemas. For eg. if I had a viewset Clients I created a corresponding ClientsSchema class and within in defined staticmethods which returned schema instances.
Example,
In file where I am defining my schemas,
class ClientsSchema():
#staticmethod
def list_schema():
schema = schemas.ManualSchema(
fields=[],
description=''
)
schema.endpoint_name = 'Clients Listing'
return schema
In my apis.py,
class Clients(viewsets.ViewSet):
schemas = {
'list': ClientsSchema.list_schema()
}
def list(self, request, **kwargs):
pass
This setup allows me to define schemas for any type of functions that I add to my viewsets. In addition to it, I also wanted that the endpoints have an identifiable name and not the one generated by DRF which is like a > b > update > update.
In order to achieve that, I added endpoint_name property to schema object that is returned. That part is handled in get_keys function that is overridden.
Finally in the urls.py where we include urls for documentation we need to use our custom schema generator. Something like this,
urlpatterns.append(url(r'^livedocs/', include_docs_urls(title='My Services', generator_class=CoreAPISchemaGenerator)))
For security purposes I cannot share any snapshots. Apologies for that. Hope this helps.

I think what you are trying to do is not possible. The ViewSet does not provide any method handlers, hence, you cannot use the #action decorator on the methods create and list, as they are existing routes.

Related

Django REST endpoint for list of objects

I have a Django application which under /api/v1/crm/ticket can create tickets via a POST call. Now I want to be able to send different types of tickets (more then the one in the example code) to the same endpoint having a "dynamic" serializer depending on the data send. The endpoint should select the right "model" depending on the data properties existing in the request data.
I tried Django db.models but did not get them to work as I write the tickets to another external system and just pass them through, so no database table is existing and the model lacks the necessary primary key.
Can you help me out how to add more ticket types having the same endpoint?
Code
class TicketAPIView(CreateAPIView):
serializer_class = TicketSerializer
permission_classes = (IsAuthenticated,)
class TicketSerializer(serializers.Serializer):
title = serializers.CharField(max_length=256)
description = serializers.CharField(max_length=2048)
type = serializers.ChoiceField(TICKET_TYPES)
def create(self, validated_data):
if validated_data['type'] == 'normal':
ticket = TicketPOJO(
validated_data['title'],
validated_data['description'],
)
...
else:
raise Exception('Ticket type not supported')
return ticket
Files
/my-cool-app
/apps
/crm
/api
/v1
/serializers
serializers.py
__init.py
urls.py
views.py
/clients
/ticket
provider.py
/user
provider.py
/search
/config
Since your models are different for each of your ticket type, I would suggest you create an individual serializer that validates them for each different model with one generic view.
You can override the get_serializer method in your view to select an appropriate serializer depending upon the type of ticket. Something like this
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
type = self.request.data.get("type", '')
if type === 'normal':
return NormalTicketSerializer(*args, **kwargs)
elif type == 'abnormal':
return AbnormalTicketSerializer(*args, **kwargs)
else:
raise ParseError(detail='Ticket type not supported') # This will return bad request response with status code 400.
Hope this helps.

Exclude Endpoint HTTP methods from Django REST Swagger

Is there a way to hide just some of the methods of an endpoint and not the whole endpoint? (e.g. show the POST method but hide the DELETE one)
where I have tried to customize the documentation using the AutoSchema
For example an endpoint like
router.register(r'audittrial', AuditTrialViewSet, 'AuditTrial')
would have the following schema defined
class AuditTrialCustomView(AutoSchema):
#staticmethod
def get_field(name, required, location, schema, description):
return coreapi.Field(
name=name,
required=required,
location=location,
schema=schema,
description=description
)
def get_manual_fields(self, path, method):
extra_fields = []
if method == 'GET':
extra_fields = [
self.get_field("from", False, "query", coreschema.String(), "Date of the start of the Audit Trial"),
....
]
return extra_fields
Is there any method I would be able to achieve this?
DRF has following example - see if it helps you.
class CustomAutoSchema(AutoSchema):
def get_link(self, path, method, base_url):
# override view introspection here...
#api_view(['GET'])
#schema(CustomAutoSchema())
def view(request):
return Response({"message": "Hello for today! See you tomorrow!"})
so that api_view decorator should help you. it takes list of methods in list as an argument.

Accessing and Saving model objects directly without using serializers in django rest framework

I have created an api endpoint for one of my resources called "creative" . However I needed to have an action to implement pause and start of this resource (to change the field called status) . Hence I created a #detail_route() for the above actions .
What am I trying to achieve: I need to update a field of this particular resource(model) Creative as well as a field of another model whose foreign key is this model.
I am calling a custom function toggleAdGroupState inside this #detail_route() method where I directly work on the django models rather than using the serializers. Is this allowed ? Is this a proper way of implementing what i need to do ?
class CreativeSerializer(serializers.ModelSerializer):
class Meta:
model = Creative
fields = ('__all__')
#detail_route(methods=['post','get','put'])
def startcreative(self, request, pk=None):
try:
creative_object= self.get_object()
creative_object.active_status=Creative._STATUS_ACTIVE
creative_object.save()
toggleAdGroupState(creative_object.id,"active")
except Exception as e:
print 'error is ',str(e)
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_200_OK)
def toggleAdGroupState(creative_id,toggleFlag):
adgroup_obj = AdGroup.objects.filter(creative=creative_id)
for item in adgroup_obj:
if(toggleFlag == 'active'):
item.userActivate()
else:
item.userInactivate()
djangorestframework is 3.2.2
I believe are following these steps
user clicks to do post/put request.
this request active_status from
model creative Next you want to update Another Model.
You can do like this in view for each method. I have shown for put,
class View(generics.UpdateAPIView):
....
def update(self, request, *args, **kwargs):
# update creative model
response = generics.UpdateAPIView.update(self, request, *args, **kwargs)
if response.status == 200:
# update_another_model()

adding metadata to filtering in django rest framework

I have a generic ListCreateAPIView view. I've implemented a get_queryset function that performs a search. The function parses the query, extract tags and terms and returns a query set.
def get_queryset(self):
query = self.request.QUERY_PARAMS.get('query', None)
# No deleted items
queryset = Items.objects.filter(deleted__isnull=True)
if query is None:
return queryset
predicates = []
# Generate predicates from query
queryset = queryset.filter(reduce(__and__,predicates))
return queryset
What is the best way to add metadata to the response with data from the get_queryset function ?
I'm looking for something similar to the way pagination works.
{
query : {
terms : ['term1','term2'],
tags : ['tag1','tag2'] ,
}
results : [
{ name : 'item1', .... }
{ name : 'item2', .... }
]
}
EDIT
So i created a custom FilterBackend for the filtering and I now have an instance of the request and the response. Looking at the pagination code for django rest i see it's wrapping the results in serializer. The pagination is build into the view class so the fw invokes the serialization if a paginator is detected. Looking at the search api did not produce any new ideas.
My question remains, What is the best, and least intrusive way, of adding metadata from the filter backend to the response ?
One way i can think of (and one that i don't like) is to overload the matadata onto the request in the filter backend and override finalize_response in the view - without a doubt the worst way to do it.
I'm not sure it's the best way, but I would probably override get to simply intercept the response object and modify response.data however you wish. Something as simple as
from rest_framework import generics
class SomeModelList(generics.ListCreateAPIView):
"""
API endpoint representing a list of some things.
"""
model = SomeModel
serializer_class = SomeModelSerializer
def get(self, request, *args, **kwargs):
response = super(SomeModelList, self).get(request, *args, **kwargs)
# redefine response.data to include original query params
response.data = {
'query': dict(request.QUERY_PARAMS),
'results': response.data
}
return response
If you found yourself repeating this for multiple list views you could keep yourself DRY using a Mixin and include it in your list API classes:
from rest_framework import generics
from rest_framework.mixins import ListModelMixin
class IncludeQueryListMixin(ListModelMixin):
def list(self, request, *args, **kwargs):
response = super(IncludeQueryListMixin, self).list(request, *args, **kwargs)
# redefine response.data to include original query params
response.data = {
'query': dict(request.QUERY_PARAMS),
'results': response.data
}
return response
class SomeModelList(IncludeQueryListMixin, generics.ListCreateAPIView):
"""
API endpoint representing a list of some things.
"""
model = SomeModel
serializer_class = SomeModelSerializer

Tastypie error while updating - field has was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute

I keep getting this error when i try to update my resource.
The resource I am trying to update is called Message.
It has a foreign key to account:
class AccountResource(ModelResource):
class Meta:
queryset = Account.objects.filter()
resource_name = 'account'
'''
set to put because for some weird reason I can't edit
the other resources with patch if put is not allowed.
'''
allowed_methods = ['put']
fields = ['id']
def dehydrate(self, bundle):
bundle.data['firstname'] = bundle.obj.account.first_name
bundle.data['lastname'] = bundle.obj.account.last_name
return bundle
class MessageResource(ModelResource):
account = fields.ForeignKey(AccountResource, 'account', full=True)
class Meta:
queryset = AccountMessage.objects.filter(isActive=True)
resource_name = 'message'
allowed = ['get', 'put', 'patch', 'post']
authentication = MessageAuthentication()
authorization = MessageAuthorization()
filtering = {
'account' : ALL_WITH_RELATIONS
}
Now when i try to update my Message using a PATCH, I get this error:
Data passed in: {"text":"blah!"}
The 'account' field has was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: <Bundle for obj: '<2> [nikunj]' and with data: '{'lastname': u'', 'id': u'2', 'firstname': u'', 'resource_uri': '/api/v1/account/2/'}'>.
Bad Solution::
Pass in the data: {"text":"blah!", "account":{"pk":2}}
I dont want to pass in the account. I just want to edit the text and nothing else. Why is there a need to pass in the account too?
I tried to use:
def obj_update(self, bundle, request=None, **kwargs):
return super(ChartResource, self).obj_update(bundle, request, account=Account.objects.get(account=request.user))
BUT it doesnt work!!
HELP!
Since you are using a PUT method, Tastypie expects the 'account' field within the bundle.data object when you are calling the obj_update method.
Also the keyword arguments in obj_update are not used to set values like in obj_create, they are used to search the object in question. So when you pass on the account=Account... keyword argument, you are simply telling the obj_create method to search within the AccountMessage table and filter by account.
One way to solve this problem is to set the readonly value to True
Class MessageResource(ModelResource):
account = fields.ForeignKey(AccountResource, 'account', full=True,
readonly=True)
If you need to set the account when you are creating your message, I suggest setting it on your obj_create method
def obj_create(self, bundle, request=None, **kwargs):
account = Account.objects.get(account=request.user)
return super(MessageResource,
self).obj_create(bundle, request, account=account, **kwargs)
This seems like a better solution, since from what I can understand from your code, the account value points to the creator of the message so it should be set by the system and not the user.
Try it with a Resource that doesn't have full=True in it. Tastypie has some different expectations for Resources with full=True in them -- mainly that the entire record be included. I believe this includes the child resources too.