Accessing comments on an object via reverse relationship with Tastypie - python

I'm building an API using Tastypie with Django and I've run into a bit of an issue.
I have a model called a Moment (basically a blog post, with a title and body text), and I want to be able to attach comments to it and retrieve them via the API. I'm using django.contrib.comments with Django 1.6.5 and Tastypie 0.11.1.
Now, according to the Tastypie documentation, this should be straightforward. What I've implemented is pretty close to that. This is my models.py:
class Moment(models.Model):
"""
Represents a Moment - a statement by a user on a subject
"""
ZONE_CHOICES = (
('Communication', 'Communication'),
('Direction', 'Direction'),
('Empathy', 'Empathy'),
('Flexibility', 'Flexibility'),
('Motivation', 'Motivation'),
('Ownership', 'Ownership'),
('Persistence', 'Persistence'),
('Reliability', 'Reliability'),
('Teamwork', 'Teamwork'),
)
STATUS_CHOICES = (
('Open', 'Open'),
('More Info', 'More Info'),
('Closed', 'Closed'),
)
title = models.CharField(max_length=200)
text = models.TextField()
datetime = models.DateTimeField(default=timezone.now())
zone = models.CharField(max_length=200,
choices=ZONE_CHOICES)
sender = models.ForeignKey(Student, blank=True, null=True, related_name="sender")
status = models.CharField(max_length=200,
default='Open',
choices=STATUS_CHOICES)
recipient = models.ForeignKey(Sponsor, blank=True, null=True, related_name="recipient")
comments = generic.GenericRelation(Comment, object_id_field='object_pk')
def save(self, *args, **kwargs):
"""
Override the save() method to set the recipient dynamically
"""
if not self.recipient:
self.recipient = self.sender.sponsor
super(Moment, self).save(*args, **kwargs)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-datetime"]
And this is my api.py:
class MomentResource(BaseResource):
"""
Moment resource
"""
sender = fields.ForeignKey(StudentResource, 'sender', full=True, readonly=True)
comments = fields.ToManyField('myapp.api.CommentResource', 'comments', blank=True, null=True)
class Meta:
"""
Metadata for class
"""
queryset = Moment.objects.all()
resource_name = 'moment'
always_return_data = True
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
filtering = {
'zone': ALL,
}
class CommentResource(ModelResource):
"""
Comment resource
"""
moment = fields.ToOneField(MomentResource, 'moment')
class Meta:
queryset = Comment.objects.all()
resource_name = 'comments'
However, the comments always come back blank.
Now, I know that the model seems to be correct because in the Django shell, the following returns the comments on a Moment:
Moment.objects.all()[0].comments.all()
I think the problem is therefore in api.py, but I haven't been able to track it down. Can anyone see where I've gone astray?

Finally got it working with the following:
class MomentResource(BaseResource):
"""
Moment resource
"""
sender = fields.ForeignKey(StudentResource, 'sender', full=True, readonly=True)
comments = fields.ToManyField('myapp.api.CommentResource', 'comments', null=True, full=True)
class Meta:
"""
Metadata for class
"""
queryset = Moment.objects.all()
resource_name = 'moment'
always_return_data = True
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
filtering = {
'zone': ALL,
}
class CommentResource(BaseResource):
"""
Comment resource
"""
moment = fields.ToOneField(MomentResource, 'content_object')
class Meta:
queryset = Comment.objects.all()
resource_name = 'comments'
I'm pretty sure the issue was with returning the Moment object from CommentResource, which was resolved by changing the attribute to content_object.

Related

Django REST: Serializer lookup by UUID

