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:
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.
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:
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....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?I don't think it's unsafe to have stamp.save()
in PUT method because by definition it supposes to alter object's value.
For returning only relevant stampcards, you could just add stampcard to a set like this
class StampUpdateView(APIView):
def get_object(self, uuid):
try:
return Stamp.objects.get(uuid=uuid)
except Stamp.DoesNotExist():
raise Http404
def put(self, request, *args, **kwargs):
stampcard_set = set()
for stamp_data in request.data:
stamp = self.get_object(stamp_data['stamp_uuid'])
user_stampcard = request.user.stampcard_set.exclude(done=True).filter(stampcardformat=stamp.stampcardformat)
if user_stampcard.exists():
stampcard = user_stampcard.earliest('timestamp')
else:
stampcard = Stampcard(stampcardformat=stamp.stampcardformat, user=request.user)
stampcard.save()
stamp.stampcard = stampcard
stamp.save()
if stampcard.stamp_set.count() == stampcard.stampcardformat.limit:
stampcard.done = True
stampcard.save()
stampcard_set.add(stampcard)
stampcard_serializer = StampcardSerializer(stampcard_set, many=True)
return Response(stampcard_serializer.data)
This way it doesn't matter if returning stampcards are already done or not.
Also note that I move down limit checking lines in your code to be after when stamp was saved because if limit was set to 1, stampcard will have to be set as done immediately after a stamp was added.