Do not create sub-objects in django - python

I have a model representing a Status - and have a foreign key to Status from an Object model. I want to be able to create new objects, but do not want to allow the possibility of creating any more Status entries (there are a set of 5 pre-defined ones that are migrated into the database). I believe I have figured out how to structure serializers in such a way as to only re-use existing Status entries, but I'm not sure if it is the best way to go about doing something like this...
Some Simplified Code:
class StatusSerializer(serializers.ModelSerializer):
class Meta:
model = Status
fields = ('name',)
def to_representation(self, obj):
return obj.name
def to_internal_value(self, data):
return {
'name': data
}
class ObjectSerializer(serializers.ModelSerializer):
status = StatusSerializer(read_only=True)
class Meta:
model = Object
fields = ('obj_name', 'status',)
def create(self, validated_data):
# We do not want to create new statuses - only use existing ones
status = Status.objects.get(name=self.initial_data['status'])
return Object.objects.create(status=status, **validated_data)
def update(self, instance, validated_data):
instance.obj_name = validated_data.get('obj_name', instance.obj_name)
# We do not want to create new statuses - only use existing ones
instance.status = Status.objects.get(name=self.initial_data['status']) if 'status' in self.initial_data else instance.status
return instance
As seen above, I also flatten out the Status object when it is displayed - e.g. I turn the following
{
'obj_name': 'ObjectName',
'status': {
'name': 'StatusName'
}
}
Into this
{
'obj_name': 'ObjectName',
'status': 'StatusName'
}
This seems to work fine -- however I am not sure how to handle cases where a user of the API gives me an invalid Status name. Right now, the api would bubble up a Status.DoesNotExist exception from one of the Status.objects.get(...) requests - should I just catch that and re-raise as something the serializer/view would expect?
Thanks!
Edit: Realized my question wasnt exactly clear...
Is the above a good way to prohibit creation of Status objects - and enforce that any Object created will use one of those statuses?
What is the best way to handle the case where a user attempts to create an Object with an invalid Status name?

You can use validate method in the ObjectSerializer.
class ObjectSerializer(serializers.ModelSerializer):
status = StatusSerializer(read_only=True)
class Meta:
model = Object
fields = ('obj_name', 'status',)
def validate(self, attrs):
validated_data = super().validate(attrs)
status = self.initial_data.get('status')
# Here assuming that None is not the valid value for status
if status is not None:
status_obj = Status.objects.filter(name=status)
if not status_obj:
raise serializer.ValidationError('Invalid Status')
status_obj = status_obj[0]
validated_data['status'] = status_obj
return validated_data
# Nothing special to be done in create/update since we are sending data
# in validated_data which will directly sent to the instance.
# def create(self, validated_data):
# We do not want to create new statuses - only use existing ones
# status = Status.objects.get(name=self.initial_data['status'])
#return Object.objects.create(status=status, **validated_data)
#def update(self, instance, validated_data):
# instance.obj_name = validated_data.get('obj_name', instance.obj_name)
# We do not want to create new statuses - only use existing ones
# instance.status = Status.objects.get(name=self.initial_data['status']) if 'status' in self.initial_data else instance.status
# return instance
And if you can use different keys for write and read status, then you can modify ObjectSerializer like this:
class ObjectSerializer(serializer.ModelSerializer):
status = serializer.SlugRelatedField(slug_field='name', queryset=Status.objects.all(), write_only=True)
status_data = StatusSerializer(read_only=True, source='status')
class Meta:
model = Object
fields = ('obj_name', 'status', 'status_data')
In this case, if you pass {'obj_name': 'ObjectName', 'status': 'StatusName'} data to serializer, the serializer will first check for the StatusName value in the name field of the provided queryset (In this case we are using all) and if not valid raises ValidationError. If valid, then saves the status in the field of the instance.

Related

DRF: How to add annotates when create an object manually?