I'm creating this simple shopping API in Django REST.
Internally I'm using IDs for foreign key constraints, while guuids are brought to the outside world.
For the checkout procedure, the user provides a list of article IDs he is willing to purchase. The object in the POST data thus looks as follows:
{
assets: [
{
'product': 'd9d5044d-2284-4d15-aa76-2eee3675035b',
'amount': 4
},
....
]
}
I'm using the following ticket/asset models:
# Ticket
class Ticket(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='tickets', on_delete=models.CASCADE)
# Assets
class Asset(models.Model):
ticket = models.ForeignKey(Ticket, related_name='assets', on_delete=models.CASCADE)
stock_item = models.ForeignKey(Stock, related_name='stock_item', on_delete=models.SET_NULL, null=True)
amount = models.IntegerField(validators=[MinValueValidator(0)])
And the serializers look as follows:
# Asset serializer
class AssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ('stock_item', 'amount')
# Ticket serializer
class TicketSerializer(WritableNestedModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
assets = AssetSerializer(many=True)
class Meta:
model = Ticket
fields = ('uuid', 'owner', 'assets', )
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
When posting an object of the type specified above, the following error is presented:
{"assets":[{"stock_item": ["Invalid type. Expected PK, received string"]}]}
Which I can't seem to solve, how do I instruct the serializer to use the uuid as the lookup value? I solved a similar problem on view-level earlier by using the lookup_field member, but that doesn't seem to solve it. Any suggestions?
Enter code here
If I have understood you correctly, a SlugRelatedField should be able to find the correct related object.
class AssetSerializer(serializers.ModelSerializer):
ticket = serializers.SlugRelatedField(
read_only=True,
slug_field='uuid',
queryset=Ticket.objects.all() # Might be redundant with read_only=True
)
class Meta:
model = Asset
fields = ('ticket', 'stock_item', 'amount')
Elaborating on #BjornW's comment:
class UUIDRelatedField(serializers.SlugRelatedField):
slug_field = 'uuid'
def __init__(self, **kwargs):
super().__init__(slug_field=self.slug_field, **kwargs)
def to_representation(self, obj):
return getattr(obj, self.slug_field).hex

DRF - Nested Routers - Create/Update nested object on POST/PUT/PATCH

I'm currently starting a simple Task App and I'm using Django 2.0.7, DRF 3.8.2 and drf-nested-routes 0.90.2
I have these models :
class Client(TimeStampedModel):
"""
This model describes a client for the railroader. It can be created by the manager in the back office
We have at least one internal Client, which is Seelk, for internal projects
"""
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return "{} : {}".format(self.name, self.description)
class Project(TimeStampedModel):
"""
This model represents a project for a client, which we are gonna track actions on
"""
client = models.ForeignKey(
'railroader.Client', on_delete=models.PROTECT, related_name='projects')
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return "{} for client {}".format(self.name, self.client.name)
So, following the documentation of drf-nested-routers, I set up my serializers like this :
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ("id", "name", "description", "is_active", "projects")
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ("id", "name", "description", "is_active")
And my viewsets like this :
class ClientViewset(viewsets.ModelViewSet):
serializer_class = ClientSerializer
permission_classes = (permissions.IsAuthenticated, AccountPermission)
def get_queryset(self):
queryset = Client.objects.all()
is_active = self.request.query_params.get("is_active")
if is_active:
queryset = queryset.filter(is_active=is_active)
return queryset
class ProjectViewset(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
permission_classes = (permissions.IsAuthenticated, AccountPermission)
def get_queryset(self):
queryset = Project.objects.filter(client=self.kwargs["client_pk"])
is_active = self.request.query_params.get("is_active")
if is_active:
queryset = queryset.filter(is_active=is_active)
return queryset
And finally, my urls like so :
router = routers.SimpleRouter()
router.register(r"clients", viewsets.ClientViewset, base_name="clients")
projects_router = routers.NestedSimpleRouter(router, r"clients", lookup="client")
projects_router.register(r"projects", viewsets.ProjectViewset, base_name="projects")
urlpatterns = [
re_path(r"^", include(router.urls)),
re_path(r"^", include(projects_router.urls))
]
With this setup, I'm able to have the desired nested routes, but I can't have my routes to create a new object if I post on a nested route.
I've seen an issue on the github speaking about it, but as it was 2 years ago, I wonder if anyone knows how to do it.
Thanks in advance.
Found out I just forgot that returning an instance in DRF create method of the serializer would not create the object in base. At the end I have this serializer :
class ProjectSerializer(serializers.ModelSerializer):
def create(self, validated_data):
client = Client.objects.get(pk=self.context["view"].kwargs["client_pk"])
validated_data["client"] = client
return Project.objects.create(**validated_data)
class Meta:
model = Project
fields = ("id", "name", "description", "is_active")

Django REST framework - Getting data instead of links

I have taken a look around, and I didn't find an answer to this question.
Serializers.py
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = ['body', 'user', 'date']
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['id', 'user']
Models.py
class Post(models.Model):
# Children
comments = models.ManyToManyField('Comment', blank=True)
# Content
title = models.TextField(default="-->Title here<--")
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.title)
class Comment(models.Model):
comments = models.ManyToManyField('Comment', blank=True)
# Content
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.body)
Views.py
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def index(request):
return render(request, 'index.html', [])
The index template is a "homepage" that loads the latest posts. Under each post, I'm showing the comments. In the JSON those are links, and I found out how to load them trougth the links (so it's working).
Someone told me that instead of doing it this way, I should make it "load" the comments in the backend, and send them together with the posts (the data, not links). He told me to take a look into: http://www.django-rest-framework.org/api-guide/filtering/#overriding-the-initial-queryset
I can't really figure it out.
How do I get the data, insted of links for the ManyToManyField?
To unfold all the related data one level deep you can use depth param:
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
depth = 1
This will replace post.user and post.comments ids with actual records. depth=2 will also unfold post.comments.user. If you want to selectively pull post.comments only one level deep and not post.user:
class PostSerializer(serializers.HyperlinkedModelSerializer):
comments = CommentSerializer(many=True) #has to be declared higher above
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
If on top you want to have post.comments.user unfolded, need to either put depth=1 into existing comment serializer or create a new serializer with unfolded users just for this view, similar to examples above.
Also make sure you are using prefetch_related on your queryset or the performance will take a serious hit, like:
Post.objects.all().prefetch_related('comments', 'comments__user')

