I’m trying to refactor my DRF app where I have the following models:
class Exam(models.Model):
name = models.TextField()
# ...
class Category(models.Model):
exam = models.ForeignKey(
Exam, null=True, on_delete=models.SET_NULL, related_name="categories"
)
name = models.TextField()
# ...
class Question(models.Model):
text = models.TextField()
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="questions",
)
exam = models.ForeignKey(
Exam, null=True, on_delete=models.SET_NULL, related_name="questions"
)
# ...
On my frontend, I made a form where the user can create/edit an exam, and basically add/edit both categories and questions at the same time. Everything gets then sent at once in a POST/PUT request. The problem is, by the time everything is sent, categories haven’t been created in the db yet, so questions can’t reference them using their pk. In my frontend application, I temporarily use a UUID until I get an id from the db for the categories. The problem is I haven’t been able to find a good way to deal with the data on the server, as I can’t just use say a PrimaryKeyRelatedField
on my question serializer to reference the category.
I’ll show the solution I’ve been using (which works fine but is ugly), and I’d very much appreciate it if someone could help me find a better way to achieve this, or even just give me some intuition as to what a better approach could look like.
What I did is I added a UUID field to Category
: tmp_uuid
. This holds the UUID temporarily generated in the frontend to reference the not-yet-created category.
The serializer for Question
then gets added a field: category_uuid
, which is write_only
and only used to temporarily hold the relation to the category.
The idea is to, inside the update
method of ExamSerializer
, first create all the categories, then use the temporary UUID to get the questions referencing those categories, and build the permanent relation using the actual foreign keys.
This is the code:
def update(self, instance, validated_data):
# get data about categories and questions
questions_data = validated_data.pop("questions")
categories_data = validated_data.pop("categories")
instance = super(ExamSerializer, self).update(instance, validated_data)
questions = instance.questions.all()
categories = instance.categories.all()
# update each category
for category_data in categories_data:
if category_data.get("id") is not None:
category = Category.objects.get(pk=category_data["id"])
save_id = category_data.pop("id")
else:
category = Category(exam=instance)
category.save()
save_id = category.pk
serializer = CategorySerializer(
category, data=category_data, context=self.context
)
serializer.is_valid(raise_exception=True)
# update category
serializer.update(instance=category, validated_data=category_data)
# remove category from the list of those still to process
categories = categories.exclude(pk=save_id)
# remove any questions for which data wasn't sent (i.e. user deleted them)
for category in categories:
category.delete()
# update each question
for question_data in questions_data:
if question_data.get("id") is not None:
question = Question.objects.get(pk=question_data["id"])
save_id = question_data.pop("id")
else:
question = Question(exam=instance)
question.save()
save_id = question.pk
serializer = QuestionSerializer(
question, data=question_data, context=self.context
)
# pop question category as it's not handled by the serializer
question_category = question_data.pop("category", None)
# question belongs to a new category we just created
if question_category is None:
question_category = Category.objects.get(
tmp_uuid=question_data["category_uuid"]
)
serializer.is_valid(raise_exception=True)
# update question
updated_question = serializer.update(
instance=question, validated_data=question_data
)
# update question category
updated_question.category = question_category
updated_question.save()
# remove question from the list of those still to process
questions = questions.exclude(pk=save_id)
# remove any questions for which data wasn't sent (i.e. user deleted them)
for question in questions:
question.delete()
This is pretty contrived and not particularly elegant. The issue here is having to represent relations between objects that don’t yet exist.
Do you know any better way I could achieve this? Thank you!