Currently I'm trying to create an expected json to use in my test:
#pytest.mark.django_db(databases=['default'])
def test_retrieve_boards(api_client):
board = baker.make(Board)
objs = BoardSerializerRetrieve(board)
print(objs.data)
url = f'{boards_endpoint}{board.id}/'
response = api_client().get(url)
assert response.status_code == 200
But i'm receiving the following error:
AttributeError: Got AttributeError when attempting to get a value for field `cards_ids` on serializer `BoardSerializerRetrieve`.
E The serializer field might be named incorrectly and not match any attribute or key on the `Board` instance.
E Original exception text was: 'Board' object has no attribute 'cards_ids'
Currently cards_idsare added on my viewSet on get_queryset method:
def get_queryset(self):
#TODO: last update by.
#TODO: public collections.
"""Get the proper queryset for an action.
Returns:
A queryset object according to the request type.
"""
if "pk" in self.kwargs:
board_uuid = self.kwargs["pk"]
qs = (
self.queryset
.filter(id=board_uuid)
.annotate(cards_ids=ArrayAgg("card__card_key"))
)
return qs
return self.queryset
and this is my serializer:
class BoardSerializerRetrieve(serializers.ModelSerializer):
"""Serializer used when retrieve a board
When retrieve a board we need to show some informations like last version of this board
and the cards ids that are related to this boards, this serializer will show these informations.
"""
last_version = serializers.SerializerMethodField()
cards_ids = serializers.ListField(child=serializers.IntegerField())
def get_last_version(self, instance):
last_version = instance.history.first().prev_record
return HistoricalRecordSerializer(last_version).data
class Meta:
model = Board
fields = '__all__'
what is the best way to solve it? I was thinking in create a get_cards_ids method on serializer and remove annotate, but I don't know how to do it and justing googling it now. rlly don't know if this is the correct way to do it.
Test the view, not the serializer, i.e. remove BoardSerializerRetrieve(board) from your test code.
cards_ids is annotated on ViewSet level. The annotated queryset is then passed to serializer.
#pytest.mark.django_db(databases=['default'])
def test_retrieve_boards(api_client):
board = baker.make(Board)
url = f'{boards_endpoint}{board.id}/'
response = api_client().get(url)
assert response.status_code == 200
Also, instead of building the URL manually with url = f'{boards_endpoint}{board.id}/', consider using reverse, e.g. url = reverse("path-name", kwargs={"pk": board.id}).

Getting last insert id of the object saved by serializer in Django

I have a visitorSaveSerializer which is responsible for validating the data to be saved:
class VisitorSaveSerializer(serializers.ModelSerializer):
class Meta:
model = Visitor
fields = ('gsm', 'email', 'firstname', 'lastname')
The problem is:
visitor_serializer = VisitorSaveSerializer(data={...related data here...})
if visitor_serializer.is_valid():
visitor_serializer.save()
visitor_id = visitor.serializer.data.get("id", 0) // Fails for sure.
OK, I know id is not among serializer fields, so last line fails.
How should I approach saving an object when I need to get last inserted id?
The serializer returns the instance saved, so you can obtain the primary key of that instance with:
visitor_serializer = VisitorSaveSerializer(data={…})
if visitor_serializer.is_valid():
visitor = visitor_serializer.save()
visitor_id = visitor.pk

Django Rest Framework, updating multiple objects in one

I am trying to update multiple objects using PATCH to my Django backend. This is the request I am sending:
[
{
"pk":78,
"weekday":1,
"from_hour":"21:00",
"to_hour":"12:00:00",
"closed":false,
"lunch":true,
"lunch_start":null,
"lunch_end":null,
"lunch2":false,
"lunch_start2":null,
"lunch_end2":null,
"appointment_interval":15,
"num_appointments_interval":4,
"office":79
},
{
"pk":79,
"weekday":2,
"from_hour":"09:00:00",
"to_hour":"12:00:00",
"closed":false,
"lunch":true,
"lunch_start":null,
"lunch_end":null,
"lunch2":false,
"lunch_start2":null,
"lunch_end2":null,
"appointment_interval":15,
"num_appointments_interval":4,
"office":79
},
{
"pk":80,
"weekday":3,
"from_hour":"09:00:00",
"to_hour":"12:00:00",
"closed":false,
"lunch":true,
"lunch_start":null,
"lunch_end":null,
"lunch2":false,
"lunch_start2":null,
"lunch_end2":null,
"appointment_interval":15,
"num_appointments_interval":4,
"office":79
},
{
"pk":81,
"weekday":4,
"from_hour":"09:00:00",
"to_hour":"12:00:00",
"closed":false,
"lunch":false,
"lunch_start":"14:59:50",
"lunch_end":"14:59:51",
"lunch2":false,
"lunch_start2":null,
"lunch_end2":null,
"appointment_interval":15,
"num_appointments_interval":4,
"office":79
},
]
I send this to a custom view where I am trying to serialize and update the data.
#api_view(['PATCH'])
#parser_classes((JSONParser,))
def updateOfficeHours(request):
office_id = request.data[0]['office']
qs = OfficeHour.objects.filter(office__pk=office_id)
office_hours = OfficeHoursSerializer(qs, data=request.data, many=True, partial=True)
if not office_hours.is_valid():
print(":(")
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
office_hours.save()
return Response(status=status.HTTP_200_OK)
I only end up getting this error:
AttributeError: 'QuerySet' object has no attribute 'pk'
It seems like this error comes up when you are looking for one object, but I have many=True. What am I doing wrong?
ListSerializer can solve the problem. Here's how:
class OfficeHoursListSerializer(serializers.ListSerializer):
def update(self, instances, validated_data):
# here you can implement your own logic for updating objects
# this is just an example
result = []
for instance in instances:
for data in validated_data:
if data['id'] == instance.pk:
instance.some_field = data['some_field']
instance.save()
result.append(instance)
return result
Then you need to specify the ListSerializer in the OfficeHoursSerializer:
class OfficeHoursSerializer(serializers.ModelSerializer):
# It needs to identify elements in the list using their primary key,
# so use a writable field here, rather than the default which would be read-only.
id = serializers.IntegerField()
...
class Meta:
...
list_serializer_class = OfficeHoursListSerializer