Django ValueError at /imagetarget/ Cannot assign "<SimpleLazyObject: <User: dip7777>>":

Error message
Cannot assign "<SimpleLazyObject: <User: dip7777>>": "ImageTarget.uploaderclient" must be a "UploaderClient" instance.
I'm working on Python 3.4.3 and Django 1.8 and using Django Rest Framework 3.
I'm using the DRF browsable API for testing.
What I'm trying to do is to attach the currently logged in user to the file that he uploads.
Whenever I try to do a POST and upload a file with a logged in user, it throws up the above error.
I have a couple of UploaderClient created and the user dip7777 is one such user tied to a UploaderClient. I have logged in using that user.
What I am able to accomplish with the current code is edit the currently uploaded file ie. upload a new file in place of the current file using the ImageTargetDetail view.
(I had uploaded a couple of files and tied them to UploaderClients using the admin interface)
What I want to do is upload a new file using the ImageTargetList view and not replace a preexisting one.
But the error shows up and I do not understand how to assign the current(logged in) uploaderclient instance to the ImageTarget's uploaderclient
I have two models 1)UploaderClient
class UploaderClient(models.Model):
user = models.OneToOneField(User)
company_name = models.CharField(_('company name'), max_length=100)
class Meta:
verbose_name = _('uploaderclient')
verbose_name_plural = _('uploaderclients')
def __str__(self):
return 'UploaderClient: {}'.format(self.user.username)
and 2) ImageTarget
class ImageTarget(models.Model):
uploaderclient = models.ForeignKey('authenticateclients.UploaderClient', related_name='imagetargets')
targetName = models.CharField(max_length=50, unique=True)
imageWidth = models.IntegerField()
targetFile = models.FileField(upload_to=get_upload_imagetargetfile_name)
in two different apps authenticateclients and clientupload
My serializers are:
class UploaderClientSerializer(serializers.HyperlinkedModelSerializer):
username = serializers.CharField(source='user.username')
email = serializers.CharField(source='user.email', required=False, allow_null=True)
password = serializers.CharField(source='user.password', write_only=True, required=False)
date_joined = serializers.DateTimeField(source='user.date_joined', read_only=True)
last_login = serializers.DateTimeField(source='user.last_login', read_only=True)
company_name = serializers.CharField()
imagetargets = serializers.HyperlinkedRelatedField(many=True, view_name='imagetargetsdetail', read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='uploaderclientsdetail')
class Meta:
model = UploaderClient
fields = ('url', 'email', 'username', 'company_name', 'date_joined', 'last_login', 'password','imagetargets',)
read_only_fields = ('user.date_joined', 'user.last_login',)
and
class ImageTargetSerializer(serializers.HyperlinkedModelSerializer):
uploaderclient = UploaderClientSerializer(read_only=True,required=False)
targetFile = serializers.FileField(label='TargetFile')
url = serializers.HyperlinkedIdentityField(view_name='imagetargetsdetail')
class Meta:
model = ImageTarget
fields = ('url','uploaderclient','targetName','imageWidth','targetFile',)
def get_validation_exclusions(self):
exclusions = super(ImageTargetSerializer, self).get_validation_exclusions()
return exclusions + ['uploaderclient']
My views are
class UploaderClientList(generics.ListAPIView):
queryset = UploaderClient.objects.all()
serializer_class = UploaderClientSerializer
class UploaderClientDetail(generics.RetrieveAPIView):
queryset = UploaderClient.objects.all()
serializer_class = UploaderClientSerializer
and
class ImageTargetList(generics.ListCreateAPIView):
queryset = ImageTarget.objects.all()
serializer_class = ImageTargetSerializer
def perform_create(self, serializer):
instance = serializer.save(uploaderclient=self.request.user)
return super(ImageTargetList, self).perform_create(serializer)
permission_classes = (permissions.IsAuthenticated,IsOwnerOrNothing,)
class ImageTargetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = ImageTarget.objects.all()
serializer_class = ImageTargetSerializer
permission_classes = (permissions.IsAuthenticated,IsOwnerOrNothing,)
#api_view(('GET',))
def api_root(request, format=None):
return Response({
'uploaderclients': reverse('uploaderclients', request=request, format=format),
'imagetargets': reverse('imagetargets', request=request, format=format),
})
In ImageTargetList.perform_create:
instance = serializer.save(uploaderclient=self.request.user)
However uploaderclient should not be a user.

