Hello good Django people!
In my project develpoing a online course app, now i have a new challange
The idea:
To allow the client to create a test for the course module, and add questions to that test.
Frontend:
Test is rendered on a page of the course. The Student has to submit answers to that test. Then the client will grade the answers. If the student passes the test, he can download the certificate (this is yet to be thought about, not important for this post)
I have managed to create the features for adding the test and questions to that test. Everything is working, in the admin i can see the test and the question that bellong to it.
Been using HTMX and it was really really helpful.
I am totally lost on how to render the test with questions and fields to input the answer and save that answers to database for that Student, so the client can grade the test and give feedback.
These are my models for custom user
from django.db import models
# import abstract user
from django.contrib.auth.models import AbstractUser
# when using file upload path, the error appears when editing profile
def file_upload_path(instance, filename):
return f'user-{instance.id}/images/{filename}'
# DEFINE OUR CUSTOM USER
class User(AbstractUser):
class Role(models.TextChoices):
ADMIN = 'ADMIN', 'Admin'
OWNER = 'OWNER', 'Owner'
EMPLOYEE = 'EMPLOYEE', 'Employee'
MODERATOR = 'MODERATOR', 'Moderator'
CUSTOMER = 'CUSTOMER', 'Customer'
STUDENT = 'STUDENT', 'Student'
email = models.EmailField(unique=True, max_length=50)
username = models.CharField(unique=True, max_length=50)
role = models.CharField(max_length=20, choices=Role.choices, null=True, blank=True)
phone_number = models.CharField(max_length=20, null=True, blank=True)
image = models.ImageField(upload_to=file_upload_path, default='default-images/profile.png', null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
agree_to_terms = models.BooleanField('Slažem se sa uslovima', default=False, blank=False, null=False)
def __str__(self):
return self.email
# to show full name
def full_name(self):
return f'{self.first_name} {self.last_name}'
# to check the role
def get_role(self):
if self.is_superuser:
user_role = "Superuser"
elif self.role == User.Role.ADMIN:
user_role = 'Admin'
elif self.role == User.Role.OWNER:
user_role = 'Owner'
elif self.role == User.Role.EMPLOYEE:
user_role = 'Employee'
elif self.role == User.Role.CUSTOMER:
user_role = 'Customer'
elif self.role == User.Role.STUDENT:
user_role = 'Student'
else:
user_role = 'Unknown'
return user_role
# when user is created, create a profile for that user via signals
class UserProfile(models.Model):
# relation to User
user = models.OneToOneField(User, on_delete=models.CASCADE)
def __str__(self):
return self.user.email
These are the models for the Course
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError
#import magic
# rich text field
from ckeditor_uploader.fields import RichTextUploadingField
# ili from ckeditor.fields import RichTextField
from django.utils import timezone
def course_file_upload_path(instance, filename):
return f'user-{instance.created_by.id}/courses/files/{filename}'
class Course(models.Model):
title = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=110, unique=True)
image = models.ImageField(upload_to=course_file_upload_path, default='default-images/post.png')
description = RichTextUploadingField(null=True, blank=True)
#description = models.TextField(null=True, blank=True)
short_description = models.CharField(max_length=100)
price = models.DecimalField(max_digits=7, decimal_places=2, default='100.00')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
#is_paid = models.BooleanField(default=False)
# is featured - prikazi u sidebaru na blogu
is_featured = models.BooleanField('Naglasi kurs', default=False)
# relations
# to owner of the course = logged in user who is creating the course
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='courses')
# student user
# to a user that will attend the course
student_user = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='enrollments', through='Enrollment')
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('courses:view_course', args=[str(self.slug)])
def clean(self):
self.title = self.title.capitalize()
self.slug = self.slug.lower()
class Enrollment(models.Model):
# user to be enrolled
student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='student_enrollments')
# course to be enrolled in
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_enrollments')
enrolled_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
is_paid = models.BooleanField('Placeno',default=False)
class Meta:
unique_together = ('student', 'course')
def __str__(self):
return self.student.email
class Module(models.Model):
title = models.CharField(max_length=50)
tagline = models.CharField(max_length=50)
created_at = models.DateTimeField(auto_now_add=True)
# relation to Course
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='modules')
def __str__(self):
return self.title
class Meta:
ordering = ['created_at']
class Lesson(models.Model):
STATUS_CHOICES = (
('draft','Draft'),
('published','Published'),
('waiting_approval','Waiting approval'),
)
title = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=110, unique=True)
tizer = models.CharField(max_length=100)
content = RichTextUploadingField(null=True, blank=True)
#video = models.FileField(upload_to=course_file_upload_path, null=True, blank=True)
video = models.CharField(max_length=300)
duration = models.DurationField(help_text='HH:MM:SS', null=True, blank=True)
created_at = models.DateField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
#order = models.SmallIntegerField()
order = models.IntegerField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='published')
# mark lesson as complete
is_completed = models.BooleanField(default=False)
is_free_preview = models.BooleanField(default=False)
# relations
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='lessons')
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='lessons')
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='lessons')
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('courses:view_lesson', args=[str(self.course.slug), str(self.slug)])
def clean(self):
self.title = self.title.capitalize()
self.slug = self.slug.lower()
class Meta:
unique_together = ('course', 'order')
ordering = ['order']
pdf_ext_validator = FileExtensionValidator(['pdf'])
'''def validate_file_mimetype(file):
accept = ['application/pdf']
file_mime_type = magicfrom_buffer(file.read(1024), mime=True)
print(file_mime_type)
if file_mime_type not in accept:
raise ValidationError("Unsupported file type")'''
class Pdf(models.Model):
title = models.CharField(max_length=100, null=True, blank=True)
file = models.FileField(upload_to=course_file_upload_path, validators=[pdf_ext_validator], null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# relation to Lesson
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='pdfs', null=True, blank=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def __str__(self):
return self.title or "PDF Default Title"
class Test(models.Model):
title = models.CharField(max_length=100)
description = models.CharField(max_length=200, blank=True, null=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(default=timezone.now)
minimum_to_pass = models.IntegerField(default=80, help_text='Minimum % needed to pass the test')
# relation to course Module
module = models.ForeignKey(Module, on_delete=models.CASCADE)
# relation to user who is creating the test
# to owner of the course = logged in user who is creating the course and the test
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='tests')
# student user
# to a user that will take the test
student_user = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='exams', through="Exam")
def __str__(self):
return self.title
def clean(self):
self.title = self.title.capitalize()
def get_questions(self):
return self.questions.all()
class Exam(models.Model):
# to Test
test = models.ForeignKey(Test, on_delete=models.CASCADE)
# student who will take the test
student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='student_exams')
result = models.IntegerField()
def __str__(self):
return self.student.email
class Question(models.Model):
question_text = models.CharField(max_length=150)
answer_text = models.CharField(max_length=300, default='answer')
# relation to Test
test = models.ForeignKey(Test, on_delete=models.CASCADE, related_name='questions')
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.question_text
These are the views for adding the questions and the view to take the test
def add_test(request):
form = TestForm()
if request.method == 'POST':
form = TestForm(request.POST)
if form.is_valid():
test = form.save(commit=False)
test.created_by = request.user
test.save()
messages.success(request, 'Test is created successfully!')
return redirect('dashboard:dashboard')
context = {
'form': form
}
return render(request, 'courses/tests/add-test.html', context)
# partial that contains only a form to add a new question
def add_question_form(request):
#form = QuestionForm()
context = {
'form': QuestionForm()
}
return render(request, 'courses/tests/partials/add-question-form.html', context)
def create_question(request, pk):
test = Test.objects.get(pk=pk)
questions = Question.objects.filter(test=test)
form = QuestionForm(request.POST or None)
if request.method == 'POST':
if form.is_valid():
question = form.save(commit=False)
question.test = test
question.save()
return redirect('dashboard:question-detail', pk=question.id)
else:
return render(request, 'courses/tests/partials/add-question-form.html', {'form':form})
context = {
'form': form,
'test': test,
'questions': questions
}
return render(request, 'courses/tests/create-test.html', context)
def question_detail(request, pk):
question = Question.objects.get(id=pk)
context = {
'question':question,
}
return render(request, 'courses/tests/partials/question_detail.html', context)
def question_delete(request, pk):
question = Question.objects.get(id=pk)
question.delete()
return HttpResponse('')
def question_update(request, pk):
question = Question.objects.get(id=pk)
form = QuestionForm(request.POST or None, instance=question)
if request.method == 'POST':
if form.is_valid():
question = form.save()
return redirect('dashboard:question-detail', pk=question.id)
context = {
'form':form,
'question':question,
}
return render(request, 'courses/tests/partials/add-question-form.html', context)
def test(request, pk):
test = Test.objects.get(id=pk)
context = {
'test':test
}
return render(request, 'courses/tests/take-test.html', context)
Everything is working for now, but i need help on how to make possible for Students to take the test.
I have to say this is not really a quiz or a test, the client wants to create questions on which the Student needs to answer and send that data back to them, so they can evaluate it.
I hope you understand the idea and can help!
Thank you again!
Tomislav