I have a django-application with the followin files
models.py
from datetime import datetime
class Comment(object):
def __init__(self, email, content, created=None):
self.email = email
self.content = content
self.created = created or datetime.now()
serializers.py
from rest_framework import serializers
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
url = serializers.CharField(source='get_absolute_url', read_only=True)
in views.py I now define a a ViewSet to return the serialized results. In this class I define a list of comments
views.py
from rest_framework import viewsets
from .serializers import *
from .models import Comment
from rest_framework.response import Response
class CommentViewSet(viewsets.ViewSet):
lc = [Comment(email='jan#auto.com', content='hallo mike'),
Comment(email='jan#auto.com', content='hallo jan'),
Comment(email='jan#auto.com', content='hallo niklas')]
def list(self, request):
serializer = CommentSerializer(self.lc, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
user = self.lc[int(pk)]
serializer = CommentSerializer(user)
return Response(serializer.data)
When I now call the api (http://127.0.0.1:8000/comments/?format=json) I get the following result
[
{
"email": "jan#auto.com",
"content": "hallo mike",
"created": "2019-08-16T16:53:56.371890Z"
},
{
"email": "jan#auto.com",
"content": "hallo jan",
"created": "2019-08-16T16:53:56.371890Z"
},
{
"email": "jan#auto.com",
"content": "hallo niklas",
"created": "2019-08-16T16:53:56.371890Z"
}
]
In this response I would have hoped to see a url for each dataset. The error ist probably thats for url = serializers.CharField(source='get_absolute_url', read_only=True) the source is undefined in the Comment class. However I have no clue how to achieve this. Any help is appreciated.
You need to define a get_absolute_url method [Django-doc] on your model, like:
# app/models.py
from django.db import models
from django.urls import reverse
class Comment(models.Model):
email = models.EmailField()
content = models.CharField(max_length=128)
created = models.DateTimeField(auto_now_add=True)
def get_absolute_url(self):
return reverse('name-of-view', kwargs={'pk': self.pk})
Here 'name-of-view' is the name of the view you defined, for example in your urls.py, and the kwargs=... is given a dictionary that maps the values for the corresponding URL path parameters.
Related
I am new in Django and python. Now I am trying to do web API with Django and python. My GET, POST, and DELETE requests are working, but PUT gives me error:
{
"non_field_errors": [
"No data provided"
] }
(i used Postman)
Here's my code:
Serializer:
from rest_framework import serializers
from .models import Topic
class TopicSerializer(serializers.ModelSerializer):
title = serializers.CharField(max_length=50)
text = serializers.CharField(max_length=500)
class Meta:
model = Topic
fields = [
'title', 'text'
]
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('text', instance.description)
instance.save()
return instance
Views:
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Topic
from .serializers import TopicSerializer
class TopicView(APIView):
def get(self, request):
topics = Topic.objects.all()
serializer = TopicSerializer(topics, many=True)
return Response({'topic': serializer.data})
def post(self, request):
topic = request.data
serializer = TopicSerializer(data=topic)
if serializer.is_valid(raise_exception=True):
topic_saved = serializer.save()
return Response({'success': 'Topic {} created successfully'.format(topic_saved.title)})
def put(self, request, pk):
# saved_topic = get_object_or_404(Topic.objects.all())
saved_topic = get_object_or_404(Topic.objects.filter(id=pk))
data = request.data.get('topic')
serializer = TopicSerializer(instance=saved_topic, data=data, partial=True)
if serializer.is_valid(raise_exception=True):
topic_saved = serializer.save()
return Response({
"success": "Topic '{}' updated successfully".format(topic_saved.title)
})
def delete(self, request, pk):
topic = get_object_or_404(Topic.objects.all(), pk=pk)
topic.delete()
return Response({
"message": "Topic with id `{}` has been deleted.".format(pk)
}, status=204)
App URLs:
from django.urls import path
from .views import TopicView
app_name = "rest_test_app"
# app_name will help us do a reverse look-up latter.
urlpatterns = [
path('topics/', TopicView.as_view()),
path('topics/<int:pk>', TopicView.as_view())
]
request body:
{
"title": "pipiska",
"text": "pipiska111"
}
is this because of using wrong methods?
(sorry for terrible english)
Two changes and PUT request will work fine.
One is in the serializers.py file.
Instead of instance.description = validated_data.get('text', instance.description) change it to instance.text = validated_data.get('text', instance.text)
Another one, as mentioned in the comments, in views.py in put definition use data = request.data instead of data = request.data.get('topic')
Then give PUT request from Postman with the following request body:
{
"title": "pipiska",
"text": "pipiska111"
}
it will work fine.
I want to save a new user via POST request using the #api_view decorator in DRF but got the following error :
{
"detail": "JSON parse error - Expecting ':' delimiter: line 4 column 21 (char 103)"
}
models.py code:
from django.db import models
from django.contrib.auth.models import User
from PIL import Image
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(User, on_delete = models.CASCADE)
image = models.ImageField(default = 'users/default.jpg', upload_to = 'users/profile_pics')
def __str__(self):
return f'{self.user.username} Profile'
def save(self, **kwargs):
super().save()
img = Image.open(self.image.path)
if img.height > 300 or img.width > 300:
output_size = (300, 300)
img.thumbnail(output_size)
img.save(self.image.path)
views.py code:
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.http import HttpResponse, JsonResponse
from rest_framework.parsers import JSONParser
from .serializers import UserSerializer
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
#api_view(['GET', 'POST'])
def user_list(request):
if request.method == 'GET':
users = User.objects.all().order_by('id')
serializer = UserSerializer(users, many = True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = UserSerializer(data = request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_created)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
serializers.py code:
from rest_framework import serializer
from django.contrib.auth.models import User
from .models import Profile
from django.contrib.auth.hashers import make_password
class UserSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only = True)
password = serializers.CharField(
required = False,
write_only = True,
style = {"input_type": "password", "placeholder": "Password"}
)
class Meta:
model = User
fields = ['id', 'username', 'email', 'password']
def create(self, validated_data):
validated_data['password'] = make_password(validated_data.get('password'))
return super(UserSerializer, self).create(validated_data)
urls.py code:
from django.urls import path, include
from .views import user_list, user_detail
from rest_framework import routers
urlpatterns = [
path('users/', user_list),
path('users/<int:pk>/', user_detail),
]
I am learning DRF from scratch and am following the documentation accordingly. I am not sure why I am getting the HTTP 400 Bad Request error here. It would be great if anyone can help me out.
This is an example of my POST request:
{
"username": "user_1",
"email": "user1m#gmail.com",
"password" : "testing1234"
}
ID is auto-incremented and a password is required (as mentioned in the serializer.py code)
you should post like this:
{
"username": "user_1",
"email": "user1m#gmail.com",
"password" : "testing1234"
}
Let's say I have three models as:
class User(AppModel):
name = models.CharField(max_length=255)
class Business(AppModel):
owner = models.ForeignKey("User", related_name="businesses", on_delete=models.CASCADE)
legal_name = models.CharField(max_length=255)
class Invoice(AppModel):
business = models.ForeignKey("Business", related_name="invoices", on_delete=models.CASCADE)
amount = models.integerField()
As you can see, a user can have multiple businesses and a business can have multiple invoices.
My serializers.py:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields= ('name')
class BusinessSerializer(serializers.ModelSerializer):
owner = UserSerializer(many=False)
class Meta:
model = Business
fields= ('owner','legal_name')
class InvoiceSerializer(serializers.ModelSerializer):
business= BusinessSerializer(many=False)
class Meta:
model = Invoice
fields= ('business','amount')
views.py:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
class InvoiceViewSet(viewsets.ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
urls.py:
router = DefaultRouter()
router.register('user', UserViewSet, base_name='users')
router.register('business', BusinessViewSet, base_name='businesses')
router.register('invoice', InvoiceViewSet, base_name='invoices')
urlpatterns = router.urls
http://example.com/api/user returns all users. Not a problem.
But the functionality I'm looking for is:
http://example.com/api/business/ returns
[
{
"legal_name": "1business",
"owner": 1,
},
{
"legal_name": "2business",
"owner": 1,
},]
http://example.com/api/business/1/ returns
{
"legal_name": "1business",
"owner": 1,
}
The above is ok. But I also need:
http://example.com/api/business/1/invoices/ should return
[
{
"business": 1,
"amount": 100,
},
{
"business": 1,
"amount": 999,
},]
As well I should be able to create update delete those invoices there.
Any Help? I'm new to django rest framework. The above classes are just a sample. Ignore errors.
You should use django decorators which are #list_route and #detail_route for your viewset. But be careful with your DRF version. Because those decorators merged together as #action in DRF 3.8+. Here is the announcement.
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class BusinessViewSet(viewsets.ModelViewSet):
queryset = Business.objects.all()
serializer_class = BusinessSerializer
#action(detail=True, methods=["GET"], url_path="invoices")
def invoices(self, request, pk=None):
"""
Your codes comes here to return related result.
pk variable contains the param value from url.
if you do not specify the url_path properties then action will accept the function's name as url path.
"""
entity = Invoice.objects.filter(business=pk)
serializer = self.get_serializer(entity, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Then, you will be able to call this endpoints from;
http://example.com/api/business/{{PK}}/invoices/
http://example.com/api/business/1/invoices/
http://example.com/api/business/3/invoices/
http://example.com/api/business/23/invoices/
Here you can find more details about #actions from documentation.
PS: Don't forget to control empty entity results in your codes. You should return correct response with correct status codes.
im trying to add a custom action to my ViewSet in Django2, using django-rest-framework. Problem is that my serializer is not serializing nested model and thus giving me error:
{
"labels": [
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got Label."
]
},
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got Label."
]
}
]
}
I have two models which have M:N relationship.
Label model:
class Label(models.Model):
name = models.CharField(max_length=30, help_text='Name of Label')
desc = models.CharField(max_length=200, help_text='Description of Label')
def __str__(self):
return self.name
LabelSet model:
class LabelSet(models.Model):
labels = models.ManyToManyField(Label, blank=True, help_text='ManyToMany field of corresponding labels')
name = models.CharField(max_length=30, help_text='Name of Label Set')
desc = models.CharField(max_length=200, help_text='Description of Label Set')
def __str__(self):
return self.name
Machine Model:
class Machine(models.Model):
name = models.CharField(max_length=30, help_text='Name of machine')
desc = models.CharField(max_length=200, help_text='Description of machine')
location = models.ForeignKey(Location, null=True, blank=True, on_delete=models.CASCADE, help_text='ID of machine location')
labelset = models.ForeignKey(LabelSet, null=True, blank=True, on_delete=models.DO_NOTHING, help_text='ID of set of labels relevant for this machine')
def __str__(self):
return self.name
Serializers:
class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = '__all__'
class LabelSetSerializer(serializers.ModelSerializer):
qs = Label.objects.all().values()
labels = LabelSerializer(qs, many=True)
class Meta:
depth = 1
model = LabelSet
fields = ('name', 'desc', 'labels')
Custom action in viewsets.py (I want to retrieve available labels by machine, so path is /machines/{id}/labels
class MachineViewSet(viewsets.ModelViewSet):
'''
A viewset used for retrieving and editing Machine instances.
'''
#permission_classes = (DRYPermissions,)
serializer_class = MachineSerializer
queryset = Machine.objects.all()
# /api/v1/machines/{machine_id}/labels
#action(detail=True)
def labels(self, request, pk=None):
# Get labelset id
ls = Machine.objects.get(pk=pk).labelset
# Get LabelSet instance
serializer = LabelSetSerializer(data=model_to_dict(ls))
if serializer.is_valid():
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The endpoint works fine, but when querying /machines/1/labels i got the response which is the first snippet:
"Invalid data. Expected a dictionary, but got Label."
Im literally out of ideas, tried even making dict from qs = Label.objects.all().values() in Serializer, no luck.
Thanks to #Jerin Peter George, output is now:
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"name": "TestSet",
"desc": "asd",
"labels": [
{
"id": 1,
"name": "OK",
"desc": "desc"
},
{
"id": 2,
"name": "Broken",
"desc": "asd"
}
]
}
So /api/v1/machines/1/labels works, but suddenly /api/v1/machines/ does not. (502 Bad Gateway with error TypeError: 'LabelSet' object is not iterable)
APP level urls:
from django.conf.urls import url
from devices.viewsets import *
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'devices', DeviceViewSet, base_name='device')
router.register(r'projects', ProjectViewSet, base_name='project')
router.register(r'locations', LocationViewSet, base_name='location')
router.register(r'industries', IndustryViewSet, base_name='industry')
router.register(r'companies', CompanyViewSet, base_name='companies')
router.register(r'project_types', ProjectTypeViewSet, base_name='project_type')
router.register(r'device_types', DeviceTypeViewSet, base_name='device_type')
router.register(r'machines', MachineViewSet, base_name='machine')
router.register(r'records', RecordViewSet, base_name='record')
router.register(r'labels', LabelViewSet, base_name='label')
router.register(r'labelsets', LabelSetViewSet, base_name='label_set')
urlpatterns = router.urls
App level urls.py
from django.contrib import admin
from django.conf.urls import url
from django.urls import include, path
from rest_framework.documentation import include_docs_urls
from rest_framework_expiring_authtoken import views
from devices.views import AudioUploadView
API_PREFIX = 'api/v1/'
urlpatterns = [
url(API_PREFIX + 'docs/', include_docs_urls(title='API Docs')),
url(API_PREFIX + 'admin/', admin.site.urls),
url(API_PREFIX + 'api-token-auth/', views.obtain_expiring_auth_token),
path(API_PREFIX, include('devices.urls'))
]
EDIT: SOLVED
Apparently i added one more nested serializer to MachineSerializer
class MachineSerializer(serializers.ModelSerializer):
labelsets = LabelSetSerializer(many=True)
class Meta:
model = Machine
fields = '__all__'
So removing the line labelsets = LabelSetSerializer(many=True) did the trick.
And that is where the error came from, now is everything working as expected, thanks:)
Replace your labels() with below snippet,
#action(detail=True)
def labels(self, request, pk=None):
# Get labelset id
ls = Machine.objects.get(pk=pk).labelset
# Get LabelSet instance
serializer = LabelSetSerializer(ls)
return Response(serializer.data)
This is my models.py:
from tastypie.utils.timezone import now
from django.contrib.auth.models import User
from django.db import models
from django.template.defaultfilters import slugify
class Link(models.Model):
user = models.ForeignKey(User)
pub_date = models.DateTimeField(default=now)
title = models.CharField(max_length=200)
slug = models.SlugField()
body = models.TextField()
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
# For automatic slug generation.
if not self.slug:
self.slug = slugify(self.title)[:50]
return super(Link, self).save(*args, **kwargs)
class OAuthConsumer(models.Model):
name = models.CharField(max_length=255)
key = models.CharField(max_length=255)
secret = models.CharField(max_length=255)
active = models.BooleanField(default=True)
class Meta:
db_table = "api_oauth_consumer"
def __unicode__(self):
return u'%s' % (self.name)
Everything works now and I get this as response to: /api/v1/links/list/?format=json
{
"meta": {
"previous": null,
"total_count": 1,
"offset": 0,
"limit": 20,
"next": null
},
"objects": [
{
"body": "http://www.youtube.com/watch?v=wqQ6BF50AT4&feature=relmfu",
"title": "Youtube",
"slug": "main-kya-karoon",
"user": "/api/v1/users/1/",
"pub_date": "2012-10-01T00:23:53",
"id": 1
}
]
}
I want to make these changes:
Pass the username and get all the links belonging to that username.
I am currently adding content and creating new user via django admin as while doing post I always manage to get an error. I think I might have gone wrong so any help maybe a curl one liner of creating a new user using my current api would be helpful.
Edit:
This is my new api.py(I decided to create a new UserSignUpResource):
# myapp/api.py
from django.contrib.auth.models import User
from tastypie.authorization import Authorization
from tastypie import fields
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from links.models import Link
from tastypie.serializers import Serializer
from tastypie.admin import ApiKeyInline
from tastypie.models import ApiAccess, ApiKey
from django.db import models
from tastypie.authentication import ApiKeyAuthentication
from tastypie.models import create_api_key
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'users'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
authorization = Authorization()
allowed_methods = ['post','get']
fields = ['username']
def obj_create(self, bundle, request=None, **kwargs):
try:
bundle = super(CreateUserResource, self).obj_create(bundle, request, **kwargs)
bundle.obj.set_password(bundle.data.get('password'))
bundle.obj.set_username(bundle.data.get('username'))
bundle.obj.save()
except IntegrityError:
raise BadRequest('That username already exists')
return bundle
class LinkResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
authorization = Authorization()
class Meta:
queryset = Link.objects.all()
resource_name = 'links/list'
excludes = ['id']
authorization = Authorization()
include_resource_uri = False
excludes = ['limit']
def apply_filters(self,request,applicable_filters):
base_object_list = super(LinkResource, self).apply_filters(request, applicable_filters)
query = request.META.get('HTTP_AUHTORIZATION')
if query:
qset = (
Q(api_key=query))
base_object_list = base_object_list.filter(qset).distinct()
return base_object_list
class UserSignUpResource(ModelResource):
class Meta:
object_class = User
queryset = User.objects.all()
allowed_methods = ['post']
include_resource_uri = False
resource_name = 'newuser'
excludes = ['is_active','is_staff','is_superuser']
authentication = ApiKeyAuthentication()
authorizaton = Authorization()
models.signals.post_save.connect(create_api_key, sender=User)
def obj_create(self,bundle,request=None,**kwargs):
try:
bundle = super(UserSignUpResource, self).obj_create(bundle,request,**kwargs)
bundle.obj.set_password(bundle.data.get('password'))
bundle.obj.save()
except IntegrityError:
raise BadRequest('The username already exists')
return bundle
def apply_authorization_limits(self,request,object_list):
return object_list.filter(id=request.user.id,is_superuser=True)
Now when I do this:
curl -v -X POST -d '{"username" : "puck", "password" : "123456"}' -H "Authorization: ApiKey superusername:linklist" -H "Content-Type: application/json" http://127.0.0.1:8000/api/v1/newuser
I get this a 404 error: The url doesn't exist. I checked and rechecked and I don't see any problem with the url.
Edit:
It was a rather dumb mistake on my part. I had forgotten to register the UserSignUpResource in urls.py.
1. Pass the username and get all the links belonging to that username
Look at filtering resources based on the api call in tastypie as explained in passing request variables in django/tastypie resources
For creating user see How to create or register User using django-tastypie API programmatically?