Django REST Framework customize update method - python

My application has the following structure:
models.py
class EventHost(models.Model):
hostid = models.ForeignKey(Host, on_delete=models.PROTECT)
eventid = models.ForeignKey(Event, on_delete=models.CASCADE)
someparam = models.CharField(max_length=100, blank=True, null=True)
...
class Meta:
unique_together = ("hostid", "event")
class Host(models.Model):
hostid = models.IntegerField(primary_key=True)
hostname = models.CharField(max_length=100)
...
class Event(models.Model):
eventid = models.AutoField(primary_key=True)
eventname = models.CharField(max_length=100)
...
hosts = models.ManyToManyField(Host, through='EventHost')
views.py
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.order_by('-date')
serializer_class = EventSerializer
class HostViewSet(viewsets.ModelViewSet):
queryset = Host.objects.order_by('-hostid')
serializer_class = HostSerializer
class EventHostViewSet(viewsets.ModelViewSet):
queryset = EventHost.objects.all()
serializer_class = EventHostSerializer
Currently to update EventHost table I'm doing http put providing id (which is primary key) in my url .
I'd like to be able to update EventHost providing hostname and eventname (which will be passed in url) instead of id.
Using SQL it would look like this:
update eventhost set someparam='somevalue' from eventhost as a, event as b, host as c where b.id = a.eventid and c.hostid = a.hostid and b.eventname='myevent' and c.hostname='myhost';
From documentation I understood that I would need to modify the default update method for the viewset or/and modify queryset. Any ideas how should it be achieved?

You can override get_object method and use your parameters provided from url like that:
class EventHostViewSet(viewsets.ModelViewSet):
queryset = EventHost.objects.all()
serializer_class = EventHostSerializer
def get_object(self):
return EventHost.objects.get(hostid=self.kwargs['hostid'],eventid=self.kwargs['eventid'])
In this way, you must manage if there is no record with this query scenario, as custom

