DRF YASG Customizing - django

I'm trying to customize my api documentation buuild with yasg.
First off, I would like to determine the naming of my own sections, and what endpoints should be included in this section. It seems that the naming of sections is based on the first prefix not belonging to the longest common prefix e.g.:
if we have the urls api/v1/message and api/v1/test than the sections will be named message and test. Is there a way for me to determine A custom naming for this section?
Furthermore, the introduction of every section is empty, how do I add text here?
And last but not least, Stripe has these amazing section dividers how can I add these in drf yasg.

Currently, I'm using APIView and #swagger_auto_schema to define the documentation of my endpoint.
In the code below, you can see how to add more information to define your endpoint. I hope it helps you
##serializers.py
class CategorySerializer(serializers.ModelSerializer):
"""
Serializing Categories
"""
class Meta:
model = Category
fields = [
'id', 'name', 'slug'
]
read_only_fields = [
'slug',
]
##views.py
username_param = openapi.Parameter('username', in_=openapi.IN_QUERY, description='Username',
type=openapi.TYPE_STRING)
email = openapi.Parameter('email', in_=openapi.IN_QUERY, description='Email',
type=openapi.TYPE_STRING)
category_response = openapi.Response('response description', CategorySerializer)
class CategoryList(APIView):
permission_classes = [AllowAny]
#swagger_auto_schema(
manual_parameters=[username_param, email],
query_serializer=CategorySerializer,
responses = {
'200' : category_response,
'400': 'Bad Request'
},
security=[],
operation_id='List of categories',
operation_description='This endpoint does some magic',
)
def get(self, request, format=None):
"""
GET:
Return a list of all the existing categories.
"""
categories = Category.objects.all()
serializer = CategorySerializer(categories, many=True)
return Response(serializer.data)
#swagger_auto_schema(
request_body=CategorySerializer,
query_serializer=CategorySerializer,
responses={
'200': 'Ok Request',
'400': "Bad Request"
},
security=[],
operation_id='Create category',
operation_description='Create of categories',
)
def post(self, request, format=None):
"""
POST:
Create a new category instance.
"""
serializer = CategorySerializer(data=request.data)
if serializer.is_valid():
serializer.save(created_by=self.request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
By last, if you want to see your endpoints in groups by link, you can test commenting the line below in yours urls.py
#urlpatterns = format_suffix_patterns(urlpatterns)
Below, some screen of how you should see it
You can find more information in the following link: https://drf-yasg.readthedocs.io/en/stable/custom_spec.html

Related

string list as a parameter in url in django

i am working in a search view which take two inputs as a filter :
search word
2.cities (multi select)
i did it using serializer and it worked but paggination does not work because it was a post method , so i'm trying to take these inputs from url parameters , i tried this pattern :
path(r"search/<str:search>/(?P<city>\w*)/",
SearchView.as_view({"get": "search"}), name="search"),
but when i browse : http://127.0.0.1:8000/company/search/taxi/montrial/
it return Not Found: /company/search/
so how to pass the paramteres or is there another way to use paggination with post method
I suggest to use pagination with get request or in case you should do it with post
class CustomPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(), #you can read page number from url and put it here
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})
to read data from url you can use request.query_params
https://www.django-rest-framework.org/api-guide/pagination/
i solved it using the serializer method, inherit from viewsets.GenericViewsets which has pagingation methods
class SearchView(viewsets.GenericViewSet):
permission_classes = [IsDriver]
queryset = CompanyProfile.objects.all()
serializer_class = SearchSerializer
#action(methods=['post'], detail=False)
def search(self, request, format=None):
serializer = SearchSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
page = self.paginate_queryset(serializer.data["companies"])
if page is not None:
# used CompanyProfileSerializer to serialize the companies query
serializer = CompanyProfileSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(serializer.data)

Testing custom action on a viewset in Django Rest Framework

I have defined the following custome action for my ViewSet Agenda:
class AgendaViewSet(viewsets.ModelViewSet):
"""
A simple viewset to retrieve all the Agendas
"""
queryset = Agenda.objects.all()
serializer_class = AgendaSerializer
#action(detail=False, methods=['GET'])
def get_user_agenda(self, request, pk=None):
print('here1')
id = request.GET.get("id_user")
if not id:
return Response("No id in the request.", status=400)
id = int(id)
user = User.objects.filter(pk=id)
if not user:
return Response("No existant user with the given id.", status=400)
response = self.queryset.filter(UserRef__in=user)
if not response:
return Response("No existant Agenda.", status=400)
serializer = AgendaSerializer(response, many=True)
return Response(serializer.data)
Here, I'd like to unit-test my custom action named "get_user_agenda".
However, when I'm testing, the debug output("here1") doesn't show up, and it always returns 200 as a status_code.
Here's my test:
def test_GetUserAgenda(self):
request_url = f'Agenda/get_user_agenda/'
view = AgendaViewSet.as_view(actions={'get': 'retrieve'})
request = self.factory.get(request_url, {'id_user': 15})
response = view(request)
self.assertEqual(response.status_code, 400)
Note that:
self.factory = APIRequestFactory()
Am I missing something?
Sincerely,
You will have to use the method name of the custom action and not retrieve so:
view = AgendaViewSet.as_view(actions={'get': 'get_user_agenda'})
You have to specify request url
#action(detail=False, methods=['GET'], url_path='get_user_agenda')
def get_user_agenda(self, request, pk=None):
And in my opinion it would be better to use detail=True, and get pk from url.
For example: 'Agenda/pk_here/get_user_agenda/'

