Update many objects in one query DRF - python

I need to bulk update ("is_read" = True) Message instanses by given list of ids in one request with this code:
{"ids": [11, 4, 7]}
Model:
class Message(models.Model):
text = models.TextField(max_length=500, verbose_name=_("Text"))
sender = models.ForeignKey(
to=User,
on_delete=models.CASCADE,
related_name="sender_message",
verbose_name=_("User"),
)
thread = models.ForeignKey(
to="Thread",
on_delete=models.CASCADE,
related_name="thread_message",
verbose_name=_("Thread"),
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
is_read = models.BooleanField(default=False, verbose_name=_("Is read"))
I have this serializer:
class MessageIsReadSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ("id", "text", "sender", "is_read")
And method in views.py:
class MessageIsRead(APIView):
permission_classes = (AllowAny,)
queryset = Message.objects.all()
def put(self, request, *args, **kwargs):
id_list = request.data['ids']
instances = []
for item in id_list:
obj = self.queryset.filter(id=item)
obj.is_read = True
instances.append(obj)
serializer = MessageIsReadSerializer(instances, many=True)
return Response(serializer.data)
urls.py
urlpatterns = [
path("messages-read/", MessageIsRead.as_view()),
]
But as a result of running this query I get an error:
AttributeError at /messages-read/
Got AttributeError when attempting to get a value for field `text` on serializer
`MessageIsReadSerializer`.
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 'text'.
What is wrong?
With help of Bartosz Stasiak I've fixed my verion of put method.
def put(self, request, *args, **kwargs):
id_list = request.data['ids']
instances = []
for item in id_list:
obj = self.queryset.get(id=item)
obj.is_read = True
obj.save()
instances.append(obj)
serializer = MessageIsReadSerializer(instances, many=True)
return Response(serializer.data)

First: here you are getting a queryset, not an instance, so later in your code you are appending querysets to the instances list. If you want to access single instance you should use get instead of filter
single_instance = self.queryset.get(id=item)
If you want to update multiple items you can use:
def put(self, request, *args, **kwargs):
id_list = request.data['ids']
instances = self.queryset.filter(id__in=id_list)
instances.update(is_read=True)
serializer = MessageIsReadSerializer(instances, many=True)
return Response(serializer.data)

Related

Django : TypeError: int() argument must be a string, a bytes-like object or a number, not 'Driver'

When creating vehicle, in driver_id, I must pass an instance of an object Drive (so when I pass a number, I find an object with this ID in the serializer and pass it, because otherwise it would give an error that an instance needs to be passed).
But when I want to display information on some object vehicle/1/, it will give an error if this object has a driver_id field of an object instance, But when it is null, then everything is fine
Vehicle Model:
class Vehicle(models.Model):
driver_id = models.ForeignKey(Driver,
related_name='vehicles',
on_delete=models.PROTECT,
null=True,
blank=True)
make = models.CharField(max_length=255)
model = models.CharField(max_length=255, verbose_name='Model')
plate_number = models.CharField(max_length=255, verbose_name='Plate Number')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated')
def __str__(self):
return self.model
URLs:
urlpatterns = [
path('vehicle/', VehicleListView.as_view()),
path('vehicle/<int:vehicle_id>/', VehicleView.as_view())
]
class VehicleListView(APIView):
def get(self, request):
vehicles = Vehicle.objects.all()
serializer = VehicleSerializer(vehicles, many=True)
return Response({"vehicles": serializer.data})
def post(self, request):
vehicles = request.data.get('vehicle')
serializer = VehicleSerializer(data=vehicles)
if serializer.is_valid(raise_exception=True):
vehicle_saved = serializer.save()
context = {
'success': f'Vehicle {vehicle_saved.plate_number} created successfully'
}
return Response(context)
class VehicleView(APIView):
def get(self, request, vehicle_id):
vehicle = Vehicle.objects.filter(id=vehicle_id)
serializer = VehicleSerializer(vehicle, many=True)
return Response({"vehicle": serializer.data})
def put(self, request, vehicle_id):
vehicle_saved = get_object_or_404(Vehicle.objects.all(), pk=vehicle_id)
data = request.data.get('vehicle')
serializer = VehicleSerializer(instance=vehicle_saved, data=data, partial=True)
if serializer.is_valid(raise_exception=True):
vehicle_saved = serializer.save()
return Response({
"success": f"Vehicle '{vehicle_saved.plate_number}' updated successfully"
})
def delete(self, request, vehicle_id):
vehicle = get_object_or_404(Vehicle.objects.all(), pk=vehicle_id)
vehicle.delete()
return Response({
"message": f"Vehicle with id `{vehicle_id}` has been deleted."
}, status=204)
class VehicleSerializer(serializers.Serializer):
driver_id = serializers.IntegerField(required=False)
make = serializers.CharField(max_length=255)
model = serializers.CharField(max_length=255)
plate_number = serializers.CharField(max_length=255)
def create(self, validated_data):
validated_data['driver_id'] = Driver.objects.get(id=validated_data['driver_id'])
return Vehicle.objects.create(**validated_data)
def update(self, instance, validated_data):
if "driver_id" in validated_data:
instance.driver_id = Driver.objects.get(id=validated_data['driver_id'])
instance.make = validated_data.get('make', instance.make)
instance.model = validated_data.get('model', instance.model)
instance.plate_number = validated_data.get('plate_number', instance.plate_number)
instance.save()
return instance
Driver Model:
class Driver(models.Model):
first_name = models.CharField(max_length=255, verbose_name='First Name')
last_name = models.CharField(max_length=255, verbose_name='Last Name')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated')
def __str__(self):
return self.first_name
Driver Views:
class DriverView(APIView):
def get(self, request, driver_id):
driver = Driver.objects.filter(id=driver_id)
serializer = DriverSerializer(driver, many=True)
return Response({"driver": serializer.data})
def put(self, request, driver_id):
saved_driver = get_object_or_404(Driver.objects.all(), pk=driver_id)
data = request.data.get('driver')
serializer = DriverSerializer(instance=saved_driver, data=data, partial=True)
if serializer.is_valid(raise_exception=True):
saved_driver = serializer.save()
return Response({
"success": f"Driver '{saved_driver.first_name}' updated successfully"
})
def delete(self, request, driver_id):
driver = get_object_or_404(Driver.objects.all(), pk=driver_id)
driver.delete()
return Response({
"message": f"Driver with id `{driver_id}` has been deleted."
}, status=204)
class DriverListView(APIView):
def get(self, request):
qp = request.query_params
if not qp:
drivers = Driver.objects.all()
else:
created_gte, created_lte = 'created_at__gte', 'created_at__lte'
timezone = created_gte if created_gte in qp else created_lte
time = qp.get(timezone)
time = map(int, time.split('-')[::-1])
if timezone == created_gte:
drivers = Driver.objects.filter(created_at__gte=datetime(*time))
else:
drivers = Driver.objects.filter(created_at__lte=datetime(*time))
serializer = DriverSerializer(drivers, many=True)
return Response({"driver": serializer.data})
def post(self, request):
drivers = request.data.get('driver')
serializer = DriverSerializer(data=drivers)
if serializer.is_valid(raise_exception=True):
driver_saved = serializer.save()
return Response({
'success': f'Driver {driver_saved.first_name} created successfully'
})
Driver Serializer:
class DriverSerializer(serializers.Serializer):
first_name = serializers.CharField(max_length=255)
last_name = serializers.CharField(max_length=255)
def create(self, validated_data):
return Driver.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.save()
return instance
Request:
"vehicle":
{
"driver_id": 1,
"make": "5000г",
"model": "Merc",
"plate_number": "AE 1111 AB"
}
}
Error Message:
TypeError: int() argument must be a string, a bytes-like object or a number, not 'Driver'
You need to pass the id of your object so replace all occurences of the following :
instance.driver_id = Driver.objects.get(id=validated_data['driver_id'])
by
instance.driver_id = Driver.objects.get(id=validated_data['driver_id']).id
Mate, you have got quite a few things confused )
For starters, this line in delete:
vehicle = get_object_or_404(Vehicle.objects.all(), pk=vehicle_id)
should be
vehicle = get_object_or_404(Vehicle, pk=vehicle_id)
objects.all() gets you a whole quesryset, which you don't need here.
Next, this line in create:
validated_data['driver_id'] = Driver.objects.get(id=validated_data['driver_id'])
You already have driver_id in request's data. That what the client of your API wants to delete. There's absolutely no reason to reassign values in that validated_data dict. What's worse, you're doing this:
load Driver model instance from db by using driver_id from the API call. OK, makes sense. What doesn't make sense is why you are substituting driver_id and id. If you need to mirror id into another field in the model, then at least don't mutate it. But my guess is, you don't need this driver_id at all.
Then you are taking the object looked up by id (in db) = drived_id (in the request), and you assigned that object to the key 'driver_id' inside the dict - and then attempt to create a new instance which you have just looked up! Now this makes no sense.

