[If this thread is too generic, let me know and I’ll make sure to ask more specific questions: I would immensely appreciate it even if you guys gave me an answer even to a single point]
I’ve been using Django for approximately 1 year, and I’m currently working on my biggest project so far. It’s a web app for my university, for teachers to create online exams and students to take on them, and I made it with Django REST fw + vue.js.
It’s become a big project and I’m proud of the fact I’ve been able to make it work all by myself, but now I’m taking a look at the code I wrote so far and I’m wondering what could’ve been done better. This is mainly a huge learning experience for me. I’ll give here a brief rundown of what the app does, and then I’ll ask some specific questions.
- Teachers and students can log in using their university account (this is done via Google social login with oauth2)
- Teachers have an editor to create Exams, which are made up of Questions and programming Exercises. All exam items belong to a Category, and there are several randomization options to have students be presented with different questions or in a different order (the words beginning with a capital letter are models).
- Students log in and type in the exam code, given to them by teachers, and are then presented sequentially all the questions and exercises. The sequence is dynanically generated as students answer or skip items. All GivenAnswers to questions and Submissions to programming exercises are recorded and stored in a db.
- At the end of an exam, teachers can download either a CSV report or a zip archive containing a PDF file for each student, to see the answers (ExamReport model).
- There are other features that aren’t relevant here, like: a dashboard for teachers to monitor progress during an exam; a websocket-based lock that prevents two teachers from editing an exam at the same time, and a node backend that runs student-submitted code for programming exercises and automatically tests it against teacher-provided test cases.
Here’s a link to the repository for the backend: GitHub - samul-1/js-exercise-platform
(The name js-exercise-platform is misleading: this started out as a platform for programming exercises in JavaScript, but then expanded as a general-purpose exam platform that any course or subject can use)
Here are some doubts I have about my code and the general sw engineering choices I made:
- I never used model inheritance, and I found myself with some repeated code. Take this model:
class Question(models.Model):
"""
A question shown in exams
"""
# ...
text = models.TextField()
rendered_text = models.TextField(null=True, blank=True, default="")
# ...
def save(self, render_tex=True, *args, **kwargs):
# ...
super(Question, self).save(*args, **kwargs)
if render_tex:
self.rendered_text = tex_to_svg(self.text)
self.save(render_tex=False)
The function tex_to_svg
runs Mathjax in a node subprocess to convert LaTeX formulas to svg’s, preventing the users’ client from having to do the rendering. We also keep the original text around to allow teachers to update the text of the items.
I have those two fields and that logic in the save
method in 3 other models. Is this considered bad practice? What the instinct tells me is I would have to create an abstract class LaTeXRenderable
or whatever, and use it as an interface to inherit those two fields. I am unsure what would happen to the save
method though. What if I have to inherit from another abstract model too? Moreover, would it be better to have an abstract ExamItem
model and inherit from it for both Question
and Exercise
?
- when it comes to nested serializers, I’m having a hard time generalizing or keeping the code concise and DRY. If you look at my ExamSerializer’s update method, you’ll concur it’s pretty ugly. I have duplicate code all around. One thing I thought of trying is the following: in my
ExamSerializer
I would keep a fieldchildren
with the name of the models I need to nested-update, and then I could do something like this:
for child in chidren:
child_model = get_model("jsplatform", child)
child_serializer = getattr(sys.modules[__name__], f"{child}Serializer")
# update each item
for item_data in items_data:
if item_data.get("id") is not None: # item has an id: object already exists
item = child_model.objects.get(pk=item_data["id"])
save_id = item_data.pop("id")
else: # item does not have an id: create new object
item = item(exam=instance)
item.save()
save_id = item.pk
serializer = child_serializer(
item, data=item_data, context=self.context
)
# pop item category as it's not handled by the serializer
item_category = item_data.pop("category", None)
# item belongs to a new category we just created; get it
if item_category is None:
item_category = Category.objects.get(
tmp_uuid=item_data["category_uuid"]
)
serializer.is_valid(raise_exception=True)
# update item
updated_item = serializer.update(
instance=item, validated_data=item_data
)
# update item category
updated_item.category = item_category
updated_item.save()
# remove item from the list of those still to process
items = items.exclude(pk=save_id)
# remove any items for which data wasn't sent (i.e. user deleted them)
for item in items:
item.delete()
Would this be any better?
- I have a method that I use for
Question
s that goes like this:
def format_for_pdf(self):
return {
"text": preprocess_html_for_pdf(self.rendered_text),
"introduction_text": preprocess_html_for_pdf(self.introduction_text),
"type": self.question_type,
"answers": [
{
"text": preprocess_html_for_pdf(a.rendered_text),
"is_right_answer": a.is_right_answer,
}
for a in self.answers.all()
],
}
When having to do this kind of processing, is there a better/more elegant way? I thought I could probably do this with a serializer rather than a model method. How would I achieve this?
In addition to the points above:
- do you see any no-no’s in my code, just glancing through the repo?
- are there queries that could be optimized? I have recently started using
prefetch_related
, but I’m learning there are more ways to optimize db access - what are the best ways to keep your Django code DRY and just compliant with the SOLID principles in general when you have several parts of your application that behave similarly, but also slightly differently?
Thank you so much to anyone who made it through the post, and I’m looking forward to hearing your feedback.