How Configure Django Rest Framework to return errors with custom text

I want to customize the JSON response when adding a new item to the database it returns the following.
HTTP 400 BAD REQUEST
Content-Type: application/json Vary:
Accept Allow: POST, OPTIONS
{
"nick": [
"Users with this Nick already exists."
]
}
and
{
"nick": [
"Your username is empty"
]
}
I want it to return (This username already exists, please use a different one.)
or
"Username %s already exists", (self.nick)
I used the following sample but does not work if the value is empty or invalid.
def validate_title(self, attrs, source):
"""
Check that the blog post is about Django.
"""
value = attrs[source]
if "django" not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return attrs
this is the JSON that gets sent to the API.
{
"name": "myname",
"nick":"",
"type_account":"1",
"email":"my-email#gmail.com",
"pass_field":"12345"
}
serializers.py
class userSerializer(serializers.ModelSerializer):
class Meta:
model = users
fields = ('nick', 'email', 'pass_field', 'type_account')
def validate_nick(self, attrs, source):
value = attrs[source]
if not value:
raise serializers.ValidationError('Username cannot be empty')
elif self.Meta.model.objects.filter(nick=value).exists():
raise serializers.ValidationError("Username "+value+" is in use")
return attrs
views.py
#api_view(['POST'])
def user_add(request):
"""
Saves a new user on the database
"""
if request.method == 'POST':
serializer = userSerializer(data=request.DATA)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
the answer to this question also adds the following as #Fiver answered
class userLoginSerializer(serializers.ModelSerializer):
nick = serializers.CharField(error_messages={'required':'Please Type a Username'})
pass_field = serializers.CharField(error_messages={'required':'Please Type a Password'})
class Meta:
model = users
fields = ('nick', 'pass_field')
I believe something like the following will work:
def validate_nick(self, attrs, source):
"""
Check that 'nick' is not already used or empty.
"""
value = attrs[source]
if not value:
raise serializers.ValidationError("Nick cannot be empty!")
elif self.Meta.model.objects.filter(nick=value).exists():
raise serializers.ValidationError("Username %s already exists", value)
return attrs
It should be possible to alter the error messages at the model level, but unfortunately REST Framework doesn't support that yet. Here is an issue dealing with the problem. It includes a suggested method for overriding the validator in the serializer.
You should use custom error handler. Follow here to setup.
Your Custom error handler should be like this:
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
for field, value in response.data.items():
value = ''.join(value)
response.data = {} # Empty django's custom error
response.data['detail'] =value #customize it how you want
return response

Django REST framework post array of objects