RelatedManager' object has no attribute 'description

I perform request http://167.71.57.114/api2/workout-exercises/3
I want to receive data about WorkoutExercise object number 3 (detail view)
Got AttributeError when attempting to get a value for field description on serializer ExerciseSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the RelatedManager instance.
Original exception text was: 'RelatedManager' object has no attribute 'description'.
serializers.py
class WorkoutExerciseSerializer(serializers.ModelSerializer):
exercises = ExerciseSerializer()
class Meta:
model = WorkoutExercise
fields = ('week', 'exercises')
views.py
class WorkoutExerciseViewSet(viewsets.ModelViewSet):
queryset = WorkoutExercise.objects.all()
serializer_class = WorkoutExerciseSerializer
http_method_names = ['get', 'post']
models.py
class WorkoutExercise(models.Model):
workout_program = models.ForeignKey(WorkoutProgram, on_delete=models.CASCADE, related_name='workout_exercises')
week = models.PositiveIntegerField(default=1)
day = models.PositiveIntegerField(default=1)
order = models.PositiveIntegerField(default=1)
def save(self, *args, **kwargs):
if not self.pk:
last_order = WorkoutExercise.objects.all().aggregate(largest=models.Max('order'))['largest']
if last_order is not None:
self.order = last_order + 1
return super(WorkoutExercise, self).save(*args, **kwargs)
def get_workout_programs(self):
return self.workout_program.name
def get_exercises(self):
pass
def __str__(self):
return self.workout_program.name
class Meta:
ordering = ('week', 'day')
Based on the fact that exercises is plural, and RelatedManager error, it means that there are multiple Exercises, so you need to serialize these with a many=True parameter:
class WorkoutExerciseSerializer(serializers.ModelSerializer):
exercises = ExerciseSerializer(many=True)
class Meta:
model = WorkoutExercise
fields = ('week', 'exercises')

