I'm following the tutorial laid out here to create generic class based views for my API - however, I have run into a small problem. I would like to update the model behind the view partially. I used to be able to do this by using the partial property when I created the serializer. However, it seems that once I start using generic class based views I lose the ability to set whether or not I can allow partial updates to the model. How can I override the partial property of a ModelSerializer? My code is quite simple:
class DejavuUserDetail(generics.RetrieveUpdateAPIView):
'''
Get a user or update a user
'''
lookup_field = "email"
queryset = DejavuUser.objects.all()
serializer_class = UserSerializer
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = DejavuUser
partial = True
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
#set the required fields and return the instance
I'm trying to access the API via PUT
For partial updates use PATCH.
Also note that partial isn't an option on the serializer metaclass, but instead set on instantiating the serializer.
Related
I have the following custom model manager in Django that is meant to count the number of related comments and add them to the objects query set:
class PublicationManager(models.Manager):
def with_counts(self):
return self.annotate(
count_comments=Coalesce(models.Count('comment'), 0)
)
Adding this manager to the model does not automatically add the extra field in DRF. In my API view, I found a way to retrieve the count_comments field by overriding the get function such as:
class PublicationDetails(generics.RetrieveUpdateAPIView):
queryset = Publication.objects.with_counts()
...
def get(self, request, pk):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset.get(id=pk))
data = {**serializer.data}
data['count_comments'] = queryset.get(id=pk).count_comments
return Response(data)
This works for a single instance, but when I try to apply this to a paginated list view using pagination_class, overriding the get method seems to remove pagination functionality (i.e. I get a list of results instead of the usual page object with previous, next, etc.). This leads me to believe I'm doing something wrong: should I be adding the custom manager's extra field to the serializer instead? I'm not sure how to proceed given that I'm using a model serializer. Should I be using a basic serializer?
Update
As it turns out, I was using the model manager all wrong. I didn't understand the idea of table-level functionality when what I really wanted was row-level functionality to count the number of comments related to a single instance. I am now using a custom get_paginated_response method with Comment.objects.filter(publication=publication).count().
Original answer
I ended up solving this problem by creating a custom pagination class and overriding the get_paginated_response method.
class PaginationPublication(pagination.PageNumberPagination):
def get_paginated_response(self, data):
for item in data:
publication = Publication.objects.with_counts().get(id=item['id'])
item['count_comments'] = publication.count_comments
return super().get_paginated_response(data)
Not sure it's the most efficient solution, but it works!
The Problem
I am using a model class Event that contains an optional ManyToManyField to another model class, User (different events can have different users), with a factory class EventFactory (using the Factory Boy library) with a serializer EventSerializer. I believe I have followed the docs for factory-making and serializing, but am receiving the error:
ValueError: "< Event: Test Event >" needs to have a value for field "id"
before this many-to-many relationship can be used.
I know that both model instances must be created in a ManyToMany before linking them, but I do not see where the adding is even happening!
The Question
Can someone clarify how to properly use a ManyToManyField using models, factory boy, and serializers in a way I am not already doing?
The Set-Up
Here is my code:
models.py
#python_2_unicode_compatible
class Event(CommonInfoModel):
users = models.ManyToManyField(User, blank=True, related_name='events')
# other basic fields...
factories.py
class EventFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Event
#factory.post_generation
def users(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of users were passed in, use them
# NOTE: This does not seem to be the problem. Setting a breakpoint
# here, this part never even fires
for users in extracted:
self.users.add(users)
serializers.py
class EventSerializer(BaseModelSerializer):
serialization_title = "Event"
# UserSerializer is a very basic serializer, with no nested
# serializers
users = UserSerializer(required=False, many=True)
class Meta:
model = Event
exclude = ('id',)
test.py
class EventTest(APITestCase):
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(email='test#gmail.com',
password='password')
def test_post_create_event(self):
factory = factories.EventFactory.build()
serializer = serializers.EventSerializer(factory)
# IMPORTANT: Calling 'serializer.data' is the exact place the error occurs!
# This error does not occur when I remove the ManyToManyField
res = self.post_api_call('/event/', serializer.data)
Version Info
Django 1.11
Python 2.7.10
Thank you for any help you can give!
Regarding the error:
It seems like the missing id is due to the use of .build() instead of .create() (or just EventFactory()). The former does not save the model, and therefore it does not get an id value, whereas the latter does (refer to factory docs and model docs).
I suspect the serializer still expects the object to have an id, even though the many-to-many relationship is optional, because it cannot enforce a potential relationship without an id.
However, there might be a simpler solution to the actual task. The above method is a way of generating the POST data passed to post_api_call(). If this data was instead created manually, then both the factory and serializer become unnecessary. The explicit data method might even be better from a test-perspective, because you can now see the exact data which has to produce an expected outcome. Whereas with the factory and serializer method it is much more implicit in what is actually used in the test.
I have Node and User models which both belong to an Organisation. I want to ensure that a User will only ever see Node instances belonging to their Organisation.
For this I want to override the Node objects Manager with one that returns a query_set of User owned filtered results.
Based on https://docs.djangoproject.com/en/2.1/topics/db/managers/#modifying-a-manager-s-initial-queryset
the relevant models.py code I have is below:
class Organisation(models.Model):
users = models.ManyToManyField(User, related_name='organisation')
...
class UserNodeManager(models.Manager):
def get_queryset(self, request):
return super().get_queryset().filter(organisation=self.request.user.organisation.first())
class Node(models.Model):
organisation = models.ForeignKey(
Organisation, related_name='nodes', on_delete=models.CASCADE)
uuid = models.UUIDField(primary_key=True, verbose_name="UUID")
...
objects = UserNodeManager
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
EDIT
I can add custom query_set to individual views and this does work as below:
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.filter(organisation__users__id=self.request.user.pk)
However, my intention is to be DRY and override a 'master' query_set method at a single point so that any view (e.g. form dropdown list, API endpoint) will perform the user restricted query without additional code.
For example, I am using django's generic list views have a form for adding a Scan object which requires a user to select a Node the Scan belongs to. The form currently shows Nodes from other Organisations, which is against the permissions logic I need.
Unfortunately, the overridden Node.objects property does not seem to have any effect and any User can see all Nodes. Am I taking the right approach?
I think the problem is here:
objects = UserNodeManager
You need to initiate UserNodeManager instance like this:
objects = UserNodeManager()
Also, it should throw error when you calling YourModel.objects.all() method(which is called from get_queryset method in view), because when it calls get_queryset() method, it does not pass request. So I think it would be a better approach:
class UserNodeManager(models.Manager):
def all(self, request=None):
qs = super(UserNodeManager, self).all()
if request:
return qs.filter(...)
return qs
Or you can create a new manager method like this(optional):
class UserNodeManager(models.Manager):
def user_specific_nodes(self, request):
return self.get_queryset().filter(...)
Also update in the view:
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.all(self.request) # where you can obviously use filter(...) or Model.objects.user_specific_nodes(self.request)
Update
from comments
Thing is that, you need to pass request with filter() or all(). In Generic views, the get_queryset method does not pass that information to all(). So you need to pass that either way. There is another way, to use a middleware like this django-crequest. You can use it like this:
from crequest.middleware import CrequestMiddleware
class UserNodeManager(models.Manager):
def all(self):
qs = super(UserNodeManager, self).all()
request = CrequestMiddleware.get_request()
return qs.filter(...)
The best way of achieving this is by using groups and custom permissions. You might add a group for every organization and set the correct permissions for those groups over your Nodes.
Take a look to this article, it might help: User Groups with Custom Permissions in Django
#ruddra thanks again for your guidance.
While your middleware example did not have effect for me (as user could still see others' objects), I was able to use that with the django documentation to finally implement the Manager similar to:
class UserDeviceManager(models.Manager):
def get_queryset(self):
request = CrequestMiddleware.get_request()
return super().get_queryset().filter(organisation=request.user.organisation)
Consider the following code:
views.py
class BHA_UpdateView(UpdateView):
model = BHA_overall
pk_url_kwarg = 'pk_alt'
form_class = BHA_overall_Form
To my understanding, pk_url_kwarg = 'pk_alt' will query and return instances of model = BHA_overall.
Is there any way that I can force pk_url_kwarg to query
& return other model instances defined in models.py (like model = other_model), while having my get_object() method to return objects in model = BHA_overall? What CBV should I use (I think UpdateView is not a good choice in this case)?
++ I'm trying to make a page that allows users to manage information about the product they use. So, ultimately I will implement forms, and the user input needs to be saved in DB
++ I need pk_url_kwarg = 'pk_alt' to query other models and generate url. But I still need get_object() method to return objects in model = BHA_overall to generate form fields on the user side.
From my understanding you need a django form generated from BHA_overall, but the data should be saved to AnotherModel right?
I will propose 2 solutions to this problem, Choose what best fits you.
Multiple views:
Have multiple views for the task, What I mean is create a view which creates the form for the frontend using BHA_overall, you can create both Create and Update view this way and update view's initial could be overwritten so form will have expected value when editing. And now post the data to another view which handles the post data. This view can have your AnotherModel doing its thing.
Using Django Form:
If you dont like having multiple views, You can keep things simple by creating a form yourself. Create a DjangoForm with the same fields you want to show to the user and use it in to create your own views, Now you wont need BHA_overall and use your AnotherModel to save datal.
I have a model named Domain which looks like this:
class Domain(models.Model):
"""
Model for storing the company domains
"""
user = models.ForeignKey(
User
)
host = models.CharField(
null=False, verbose_name="Host", max_length=128, unique=True
)
I'd like to use Django's generic views for doing CRUD operations on this. There is one field in this model that needs user input but the foreign key field doesn't need any user input. How can I exclude that field from the form that my generic view generates but assign it the value of the current authenticated user.
Thanks.
Have a look at Russel's answer to a similar question on the django-users group earlier this week.
Quoting the answer*:
Forms and Views solve different problems.
The View is solving the problem of "how do I handle this request and
convert it into a response?". The Form is solving the problem of "How
do I convert the POST data in this request into a model object (or a
change to a model object)?".
Very roughly, a view is doing the following:
View gets a request
View works out whether this is a GET or a POST
If its a POST, View asks the Form to turn the Post into a model change
Form returns success or failure
View responds to the success or failure of the Form.
View returns a response.
The functionality of the Form is a complete subset of the
functionality of the View -- and for this reason, it's a completely
interchangable internal component.
Now, in simple situations, it's possible for a View to guess all the
defaults for the form -- all it needs to know is that you're dealing
with a Foo model, and it can construct a default Foo ModelForm.
However, if you have more sophisticated form requirements, you're
going to need a customized Form.
We could have implemented this by exposing all the options of
ModelForm on the View class; but in order to keep everything clean, we
kept the ModelForm isolated, and provided the View with a way to
specify which Form class it's going to use.
So - to cover your use case of excluding fields, you define a
ModelForm that excludes the fields, then let the CreateView know the
form you want to use:
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
exclude = ('user', 'name', 'content_inlined')
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
I'm guessing when you say "fix a values for a field", you mean setting
the values of user, name and content_inlined before you save the new
Campaign instance; to do this, you need to inject some extra code into
the form processing logic of the form:
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
def form_valid(self, form):
form.instance.user = ... (something meaningful.. e.g., self.request.user)
return super(CreateCampaignView, self).form_valid(form)
This overrides the default behavior when the form is valid, and sets
the extra values. The super() implementation of form_valid() will then
save the instance.
For the record, this could also be done by overriding the save()
method on the ModelForm -- however, if you do that, you lose the
request object, which you will need if you're trying to set the
instance values to something that is request-sensitive.
*the original answer set self.object.user instead of form.instance.user. This gives an AttributeError so I have changed it above.