forked from hacktoolkit/django-htk
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add abstract Assessment models (hacktoolkit#425)
Add abstract models for Assessment.
- Loading branch information
1 parent
e4ee2b1
commit 2b5aac2
Showing
4 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Python Standard Library Imports | ||
from enum import Enum | ||
|
||
|
||
class QuestionType(Enum): | ||
UNSPECIFIED = 0 | ||
FREE_RESPONSE = 1 | ||
MULTIPLE_CHOICE = 2 | ||
YES_OR_NO = 3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# Django Imports | ||
from django.conf import settings | ||
from django.db import models | ||
|
||
# HTK Imports | ||
from htk.apps.assessments.enums import QuestionType | ||
from htk.apps.assessments.utils import build_question_type_choices | ||
from htk.utils import htk_setting | ||
|
||
|
||
class AbstractAssessment(models.Model): | ||
"""Abstract model for Assessments app to extend from. | ||
Possible use cases include: | ||
- Quizzes and Exams | ||
- Surveys | ||
""" | ||
|
||
name = models.CharField(max_length=255, blank=True) | ||
# Tracks the version of the assessment | ||
version = models.IntegerField(default=1) | ||
# Number of attempts allowed: 0 for unlimited attempts, 1 for only 1. | ||
num_allowed_attempts = models.PositiveIntegerField(default=0) | ||
# When `True` and `num_allowed_attempts` == 1, previous responses are overwritten | ||
is_repeat_allowed = models.BooleanField(default=False) | ||
# Optional "Exit message" to display when an incorrect answer is provided | ||
knockout_message = models.TextField(blank=True, null=True, max_length=512) | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
def __str__(self): | ||
return f"{self.name} (Version {self.version})" | ||
|
||
|
||
class AbstractAssessmentQuestion(models.Model): | ||
"""Represents one question in an Assessment.""" | ||
|
||
ASSESSMENT_MODEL = htk_setting('HTK_ASSESSMENT_MODEL') | ||
|
||
assessment = models.ForeignKey( | ||
ASSESSMENT_MODEL, related_name='questions', on_delete=models.CASCADE | ||
) | ||
text = models.TextField(max_length=256) | ||
question_type = models.PositiveSmallIntegerField( | ||
default=QuestionType.UNSPECIFIED.value, | ||
choices=build_question_type_choices(), | ||
) | ||
# Specifies the order of questions within an assessment | ||
order = models.IntegerField(default=0) | ||
# Must be `True` to allow empty text responses or "No Entry" for multiple choice | ||
is_optional = models.BooleanField(default=False) | ||
# Indicates if this is a knockout question | ||
is_knockout = models.BooleanField(default=False) | ||
# Optional "Exit message" to display when an incorrect answer is provided, | ||
# overrides `AbstractAssessment.knockout_message` | ||
knockout_message = models.TextField(blank=True, null=True, max_length=512) | ||
|
||
class Meta: | ||
abstract = True | ||
ordering = ['order'] | ||
|
||
def __str__(self): | ||
return self.text | ||
|
||
@property | ||
def knockout_message_text(self): | ||
return self.knockout_message or self.assessment.knockout_message | ||
|
||
|
||
class AbstractAssessmentQuestionAnswerOption(models.Model): | ||
"""Represents the options for a multiple-choice question.""" | ||
|
||
ASSESSMENT_QUESTION_MODEL = htk_setting('HTK_ASSESSMENT_QUESTION_MODEL') | ||
|
||
question = models.ForeignKey( | ||
ASSESSMENT_QUESTION_MODEL, | ||
related_name='answer_options', | ||
on_delete=models.CASCADE, | ||
) | ||
text = models.CharField(max_length=255) | ||
# Indicates if this choice is the correct answer | ||
is_correct = models.BooleanField(default=False) | ||
# Optional color for controlling the UI | ||
# This is a flexible field and can store any value up to the character limit. | ||
# As a suggestion, it can store either `None`, or one of the following: | ||
# - Color name (e.g.`'red'`, `'yellow'`, `'green'`) | ||
# - Hex color code (e.g. `#ff0000`) | ||
# - RGBA (e.g. `rgba(255, 0, 0, 0.25)`) | ||
color = models.CharField(max_length=25, blank=True) | ||
# Tracks the order of options | ||
order = models.IntegerField(default=0) | ||
|
||
class Meta: | ||
abstract = True | ||
ordering = ['order'] | ||
|
||
def __str__(self): | ||
return self.text | ||
|
||
|
||
class AbstractAssessmentAttempt(models.Model): | ||
ASSESSMENT_MODEL = htk_setting('HTK_ASSESSMENT_MODEL') | ||
|
||
user = models.ForeignKey( | ||
settings.AUTH_USER_MODEL, | ||
related_name='assessment_attempts', | ||
on_delete=models.CASCADE, | ||
) | ||
assessment = models.ForeignKey( | ||
ASSESSMENT_MODEL, related_name='attempts', on_delete=models.CASCADE | ||
) | ||
# Indicates if the attempt has been completed | ||
is_completed = models.BooleanField(default=False) | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
def __str__(self): | ||
return f'{self.user.username} - {self.assessment.name}' | ||
|
||
|
||
class AbstractAssessmentAnswer(models.Model): | ||
ASSESSMENT_ATTEMPT_MODEL = htk_setting('HTK_ASSESSMENT_ATTEMPT_MODEL') | ||
ASSESSMENT_QUESTION_MODEL = htk_setting('HTK_ASSESSMENT_QUESTION_MODEL') | ||
ASSESSMENT_QUESTION_ANSWER_OPTION_MODEL = htk_setting( | ||
'HTK_ASSESSMENT_QUESTION_ANSWER_OPTION_MODEL' | ||
) | ||
|
||
attempt = models.ForeignKey( | ||
ASSESSMENT_ATTEMPT_MODEL, | ||
related_name='answers', | ||
on_delete=models.CASCADE, | ||
) | ||
question = models.ForeignKey( | ||
ASSESSMENT_QUESTION_MODEL, | ||
related_name='answers', | ||
on_delete=models.CASCADE, | ||
) | ||
option = models.ForeignKey( | ||
ASSESSMENT_QUESTION_ANSWER_OPTION_MODEL, | ||
related_name='selected_options', | ||
blank=True, | ||
null=True, | ||
on_delete=models.CASCADE, | ||
) # For MC and Y/N | ||
free_response_text = models.TextField(blank=True, null=True) | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
def __str__(self): | ||
if self.question.question_type == QuestionType.FREE_RESPONSE.value: | ||
result = self.user_text | ||
else: # MC or Y/N | ||
result = str(self.option) if self.option else 'No answer' | ||
|
||
return result | ||
|
||
@property | ||
def is_correct(self): | ||
if self.question.question_type in [ | ||
QuestionType.MULTIPLE_CHOICE.value, | ||
QuestionType.YES_OR_NO.value, | ||
]: | ||
result = self.option and self.option.is_correct | ||
else: | ||
# Free response grading can be subjective and might need manual review | ||
result = None | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# HTK Imports | ||
from htk.apps.assessments.enums import QuestionType | ||
from htk.utils.enums import get_enum_symbolic_name | ||
|
||
|
||
def build_question_type_choices(): | ||
choices = [ | ||
( | ||
type.value, | ||
get_enum_symbolic_name(type), | ||
) | ||
for type in QuestionType | ||
] | ||
return choices |