Assuming that you have properly constructed URL.
Edited EventHostViewSet.get_object method:
class EventHostViewSet(viewsets.ModelViewSet):
...
lookup_field = 'eventname'
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
filter_kwargs = {'hostid__hostname': self.kwargs.get('hostname'),
'eventid__eventname': self.kwargs.get('eventname')}
obj = get_object_or_404(queryset, **filter_kwargs)
self.check_object_permissions(self.request, obj)
return obj
EventHostViewSet registration:
router.register(rf'event-hosts/(?P<hostname>[.]+)', EventViewSet)
Some comments about your problem:
You will have problem when in your system will exist two EventHost with same hostid__hostname and eventid__event because queryset get method should only return ONE record
in DRF PUT method needs all fields to be provided in request data, if you want to update selected fields you should use PATCH method or override update viewset method (set partial to True)
EDITED AGAIN:
This is really bad design, you should not do this like that (somehow you should use id / maybe #action decorator to construct specific url for updating like that).

Related

Django Filter name "todo__test" is not defined

I'm trying to filter my Todos by the test_id pulled from the URL. It pulls the id from the URL but it cant seem to filter with todo__test. I have also tried "test", "Todo.test.test_id", "Todo.test". I guess I'm confused about what variable I need to filter and the Django restframework documentation doesn't explicitly show what variable to use. Their example uses "purchaser__username" which I don't understand where it comes from. https://www.django-rest-framework.org/api-guide/filtering/
class TodoList(generics.ListAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
def get_queryset(self):
test_id = self.kwargs['test_id']
return Todo.objects.filter(todo__test == test_id)
class Todo(models.Model):
test = models.ForeignKey(Test, on_delete=models.CASCADE)
content = models.TextField(blank=True)
order = models.IntegerField()
def __str__(self):
return self.content + ' - ' + self.test.test_name
class Meta:
ordering = ['test_id']
i guess it will be like this. you passed incorrect foreign key field name.
Todo.objects.filter(test_id='whatever_value')

Django-filter error: 'Meta.fields' must not contain non-model field names

I am working with Django REST framework and django-filters and and I'd like to use the reverse relationship annotation_set as one of filters for a GET API that uses the model Detection.
The models are the following:
class Detection(models.Model):
image = models.ImageField(upload_to="detections/images")
def local_image_path(self):
return os.path.join('images' f"{self.id}.jpg")
class Annotation(models.Model):
detection = models.ForeignKey(Detection, on_delete=models.CASCADE)
attribute = models.CharField(max_length=255)
The serializer is:
class DetectionSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
local_image_path = serializers.CharField()
class Meta:
model = Detection
fields = '__all__'
And the viewset is:
class DetectionTrainingViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filterset_fields = ('annotation_set__id', )
#action(methods=['GET'], detail=False)
def list_ids(self, request):
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
return Response(filtered_queryset.values_list('id', flat=True))
When I make a call to the endpoint, I get the error:
'Meta.fields' must not contain non-model field names: annotation_set__id
Shouldn't the field exist?
Note: I tried to add other fields to the Annotation model and then use annotation_set__newfield but I still have the error. I can confirm that the newfield exists because it is correctly serialized and return by the API when I comment out the line that set the filterset_fields.
Looking at the django filter docs, you may have missed a reference to DjangoFilterBackend, eg
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('annotation_set__id', )
(see: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html)
Apparently I had to explicitly state the name of the reverse relationship:
class Annotation(models.Model):
detection = models.ForeignKey(Detection, on_delete=models.CASCADE, related_name='annotation_set')
attribute = models.CharField(max_length=255)
If anybody knows why, I'd love to know it! Thanks!
Try this, I had similar problem it worked for me.
In your view file change the filterset_fields to the following.
class DetectionTrainingViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('annotation__id', )

How to create 2 objects from separate models with a single serializer and also retrieve them from the database with a single serializer in Django RF?

I have 3 models: Maker, Item and MakerItem that creates the relation between the items and their makers:
class Maker(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class Item(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class MakerItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
item_id = models.ForeignKey(Item, on_delete=models.CASCADE)
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE)
the items can have a random amount of makers.
I want to create both the Item and the MakerItem objects at the same time with a single set of data,
for example if a Maker with id = "abcd" already exists, and I go to /item and send a POST request with the following data:
{
"name": "item1",
"makers": [
{
"maker_id": "abcd"
}
]
}
I want the serializer to create the Item object and the MakerItem object.
I have achieved this, with the following setup:
views.py
class ItemListCreate(ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
class Meta:
model = Item
fields = ['id', 'name', 'makers']
def create(self, validated_data):
maker_item_data = validated_data.pop('makers')
item_instance = Item.objects.create(**validated_data)
for each in maker_item_data:
MakerItem.objects.create(
item_id=check_instance,
maker_id=each['maker_id']
)
return item_instance
but when Django tries to return the created object, it always gives me the error:
AttributeError at /item/
Got AttributeError when attempting to get a value for field `makers` on serializer `ItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Item` instance.
Original exception text was: 'Item' object has no attribute 'makers'.
What am I doing wrong?
Thanks
EDIT: To clarify, the objects get created and populate the database correctly, but when the browsable API that DRF provides tries to display the created object, it gives me the error above.
Change:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
To:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True, source="makeritem_set")
Hope this works!
For clarity, you're attempting to serialise the reverse relationship between MakerItem and Item for this serialiser.
This means that the attribute on your object is automatically set by Django as fieldname_set but you can override this behaviour by setting the related_name kwarg on the field and then makemigrations and migrate it.
In your case you would need to do:
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE, related_name="maker_items")
And then update the field in the Meta to match the new field name, this way you don't have to manually specify source. Because actually the attribute "makers" is misleading, due to the fact its actually the MakerItem, not the Maker itself.
See https://docs.djangoproject.com/en/3.2/ref/models/relations/ for further details about this behaviour.

How to serialize an array of objects in Django

I am working with Django and REST Framework and I am trying to create a get function for one of my Views and running into an error. The basic idea is that I am creating a market which can have multiple shops. For each shop there can be many products. So, I am trying to query all those products which exist in one shop. Once I get all those products I want to send it to my serializer which will finally return it as a JSON object. I have been able to make it work for one product but it does not work for an array of products.
My Product model looks like this:
'''Product model to store the details of all the products'''
class Product(models.Model):
# Define the fields of the product model
name = models.CharField(max_length=100)
price = models.IntegerField(default=0)
quantity = models.IntegerField(default=0)
description = models.CharField(max_length=200, default='', null=True, blank=True)
image = models.ImageField(upload_to='uploads/images/products')
category = models.ForeignKey(Category, on_delete=models.CASCADE, default=1) # Foriegn key with Category Model
store = models.ForeignKey(Store, on_delete=models.CASCADE, default=1)
''' Filter functions for the product model '''
# Create a static method to retrieve all products from the database
#staticmethod
def get_all_products():
# Return all products
return Product.objects.all()
# Filter the data by store ID:
#staticmethod
def get_all_products_by_store(store_id):
# Check if store ID was passed
if store_id:
return Product.objects.filter(store=store_id)
The product serializer that I built is as follows:-
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
and the view that I created is below
class StoreView(generics.ListAPIView):
"""Store view which returns the store data as a Json file.
"""
# Define class variables
serializer_class = StoreSerializer
# Manage a get request
def get(self, request):
# Get storeid for filtering from the page
store_id = request.GET.get('id')
if store_id:
queryset = Product.get_all_products_by_store(store_id)
# queryset = Product.get_all_products_by_store(store_id)[0]
else:
queryset = Product.get_all_products()
# queryset = Product.get_all_products()[0]
print("QUERYSET", queryset)
return Response(ProductSerializer(queryset).data)
The above view gives me the following error
AttributeError at /market
Got AttributeError when attempting to get a value for field `name` on serializer `ProductSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `QuerySet` instance.
Original exception text was: 'QuerySet' object has no attribute 'name'.
If instead queryset = Product.get_all_products_by_store(store_id), I use the line below it where I am only selecting the first option then I get the correct JSON response but if there multiple products then I am not able to serialize. How do I make it work?
If you want to serialize more than one record, either use ListSerializer instead, or pass many=True the the constructor of ModelSerializer:
return Response(ProductSerializer(queryset, many=True).data)
I found the answer thanks to #yedpodtrzitko for giving the direction.
I had to make two changes.
Define queryset outside the function
Pass many=True the the constructor of ModelSerializer
class StoreView(generics.ListAPIView):
"""Store view which returns the store data as a Json file.
"""
# Define class variables
queryset = []
serializer_class = StoreSerializer
# Manage a get request
def get(self, request):
# Get storeid for filtering from the page
store_id = request.GET.get('id')
if store_id:
queryset = Product.get_all_products_by_store(store_id)
else:
queryset = Product.get_all_products()
print("QUERYSET", queryset)
return Response(ProductSerializer(queryset, many = True).data)

DRF-Filtering with multiple query params and foreign keys

I'm new to Django and I'm trying to create a route that can be called to retrieve an array of vehicles from my database, but I want the user to be able to provide multiple query params in the url (something like: http://127.0.0.1:8000/vehicles/?year=2020&make=Toyota). The problem that I have come across is that my vehicle model includes references to foreign keys for the make and the v_model (so named to avoid conflict with the Django "model"). I have a solution that doesn't seem very graceful. The fact that I have three nested conditional statements for each search field makes me suspicious. I tried using "filters.SearchFilter" but I was only able to provide a single value on which to base the search. So a request like this: http://127.0.0.1:8000/vehicles/?search=2020&search=Toyota would only search for vehicles with a make of "Toyota", ignoring the "year" parameter.
Is there some other way to do this that is cleaner or more "Django-approved"?
Here is my code:
models.py:
class Make(models.Model):
name = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.name
class VModel(models.Model):
name = models.CharField(max_length=200, unique=True)
make = models.ForeignKey(Make, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Vehicle(models.Model):
make = models.ForeignKey(Make, on_delete=models.CASCADE)
v_model = models.ForeignKey(VModel, on_delete=models.CASCADE)
year = models.CharField(max_length=5)
def __str__(self):
return self.year + " " + self.v_model.name
views.py:
Here is my attempt with filters.SearchFilter:
queryset = Vehicle.objects.all()
serializer_class = VehicleSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['year', 'v_model__name','make__name']
And here is my "working" solution that seems hacky:
(NOTE: I am using name__icontains so that if a user enters "Toyot" it will still get all cars with a make of Toyota).
class VehicleListViewSet(viewsets.ModelViewSet):
serializer_class = VehicleSerializer
queryset = Vehicle.objects.all()
pagination_class = CustomPagination
def get_queryset(self):
qs = super().get_queryset()
selected_make = self.request.query_params.get('make', None)
if selected_make:
try:
found_make = Make.objects.get(name__icontains=selected_make)
except:
return []
if found_make:
if found_make.id:
qs = qs.filter(make=found_make.id)
selected_v_model = self.request.query_params.get('v_model', None)
if selected_v_model:
try:
found_v_model = VModel.objects.get(name__icontains=selected_v_model)
except:
return []
if found_v_model:
if found_v_model.id:
qs = qs.filter(v_model=found_v_model.id)
selected_year = self.request.query_params.get('year', None)
if selected_year:
qs = qs.filter(year=selected_year)
return qs
You shouldn't be using filters.SearchFilter. Instead, use a filterset_fields attribute in your ViewSet, like the example from this section of the documentation.
Your viewset would be like this:
class VehicleListViewSet(viewsets.ModelViewSet):
serializer_class = VehicleSerializer
queryset = Vehicle.objects.all()
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend]
filterset_fields = ['year', 'v_model__name','make__name']
(Note there's no get_queryset override) You'll be able to query your API like this:
http://127.0.0.1:8000/vehicles/?year=2020&make__name=Toyota&v_model__name=Corolla

Categories