I just stumbled upon the hardest problem I ever had with Django Rest Framework. Let me give you my models first, and then explain:
class Stampcardformat(models.Model):
workunit = models.ForeignKey(
Workunit,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
limit = models.PositiveSmallIntegerField(
default=10
)
category = models.CharField(
max_length=255
)
class Stampcard(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
class Stamp(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
stampcard = models.ForeignKey(
Stampcard,
on_delete=models.CASCADE,
blank=True,
null=True
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
These models describe a simple stampcard model. A stampcard is considered full, when it has as many stamps via foreignkey associated to it as it's stampcardformat's limit number dictates. I need to write view that does the following:
- The view takes in a list of stamps (see below) consisting of their uuid's.
- It then needs to find the right stampcardformat for each given stamp.
Next it needs to check, whether the requests user has a stampcard with the corresponding stampcardformat.
a) If it has, it needs to check, if the stampcard is full or not.
i) If it is full, it needs to create a new stampcard of the given format and update the stamps stampcard-foreignkey to the created stampcard.
ii) If it isn't full, it needs update the stamps stampcard-foreignkey to the found stampcard
b) If the user hasn't got a stampcard of the given stampcardformat, it needs to create a new stampcard and update the stamps stampcard-foreignkey to the created stampcard.
Here is the request body list of stamps:
[
{
"stamp_uuid": "62c4070f-926a-41dd-a5b1-1ddc2afc01b2"
},
{
"stamp_uuid": "4ad6513f-5171-4684-8377-1b00de4d6c87"
},
...
]
The class based views don't seem to support this behaviour. I tried modifying the class based views, to no avail. I fail besides many points, because the view throws the error:
AssertionError: Expected view StampUpdate to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
Edit
For additional context: I need the url to be without pk, slug or anything. So the url should just be something like:
/api/stampcards/stamps/
and do a put (or any request that has a body and works) to it. The route I wrote is:
url(r'^stamps/$', StampUpdate.as_view(), name='stamp-api-update'),
Edit: HUGE update. So I managed to cheese together a view that works. First I updated the stampcard model like this (I did add anew field 'done' to track if it is full):
class Stampcard(models.Model):
stampcardformat = models.ForeignKey(
Stampcardformat,
on_delete=models.CASCADE
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True
)
done = models.BooleanField(default=False)
Then I wrote the view like this:
class StampUpdate(APIView):
permission_classes = (IsAuthenticated,)
def get_object(self, uuid):
try:
return Stamp.objects.get(uuid=uuid)
except Stamp.DoesNotExist():
raise Http404
def put(self, request, format=None):
for stamp_data in request.data:
stamp = self.get_object(stamp_data['stamp_uuid'])
if stamp.stampcard==None:
user_stampcard = self.request.user.stampcard_set.exclude(done=True).filter(stampcardformat=stamp.stampcardformat)
if user_stampcard.exists():
earliest_stampcard = user_stampcard.earliest('timestamp')
stamp.stampcard = earliest_stampcard
stamp.save()
if earliest_stampcard.stamp_set.count() == earliest_stampcard.stampcardformat.limit:
earliest_stampcard.done=True
earliest_stampcard.save()
else:
new_stampcard = Stampcard(stampcardformat=stamp.stampcardformat, user=self.request.user)
new_stampcard.save()
stamp.stampcard = new_stampcard
stamp.save()
new_stampcards = Stampcard.objects.exclude(done=True).filter(user=self.request.user)
last_full_stampcard = Stampcard.objects.filter(user=self.request.user).filter(done=True)
if last_full_stampcard.exists():
last_full_stampcard_uuid=last_full_stampcard.latest('updated').uuid
last_full_stampcard = Stampcard.objects.filter(uuid=last_full_stampcard_uuid)
stampcards = new_stampcards | last_full_stampcard
else:
stampcards = new_stampcards
print(stampcards)
stampcard_serializer = StampcardSerializer(stampcards, many=True)
return Response(stampcard_serializer.data)
But I have two issues with this code:
- My intuition tells me that the parts where is just call save() on the model instance (e.g.
stamp.save()
) are very unsafe for an api. I couldn't get it to work to serialize the data first. My question is: Is this view okay like this? Or can I improve anything? It doesn't use generic class based used for example, but I don't know how to use them here... - I would also love to return the stampcard, if it got filled up by this method. But I also want to exclude all non-relevant stampcards, which is why I called
.exclude(done=True)
. A stampcard that got filled up unfortunately has done=True though! How can I add stampcards that got filled up in the process to the return value?