I am using Django REST framework for API and Angular SPA with Restangular to communicate with the API. Sometimes, I have to add more than one object using the API and I think I can send them together in an array and do this in one request.
I receive wrong input error when I'm trying to add more than one object from the REST framework web interface. I am passing objects or array of objects like below:
// this { "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }
// or this [{ "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }]
But I receive ParseError. Where I am wrong and what do I have to change to fix this?
Another example that supports posting an array as well as posting a single object. Might be useful for anyone else looking for such an example.
class BookViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
ViewSet create and list books
Usage single : POST
{
"name":"Killing Floor: A Jack Reacher Novel",
"author":"Lee Child"
}
Usage array : POST
[{
"name":"Mr. Mercedes: A Novel (The Bill Hodges Trilogy)",
"author":"Stephen King"
},{
"name":"Killing Floor: A Jack Reacher Novel",
"author":"Lee Child"
}]
"""
queryset = Book.objects.all()
serializer_class = BookSerializer
search_fields = ('name','author')
def create(self, request, *args, **kwargs):
"""
#checks if post request data is an array initializes serializer with many=True
else executes default CreateModelMixin.create function
"""
is_many = isinstance(request.data, list)
if not is_many:
return super(BookViewSet, self).create(request, *args, **kwargs)
else:
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
I am not sure if the problem still exist. But the solution suggested by fiver did not work for me.
What works for me is overriding the get_serializer method ONLY.
def get_serializer(self, instance=None, data=None,
files=None, many=True, partial=False):
return super(ViewName, self).get_serializer(instance, data, files,
many, partial)
If you will notice I am setting default many=True in arguments of get_serializer.
Apart from that nothing is required. Overridng of create method is also not required.
Also if you are defining the pre_save and post_save method in the views, expects the list(iterable) as the argument(as you are posting the list) of method not just a single object.
def post_save(self, objects, *args, **kwargs):
"""
In the post_save, list of obj has been created
"""
for obj in objects:
do_something_with(obj)
Here's an example for setting up bulk POSTing in a ListCreateAPIView using the Django REST Framework:
class SomethingList(generics.ListCreateAPIView):
model = Something
serializer_class = SomethingSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, many=True)
if serializer.is_valid():
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The important part here is the many=True argument to the get_serializer() method. Then, to make Angular play nice with this, you can define a service factory as:
.factory('Something', ['$resource', function ($resource) {
return $resource(
"url_to_something",
{},
{
save: {
method: 'POST',
isArray: true
}
}
);
}])
Where the important part is the isArray: true. If you want to preserve posting single JSON objects, you could change save above to something like saveBulk or similar.
Building on vibhor's answer:
class ListableViewMixin(object):
def get_serializer(self, instance=None, data=None, many=False, *args, **kwargs):
return super(ListableViewMixin, self).get_serializer(
instance=instance, data=data, many=isinstance(instance, list) or isinstance(data, list),
*args, **kwargs)
Make your view inherit from this mixin class to automatically determine if a many=True serializer should be used.
If you want to post a list you have to pass in JSON encoded data.
headers = {"Token": "35754sr7cvd7ryh454"}
recipients = [{'name': 'Ut est sed sed ipsa',
'email': 'dogoka#mailinator.com',
'group': 'signers'},
{'name': 'Development Ltda.',
'email': 'test#test.com',
'group': 'signers'}
]
requests.post(url, json=recipients, headers=headers)
requests.post(url, json=recipients, headers=headers)
Use json keyword argument (not data) so the data is encoded to JSON and the Content-Type header is set to application/json.
By default, Django Rest Framework assumes you are passing it a single object. To serialize a queryset or list of objects instead of a single object instance, you should pass the many=True flag when instantiating the serializer. You can then pass a queryset or list of objects to be serialized.
To do it, you'll have to override the .create() method of your view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = self.get_serializer(data=request.data, many=many)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Documentation: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects

Django REST: Cannot add (write) nested item

I am attempting to POST a nested relationship with Django REST Framework (2.3.9), but I am getting this error:
ValueError: Cannot add "": instance is on database "default", value is on database "None"
It seems that Django (1.6) is not recognizing that my HeartbeatSerializer knows anything about ItemSerializer
{
"username":"testing",
"heartbeat": {
"items": [
{
"item_date": "2013-11-24T05:08:12Z",
"item_title": "disagreeing post",
"item_type": "post",
"item_karma": 25
}
]
}
}
Here's the serializers.py:
class ItemSerializer(serializers.ModelSerializer):
'''
Item Serializer
Items are User-generated Content which are captured
from various APIs made available from the Web or
Internet via REST consumed endpoints or Websites
which have been scraped.
'''
class Meta:
model = Item
# fields = ('id', 'item_title', 'item_karma',
# 'item_type', 'item_date', )
class DepthPatchSerializer(serializers.ModelSerializer):
def __init__(self, model=None, **kwargs):
if model is not None:
self.Meta.model = model
super(DepthPatchSerializer, self).__init__(**kwargs);
def get_nested_field(self, model_field):
return DepthPatchSerializer(model=model_field.rel.to)
class HeartbeatSerializer(DepthPatchSerializer):
'''
Heartbeat Serializer
Items are User-generated Content (USG) containers which are used
to consolidate normalized User-generated Interactions (USI).
'''
# items_queryset = Item.objects.all()
# items = ItemSerializer(items_queryset, many=True)
class Meta:
model = Heartbeat
# fields = ('id', 'items', )
depth = 1
class HackerAddSerializer(serializers.ModelSerializer):
'''
User Serializer
Each User is stored in the system itself for analysis and signaling.
We store users such that subsets of user behavior through USG can be
generated and analyzed.
'''
heartbeat = HeartbeatSerializer()
def _save_heartbeat_data(self):
heartbeat_data = self.init_data.get('heartbeat', None)
if heartbeat_data:
hs = HeartbeatSerializer(instance=self.object.heartbeat,
data=heartbeat_data)
import pdb; pdb.set_trace() # XXX BREAKPOINT
if hs.is_valid():
self.object.heartbeat = hs.object
hs.save()
else:
raise Exception(hs.errors)
def save(self):
self._save_heartbeat_data()
password = self.init_data.get('password', None)
confirm = self.init_data.get('confirm', None)
if password and password == confirm:
self.object.set_password(password)
super(HackerAddSerializer, self).save()
return self.object
class Meta:
model = Hacker
exclude = ['last_login', 'password',
'is_superuser', 'is_staff',
'is_active', 'user_permissions',
'groups', ]
Nested Serializers are Readonly at present (out of the box). You have to write your own conversion methods to support writable nested serializers (see docs).
There's work to implement writable nested serializers in the django rest framework project.
Instead, I'd suggest you use a RelatedField (like PrimaryKeyRelatedField or HyperlinkedRelatedField) to reference nested models. You can use the depth option to have these related fields nested when you serialize the data to the client.