DRF- Error when creating a new instance in an M2M through model

I have the following two models:
class User(models.Model):
user_id = models.CharField(
max_length=129,
unique=True,
)
user_article = models.ManyToManyField(
Article,
through="UserArticle",
)
occupation = models.CharField(max_length=100, default='null')
def __str__(self):
return self.user_id
and
class Article(models.Model):
uuid = models.UUIDField(editable=False, unique=True)
company = models.ForeignKey(
Company,
on_delete=models.PROTECT,
related_name='article_company_id',
)
articleType = models.ForeignKey(
ArticleType,
on_delete=models.PROTECT,
related_name='type',
)
date_inserted = models.DateField()
def __str__(self):
return self.uuid
which are modeled with a many-to-many relationship, using this through model:
class UserArticle(models.Model):
user = models.ForeignKey(User, to_field='user_id',
on_delete=models.PROTECT,)
article = models.ForeignKey(Article, to_field='uuid',
on_delete=models.PROTECT,)
posted_as = ArrayField(
models.CharField(max_length=100, blank=True),)
post_date = models.DateField()
class Meta:
db_table = "core_user_articles"
Here's my view:
class BatchUserArticleList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = UserArticle.objects.all()
serializer_class = BatchUserArticleSerializer
def create(self, request, *args, **kwargs):
serializer = BatchUserArticleSerializer(data=request.data)
if not serializer.is_valid():
return response.Response({'Message': 'POST failed',
'Errors': serializer.errors},
status.HTTP_400_BAD_REQUEST)
self.perform_create(serializer) # equal to serializer.save()
return response.Response(serializer.data, status.HTTP_201_CREATED)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
The problem I'm facing is when I want to POST data, of the following format, in the M2M table:
{
"posted_as": ["news"],
"post_date": "2020-05-26",
"user": "jhtpo9jkj4WVQc0000GXk0zkkhv7u",
"article": [
"11111111",
"22222222"
]
}
The above contains a list of many articles so I used a custom field in my serializer in order to extract each article, create a new UserArticle object and insert it, using bulk_create, into my M2M table. I think that's the way to go when the incoming data do not map exactly to the DB model, but I might be wrong. So please comment if you see something off with this approach.
Here is the serializer:
class BatchUserArticleSerializer(serializers.ModelSerializer):
article= ArticleField(source='*') #custom field
class Meta:
model = UserArticle
fields = ('posted_as', 'post_date', 'user', 'article')
def validate(self, data):
post_date = data['post_date']
if post_date != date.today():
raise serializers.ValidationError(
'post_date: post_date is not valid',
)
return data
def create(self, validated_data):
post_as = list(map(lambda item: item, validated_data['posted_as']))
post_date = validated_data['post_date']
user = validated_data['user']
list_of_articles = validated_data['article']
user_object = User.objects.get(user_id=user)
articles_objects = list(map(lambda res: Article.objects.get(uuid=res), list_of_articles))
user_articles_to_insert = list(map(
lambda article: UserArticle(
posted_as=posted_as,
post_date=post_date,
article=article,
user=user_object),
articles_objects))
try:
created_user_articles = UserArticles.objects.bulk_create(user_articles_to_insert)
for res in created_user_articles:
res.save()
return created_user_articles
except Exception as error:
raise Exception('Something went wrong: {0}'.format(error))
and
class ArticleField(serializers.Field):
def to_representation(self, value):
resource_repr = [value.article]
return resource_repr
def to_internal_value(self, data):
internal_repr = {
'article': data
}
return internal_repr
This seems to work ok as I can see data being correctly inserted in the UserArticle table:
id | posted_as | post_date | user | article
1 | news | 2020-05-26 | jhtpo9jkj4WVQc0000GXk0zkkhv7u | 11111111
2 | news | 2020-05-26 | jhtpo9jkj4WVQc0000GXk0zkkhv7u | 22222222
The problem comes when code reaches this line:
response.Response(serializer.data, status.HTTP_201_CREATED)
and more specifically, the error I'm getting is:
AttributeError: Got AttributeError when attempting to get a value for field `posted_as` on serializer `BatchUserArticleSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `list` instance. Original exception text was: 'list' object has no attribute 'posted_as'.
The original exception error is raised at the instance = getattr(instance, attr) line of the def get_attribute(instance, attrs) function in the fields.py DRF source.
What am I missing here?
First of all, there is no reason to call save method for each of bulk-created instances.
Second one is reason of exception. You call create viewset method. it calling serializers create method which must return only one instance (created object). but your serializer returns list created_user_articles. List really have no field posted_as.
So, there is two ways to fix it.
First one is override create method in view, to change the way of data representation. For ex. use another serializer for response data:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
created_user_articles = self.perform_create(serializer)
# use another way to get representation
response_data = AnotherUserArticleSerializer(created_user_articles, many=True).data
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
# add return to get created objects
return serializer.save()
Second one is return only one instance in create method of your serializer.

Original exception text was: 'QuerySet' object has no attribute 'client'

I got AttributeError when attempting to get a value for field client on serializer ClientSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
models.py
class Box(models.Model):
box = models.IntegerField()
controller = models.ForeignKey(Controller, related_name='boxes', on_delete=models.CASCADE)
def __str__(self):
return str(self.box)
class Client(models.Model):
client = models.CharField(max_length=30)
cpf = models.IntegerField()
box = models.OneToOneField(
Box,
on_delete=models.CASCADE,
primary_key=True
)
def __str__(self):
return self.client
serializers.py
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = [
"id",
"client",
"cpf",
"box",
]
class BoxSerializer(serializers.ModelSerializer):
class Meta:
model = Box
fields = [
"id",
"box",
"controller"
]
views.py
class ClientViewSet(viewsets.ModelViewSet):
serializer_class = ClientSerializer
queryset = Client.objects.all()
def list(self, request, store_pk=None, locker_pk=None, controller_pk=None, box_pk=None):
queryset = Client.objects.filter(box=box_pk, box__controller=controller_pk, box__controller__locker=locker_pk, box__controller__locker__store=store_pk)
serializer = ClientSerializer(queryset, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, store_pk=None, locker_pk=None, controller_pk=None, box_pk=None):
queryset = Client.objects.filter(box=box_pk, box__controller=controller_pk, box__controller__locker=locker_pk, box__controller__locker__store=store_pk)
client = get_object_or_404(queryset)
serializer = ClientSerializer(client, context={'request': request})
return Response(serializer.data)
I'm trying to get the object client on lockers/1/controllers/1/boxes/1/client/
which is OneToOneField relations with boxes and It's in a nested router
I already tried use decorator #action but yet didn't work.
Anyone know why it's not finding the correct object attribute ?
For a list method you should use many=True parameter when you're creating a new serializer instance:
serializer = ClientSerializer(queryset, context={'request': request}, many=True)
In case of retrieve only one object should be received. Instead of
client = get_object_or_404(queryset)
you should call first(), last() (or most basically and clearly - .get(pk=pk)) on queryset to retrieve only one item from QuerySet. Then you should just execute:
# client is one of elements of your queryset
serializer = ClientSerializer(client, context={'request': request})

Automatically create Tag objects when creating an object, Django REST Framework

I have the Task model:
class Task(models.Model):
name = models.CharField(max_length=200, blank=True)
description = models.TextField(max_length=1000, blank=True)
completed = models.BooleanField(default=False)
date_created = models.DateField(auto_now_add=True)
due_date = models.DateField(null=True, blank=True)
date_modified = models.DateField(auto_now=True)
tasklist = models.ForeignKey(Tasklist, null=True, related_name='tasks', on_delete=models.CASCADE)
tags = models.ManyToManyField(TaskType, related_name='tasks')
And class TaskType (tag in other words):
class TaskType(models.Model):
name = models.CharField(max_length=200)
I also have TaskSerializer:
class TaskSerializer(serializers.ModelSerializer):
tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=TaskType.objects.all())
class Meta:
model = Task
fields = '__all__'
read_only_fields = ('date_created', 'date_modified', 'tasklist')
When I create a Task, to add some tags I need to create them in appropriate view firstly, but I want them to be created on the fly.
So in case of editing the task, I added update method:
def update(self, request, *args, **kwargs):
instance = self.get_object()
tag_names = request.data.get('tags', [])
for tag_name in tag_names:
tag, created = TaskType.objects.get_or_create(name=tag_name)
instance.tags.add(tag)
serializer = self.serializer_class(instance=instance, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
It works fine, but when I add new tags when creating a new task it fails (400 bad request):
{
"tags": [
"Object with name=%new_tag% does not exist."
]
}
I figured out that it would be a good way to create appropriate tag object before creating Task with it, so I added perform_create method:
def perform_create(self, serializer):
print('debug')
tag_names = self.request.data.get('tags', [])
for tag_name in tag_names:
tag, created = TaskType.objects.get_or_create(name=tag_name)
list_id = self.kwargs.get('list_id', None)
try:
tasklist = Tasklist.objects.get(pk=list_id)
except Tasklist.DoesNotExist:
raise NotFound()
serializer.save(tasklist=tasklist)
It doesn't help me, actually I am not sure if perform_create method at least is called, because I see no print('debug') in the console (when I create Task with an existing tag I see it).
So the question is how to change perform_create method to be able to create fresh Tasks without creating Tag firstly.
First, you don't need to manually add tags in your view methods. The serializer will do it for you.
Second, the update method is used when you update your model. When creating you need to override create method, perform_create works, but happens too late:
def create(self, request, *args, **kwargs):
tag_names = request.data.get('tags', [])
for tag_name in tag_names:
TaskType.objects.get_or_create(name=tag_name)
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
Third, once you've created new tags, call super().*method* and let the framework do the work for you:
def update(self, request, *args, **kwargs):
tag_names = request.data.get('tags', [])
for tag_name in tag_names:
TaskType.objects.get_or_create(name=tag_name)
return super().update(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
tag_names = request.data.get('tags', [])
for tag_name in tag_names:
TaskType.objects.get_or_create(name=tag_name)
return super().create(request, *args, **kwargs)
There is also partial_update method you need to add if you are planning to use PATCH method, so you might be better off overriding your serializer's to_internal_value method instead of each of those:
class TaskSerializer(serializers.ModelSerializer):
tags = serializers.SlugRelatedField(
many=True, slug_field='name', queryset=TaskType.objects.all())
class Meta:
model = Task
fields = '__all__'
read_only_fields = ('date_created', 'date_modified', 'tasklist')
def to_internal_value(self, data):
for tag_name in data.get('tags', []):
TaskType.objects.get_or_create(name=tag_name)
return super().to_internal_value(data)
Try editing your view like this,
def update(self, request, *args, **kwargs):
instance = self.get_object()
tag_names = request.data.get('tags', [])
serializer = self.serializer_class(instance=instance, data=request.data)
if serializer.is_valid(raise_exception=True)
new_object = serializer.save()
if new_object:
for tag_name in tag_names:
tag, created = TaskType.objects.get_or_create(name=tag_name)
new_object.tags.add(tag)
return Response(serializer.data)

Categories