I feel like I am missing something simple. Working on a small personal project I have a MongoDB which I am connecting to via Djongo. I have started making a front end and have a problem with a collection using the generic MongoDB Object ID. The view will return a 404, while using the same query within MongoDB returns the desired document. Other pages are fine, this is the only one that uses the ObjectIDField as it was a hasty late additon
Model:
class Railway_Firmware(models.Model):
_id = models.ObjectIdField(name='_id', verbose_name='_id', db_column='_id', editable=False, max_length=200, primary_key=True)
qa = models.CharField(name='QA', verbose_name='QA', db_column='QA', max_length=200)
security = models.CharField(name='Security', verbose_name='Security', db_column='Security', max_length=200)
objects = models.Manager()
class Meta:
db_table = "Firmware"
verbose_name_plural = "firmware"
def get_absolute_url(self):
return reverse('firmware-detail-view', args=[self._id])
def __str__(self):
return str(self._id)
View:
class Railway_Firmware_DetailView(generic.DetailView):
model = Railway_Firmware
context_object_name = 'firmware'
template_name = 'firmware/firmware_detail.html'
pk_url_kwarg = '_id'
def get_queryset(self):
print({'_id': ObjectId(self.kwargs['_id'])})
return Railway_Firmware.objects.filter(**{'_id': ObjectId(self.kwargs['_id'])})
URL:
urlpatterns = [ path('firmware/<slug:_id>/', views.Railway_Firmware_DetailView.as_view(), name='firmware-detail-view'),
]
Admin works as expected with the same 631b1ce580404ce3f61d4565 within the link for example:
/admin/web_dev/railway_firmware/631929cc0b15c01285ae87e1/change/.
Printing the query to console: {'_id': ObjectId('631b1ce580404ce3f61d4565')} this looks correct so I am unsure where this is going wrong. Any help would be greatly appreciated, thanks!
The name of the URLConf keyword argument that contains the slug is _id in your route.
You have specified that pk_url_kwarg = '_id' in your DetailView.
Therefore, the wrong filter is applied to your queryset seeing as you don't have a pk field in your document.
queryset = queryset.filter(pk=pk)
You need to inform DetailView to use the slug field when building the filter for the queryset via its get_object method.
class Railway_Firmware_DetailView(generic.DetailView):
model = Railway_Firmware
context_object_name = 'firmware'
template_name = 'firmware/firmware_detail.html'
slug_url_kwarg = '_id'
slug_field = '_id'
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
# Ensure that _id value is an ObjectId type for query filters to work
self.kwargs["_id"] = ObjectId(self.kwargs["_id"])
Related
I was remaking a social media site as a revision of Django and the rest framework, I didn't want to use the django default linear id count and didn't like how long the uuid library's ids was, so I used the shortuuid library. I've used them on the posts and the comments just to keep the anonymity of the count of both posts and comments. On the posts side everything works for the CRUD stuff (which should be proof that the issue isn't from the shortuuid library, as far as I know), although with the comments the Create Retrieve works perfectly but the Update Destroy doesn't. so here is the code we are working with:
starting with the models to know what kind of data we are working with (models.py):
from shortuuid.django_fields import ShortUUIDField
... # posts likes etc
class Comment(models.Model):
id = ShortUUIDField(primary_key=True, length=8, max_length=10)
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
body = models.TextField(max_length=350)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
class Meta:
ordering = ['created']
def __str__(self):
return f'on {self.post} by {self.user}'
objects = models.Manager()
serializers.py:
class CommentSerializer(ModelSerializer):
username = SerializerMethodField()
def get_username(self, comment):
return str(comment.user)
class Meta:
model = Comment
fields = ['id', 'user', 'post', 'username', 'body', 'created', 'updated']
read_only_fields = ['id', 'post', 'user', 'username']
now with the routing (urls.py):
from django.urls import path
from .views import *
urlpatterns = [
...
path('<str:pk>/comments/' , Comments),
path('<str:pk>/comments/create/', CreateComment),
path('<str:pk>/comments/<str:cm>/', ModifyComment),
# pk = post ID
# cm = comment ID
]
views.py:
class ModifyComment(generics.RetrieveUpdateDestroyAPIView):
serializer_class = CommentSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
post = Post.objects.get(pk=self.kwargs['pk'])
comment = Comment.objects.get(post=post, pk=self.kwargs['cm'])
return comment
def perform_update(self, serializer):
print(Post.objects.all())
post = Post.objects.get(pk=self.kwargs['pk'])
comment = Comment.objects.filter(pk=self.kwargs['cm'], post=post)
if self.request.user != comment.user:
raise ValidationError('you can\'t edit another user\'s post')
if comment.exists():
serializer.save(user=self.request.user, comment=comment)
else:
raise ValidationError('the comment doesnt exist lol')
def delete(self, request, *args, **kwargs):
comment = Comment.objects.filter(user=self.request.user, pk=self.kwargs['cm'])
if comment.exists():
return self.destroy(request, *args, **kwargs)
else:
raise ValidationError("you can\'t delete another user\'s post")
ModifyComment = ModifyComment.as_view()
and the response to going to the url '<str:pk>/comments/<str:cm>/' comment of some post we get this:
side note, the perform_update function doesn't seem to be called ever, even putting a print statement at the beginning of the function doesn't get printed so the issue may have to do with the get_queryset even though I've tried using the normal queryset=Comment.object.all() and making the get_queryset function return the comment with the correct params but I couldn't make it work
For individual objects you need to overwrite the get_object method.
You are performing the request GET /str:pk/comments/str:cm/, this calls the retrieve method on the view, which in turn calls get_object. The default behaviour is trying to find a Comment object with id equal to pk since it's the first argument, since you need to filter through a different model you need to overwrite it.
classy drf is a good website for seing how the internals of the clases work.
News to Django and Django Rest.
I'm trying to create 2 types of view, based on the role of the user.
While creating a new demand, I have one field that I would like to not show to one role(leaving it null in DB) but to show it to the other role.
The thing is that I don't see how I could do that.
If anyone could guide me in the right direction
Here is what I have :
models.py
class demand(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=60)
description = models.CharField(max_length=400)
assigned = models.CharField(max_length=60)
serializer.py
class demandSerializer(serializers.ModelSerializer):
class Meta:
model = models.demand
fields = ('title','description','assigned')
views.py
class demandCreateAPIView(CreateAPIView):
queryset=models.demand.objects.all()
serializer_class=serializers.demandSerializer
You can just change the request data before saving it.
class DemandCreateAPIView(CreateAPIView):
queryset = Demand.objects.all()
serializer_class = serializers.DemandSerializer
def create(self, request, *args, **kwargs):
... #do whatever check you want
request.data.pop('assigned', None) # I've choose this parameter as an example
return super(DemandCreateAPIView, self).create(request, *args, **kwargs)
First, I would like to present how my managers, models, serializers and views look like upfront.
class PublishedManager(models.Manager):
"""
Only published articles. `due_date` is past.
"""
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__lt=now)
class UnpublishedManager(models.Manager):
"""
Only unpublished articles. `due_date` is future.
"""
def announced(self):
return self.get_queryset().filter(announced=True)
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__gt=now)
class Article(models.Model):
content = models.TextField()
due_date = models.DateTimeField()
announced = models.BooleanField()
# managers
objects = models.Manager() # standard manager
published = PublishedManager()
unpublished = UnpublishedManager()
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
In this code, ArticleRUDView naturally responds with all Article because of Article.objects.all(), yet this is not what I want to do. What I want to do is:
If the user is authenticated, then Article.objects.all().
If the user is anonymous,
If the entry is published (which means its due_date is less than now), then serialize all fields.
If the entry is not published (which means its due_date is greater than now), then still serialize, but content should be null in JSON.
Or, in short, how do I alter the serializer's data in a view?
Troubleshooting
This section might get updated in time. I will elaborate on what I find.
Overriding get_serializer Method from GenericAPIView
So I've found out I can get an instance of ArticleSerializer. So I did below:
def get_serializer(self, *args, **kwargs):
serializer = super().get_serializer()
if self.request.user.is_authenticated:
return serializer
obj = self.get_object() # get_object, hence the name, gets the object
due_date = obj.due_date
now = timezone.now()
if due_date > now:
serializer.data["content"] = None
return serializer
However, my tests didn't go well at all. This, oddly, returns an empty string on content field in JSON. I've tried different things but got that empty string. I do not have any single clue about what to do from here.
Environment
Python 3.7.4
Django 2.2.7
Django Rest Framework 3.10.3
I think you want to use get_serializer_class as opposed to get_serializer. You can allow the serializer class to choose what to stick in content instead of all the mucking around with managers, since you want to serialize all objects anyway. Something like this should work:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class AnonymousArticleSerializer(ArticleSerializer):
content = serializers.SerializerMethodField()
#staticmethod
def get_content(obj):
if obj.due_date > timezone.now():
return None
return obj.content
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
def get_serializer_class(self):
if self.serializer_class:
return self.serializer_class
if self.request.user.is_authenticated:
self.serializer_class = ArticleSerializer
else:
self.serializer_class = AnonymousArticleSerializer
return self.serializer_class
One thing I don't like about this solution is that if you have a more complicated serializer field you're overwriting, you'd have to put the logic somewhere, but in this case (context being a text field) it's pretty simple.
So I’m having a bit of trouble with something I was trying to do.
Basically, I have these models:
class Package(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255, null=False, blank=False)
contact = models.BooleanField(default=False)
downloaded = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
class Item(models.Model):
[…. all the atributes of the item model….]
class PackageItems(models.Model):
package = models.ForeignKey(Package, on_delete=models.CASCADE)
item = models.ForeignKey(Item, on_delete=models.CASCADE)
and now I am trying to make an endpoint that allows my users to add “package” and add a pre-existing item in an item model to that newly created package. One package can of course have many items.
SO I wrote a Package serializer then added a SerializerMethodField that allows one to do a get operation on any item that a given package contains. The method makes a call to different serializer.
Here’s the code for both the serializers:
class PackageItemsSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PackageItems
fields = ('package', 'item')
class PackagesSerializer(serializers.ModelSerializer):
"""
Creation only requires a title
"""
package_items = serializers.SerializerMethodField(read_only=True)
#swagger_serializer_method(serializer_or_field=packageItemsSerializer)
def get_package_items(self, obj):
packageItems = PackageItems.objects.all().filter(package=obj.id)
return PackageItemsSerializer(packageItems, many=True, context={'request': self.context['request']}).data
def create(self, validated_data):
package = super(packagesSerializer, self).create(validated_data)
return package
class Meta:
model = packages
fields = ('id', 'user', 'title', 'contact', 'downloaded', 'created', 'package_items')
read_only_fields = [ 'user', 'contact', 'downloaded', 'package_items’]
Now for my views, I have decided to do this:
class PackagesViewSet(viewsets.ModelViewSet):
queryset = Packages.objects.all()
serializer_class = PackagesSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Packages.objects.none()
return Packages.objects.filter(user=self.request.user).order_by('id')
def perform_create(self, serializer):
# sending default values for user, contact and downloaded
serializer.save(user=self.request.user, contact=False, downloaded=False)
def partial_update(self, request, *args, **kwargs):
response_with_updated_instance = super(PackagesViewSet, self).partial_update(request, *args, **kwargs)
return response_with_updated_instance
#action(methods=['post', 'delete'], detail=True, serializer_class=PackageItemsSerializer)
def item(self, request, pk=None):
package = self.get_object()
serializer = self.get_serializer_class()(data=request.data)
if serializer.is_valid():
serializer.save(package=package)
return Response(serializer.data)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
if request.method == 'delete':
serializer.delete(package=package)
This is what works with this:
I get these routes:
[get/post] for /api/packages/
[all methods] for /api/packages/{id}
[post/delete] for /api/packages/{id}/item
I get the right routes, but for one, Swagger gives me the wrong models for post:
{
"title": "string",
"package_items": {
"items": 0
}
}
Not sure why it's expecting this read_only field to be entered (which is a serializermethodfield as well) when I do a post to /api/packages.
Similarly, these fields show up for PUT under /api/packages/{id}.
For the nested "item" route, the POST works well, but the DELETE doesn't. The POST route allows me to enter an item ID and adds it to the given package ID under /api/packages/{id}/item route.
But delete doesn't allow me to enter an item ID to delete.
So I'm thinking my approach to the delete method for the nested ITEM is completely wrong.
I’m new to DRF/django, trying to validate if I’m going in the right direction with this.
How do I get drf-yasg to get me the right model for the HTTP verbs like POST for example?
What's the right way to delete the nested Item?
Am I approaching this problem right? Is there another way to do this more efficiently?
If someone can help me out to answer those questions, it would be really appreciated :)
Thanks in advance folks.
Instead of nesting serializer in serializer.SerializerMethodField directly, nest it like:
package_item = PackageItemsSerializer(source="package_set", many=True, read_only=True)
try this
I have this code:
#api model
class VideoResource(ModelResource):
class Meta:
queryset = Video.objects.all()
include_resource_uri = False
resource_name = 'video'
authorization = DjangoAuthorization()
class QuestionResource(ModelResource):
user = fields.ToOneField(UserResource,'user',full=True)
video = fields.ForeignKey(VideoResource,'video',full=True)
class Meta:
queryset = Question.objects.all()
resource_name = 'question'
include_resource_uri = False
authorization = DjangoAuthorization()
def obj_create(self, bundle, request=None, **kwargs):
import json
temp = json.loads(request.body, object_hook=_decode_dict)
video = Video.objects.get(pk=temp['video'])
return super(QuestionResource, self).obj_create(bundle, request, user=request.user, video=video)
#model
class Question(models.Model):
text = models.CharField('Question',max_length=120)
created = models.DateTimeField(auto_now_add=True)
enabled = models.BooleanField(default=True)
flag = models.BooleanField(default=False)
allow_comments = models.BooleanField(default=True)
thumbnail_url = models.CharField(default='video.jpg',blank=True, null=True,max_length=200)
user = models.ForeignKey(User)
video = models.ForeignKey(Video)
def __unicode__(self):
return self.text;
class Video(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now_add=True)
url = models.URLField(default="")
user = models.ForeignKey(User)
def __unicode__(self):
return str(self.pk) + ' > ' + self.status
The problem is that I am getting this error when sending this object:
{"video":21,"text":"sadasds"}
The 'video' field has was given data that was not a URI, not a
dictionary-alike and does not have a 'pk' attribute: 21.
If I comment this line:
video = fields.ForeignKey(VideoResource,'video',full=True)
Everything works fine, but then I cannot get this information (video)
when asking to /api/v1/questions/
My question is:
Should I create to resources, one to post and another to retrieve
information <- this seems not a really good solution.
or
How can I create Nested Resources ? I tried to follow the example on
the web http://django-tastypie.readthedocs.org/en/latest/cookbook.html#nested-resources
but as you can see for some reason is not working.
maybe your eyes can help me find the error :)
Thanks!
The 'video' field has was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: 21.
So, this means that the integer 21 does't meet the requirements for that field, it also give a vague hint of what will meet the requirements.
first, you can send in the URI for the record, this is probably the most correct way as URIs are really unique while pk's are not.
{"video":"/api/v1/video/21","text":"sadasds"}
or, you can send in an dictionary-alike object with the pk field set.
{"video":{'pk':21},"text":"sadasds"}
The reason it works when you comment out the ForeignKey field is because then tastypie treats it as a IntegerField, which can be referenced by a plain integer.
This had me stunted for a while to, hope it helps!