Django TastyPie Patch to a Many-to-Many

I'm trying to use TastyPie Patch to a many-to-many, but I get this error:
"error_message": "Tastypie requires a Python-style path () to lazy load related resources. Only given 'SchemeResource'.",
Why?
The patch I'm making:
/participant/84
POST: {"email":"test#test.com", "schemes":{"id":"12", "schemes"}}
Resource:
class ParticipantResource(ModelResource):
schemes = fields.ToManyField('SchemeResource', attribute='schemes', full=True, null=True)
class Meta:
queryset = Participant.objects.all()
resource_name = 'participant'
allowed_methods = ['post', 'get', 'put', 'patch']
2nd Resource:
class SchemeResource(ModelResource):
user = fields.ToOneField(UserResource, 'user', full=True)
link = fields.ToOneField(SchemeLinkResource, 'link', full=True)
class Meta:
queryset = Scheme.objects.all()
resource_name = 'scheme'
Model:
class Participant(models.Model):
email = models.EmailField(unique=True)
mobile = PhoneNumberField(null=True, blank=True)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
schemes = models.ManyToManyField(Scheme)
You must use brackets such as
[]
around your schemes items (even if singular) when posting to a m2m field.
Request would then look like :
{"email":"test#test.com", "schemes":[{"id":"12", "schemes"}]}
When you want to know what a request should look like, make a GET request on url/of/api/modelresource/schema/
If I recall correctly (and although you wrote "POST" in your request), PATCH request must have
{"objects": [...]}
enclosing the body.
EDIT :
Here's an example of what works for me :
Resources :
class VATCertificateResource(ModelResource):
class Meta:
queryset = VATCertificate.objects.all()
resource_name = 'vatcertificate'
authorization = Authorization()
class InterventionResource(ModelResource):
vatcertificates = fields.ToManyField('core.api.VATCertificateResource', 'vatcertificates',
related_name='intervention', null=True, blank=True, full=True)
Models :
class VATCertificate(Document):
intervention = models.ForeignKey(Intervention, related_name='vatcertificates', blank=True, null=True)
class Intervention(models.Model):
pass
Hope this helps,
Regards,

Categories

Resources