How to set a deadline for POST requests based on related object?

I have two related model like this:
Form:
name
fields
date_deadline
FormEntry:
form = ForeignKey(Form)
data
I want to prevent adding new entry after submission deadline. I write a validation in serializer like this:
class FormEntrySerializer(serializers.ModelSerializer):
def validate(self, data):
from datetime import datetime
form = data.get('form')
if form.date_deadline and\
datetime.date(datetime.today()) > form.date_deadline:
message = 'Entries can\'t be added after submission deadline.'
raise serializers.ValidationError(message)
return data
class Meta:
model = FormEntry
fields = (
'id', 'form', 'data',
)
It works but I can't update an form entry too after submission deadline. I want to make this validation only for POST requests (means new insertions).
Also I'm not sure this is the best way to do it. Maybe I must use permissions.
How I do it?
You can check either an instance exists:
class FormEntrySerializer(serializers.ModelSerializer):
def validate(self, data):
from datetime import datetime
form = data.get('form')
if not self.instance and form.date_deadline and\
datetime.date(datetime.today()) > form.date_deadline:
message = 'Entries can\'t be added after submission deadline.'
raise serializers.ValidationError(message)
return data
class Meta:
model = FormEntry
fields = (
'id', 'form', 'data',
)
If the instance doesn't exist then it's being created, otherwise updated.
Check the docs.

Foreign Key Resource from dynamic field

I've got an API endpoint called TrackMinResource, which returns the minimal data for a music track, including the track's main artist returned as an ArtistMinResource. Here are the definitions for both:
class TrackMinResource(ModelResource):
artist = fields.ForeignKey(ArtistMinResource, 'artist', full=True)
class Meta:
queryset = Track.objects.all()
resource_name = 'track-min'
fields = ['id', 'artist', 'track_name', 'label', 'release_year', 'release_name']
include_resource_uri = False
cache = SimpleCache(public=True)
def dehydrate(self, bundle):
bundle.data['full_artist_name'] = bundle.obj.full_artist_name()
if bundle.obj.image_url != settings.NO_TRACK_IMAGE:
bundle.data['image_url'] = bundle.obj.image_url
class ArtistMinResource(ModelResource):
class Meta:
queryset = Artist.objects.all()
resource_name = 'artist-min'
fields = ['id', 'artist_name']
cache = SimpleCache(public=True)
def get_resource_uri(self, bundle_or_obj):
return '/api/v1/artist/' + str(bundle_or_obj.obj.id) + '/'
The problem is, the artist field on Track (previously a ForeignKey) is now a model method called main_artist (I've changed the structure of the database somewhat, but I'd like the API to return the same data as it did before). Because of this, I get this error:
{"error": "The model '<Track: TrackName>' has an empty attribute 'artist' and doesn't allow a null value."}
If I take out full=True from the 'artist' field of TrackMinResource and add null=True instead, I get null values for the artist field in the returned data. If I then assign the artist in dehydrate like this:
bundle.data['artist'] = bundle.obj.main_artist()
...I just get the artist name in the returned JSON, rather than a dict representing an ArtistMinResource (along with the associated resource_uris, which I need).
Any idea how to get these ArtistMinResources into my TrackMinResource? I can access an ArtistMinResource that comes out fine using the URL endpoint and asking for it by ID. Is there a function for getting that result from within the dehydrate function for TrackMinResource?
You can use your ArtistMinResource in TrackMinResource's dehydrate like this (assuming that main_artist() returns the object that your ArtistMinResource represents):
artist_resource = ArtistMinResource()
artist_bundle = artist_resource.build_bundle(obj=bundle.obj.main_artist(), request=request)
artist_bundle = artist_resource.full_dehydrate(artist_bundle)
artist_json = artist_resource.serialize(request=request, data=artist_bundle, format='application/json')
artist_json should now contain your full artist representation. Also, I'm pretty sure you don't have to pass the format if you pass the request and it has a content-type header populated.

Categories

Resources