diff --git a/rad/settings.py b/rad/settings.py index cb786ff2..fb14fa73 100644 --- a/rad/settings.py +++ b/rad/settings.py @@ -35,6 +35,7 @@ INSTALLED_APPS = [ 'physics', 'rapids', 'longs', + 'sbas', 'wally', "reversion", "django_tables2", diff --git a/sbas/__init__.py b/sbas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sbas/admin.py b/sbas/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/sbas/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sbas/apps.py b/sbas/apps.py new file mode 100644 index 00000000..5fd02543 --- /dev/null +++ b/sbas/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SbaConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'sbas' diff --git a/sbas/migrations/0001_initial.py b/sbas/migrations/0001_initial.py new file mode 100644 index 00000000..814a6b65 --- /dev/null +++ b/sbas/migrations/0001_initial.py @@ -0,0 +1,77 @@ +# Generated by Django 3.2.5 on 2021-07-28 18:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import sortedm2m.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stem', models.TextField(help_text='Stem of the question')), + ('a_answer', models.TextField(default=True, help_text='Answer text')), + ('a_feedback', models.TextField(blank=True, help_text='Feedback for answer')), + ('b_answer', models.TextField(default=True, help_text='Answer text')), + ('b_feedback', models.TextField(blank=True, help_text='Feedback for answer')), + ('c_answer', models.TextField(default=True, help_text='Answer text')), + ('c_feedback', models.TextField(blank=True, help_text='Feedback for answer')), + ('d_answer', models.TextField(default=True, help_text='Answer text')), + ('d_feedback', models.TextField(blank=True, help_text='Feedback for answer')), + ('e_answer', models.TextField(default=True, help_text='Answer text')), + ('e_feedback', models.TextField(blank=True, help_text='Feedback for answer')), + ('feedback', models.TextField(blank=True, help_text='General question feedback')), + ('best_answer', models.CharField(choices=[('a', 'answer_a'), ('b', 'answer_b'), ('c', 'answer_c'), ('d', 'answer_d'), ('e', 'answer_e')], help_text='The correct answer', max_length=20)), + ('created_date', models.DateTimeField(default=django.utils.timezone.now)), + ('author', models.ManyToManyField(blank=True, help_text='Author(s) of question', related_name='sbas_authored_questions', to=settings.AUTH_USER_MODEL)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sbas.category')), + ], + ), + migrations.CreateModel( + name='Exam', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Name of the exam', max_length=200)), + ('active', models.BooleanField(default=False, help_text='If an exam should be available')), + ('exam_mode', models.BooleanField(default=False, help_text='If an exam should be taken in exam mode (users results will be submited to the server for marking)')), + ('publish_results', models.BooleanField(default=False, help_text='If an exams results should be available')), + ('json_creation_time', models.DateTimeField(blank=True, default=None, null=True)), + ('time_limit', models.IntegerField(blank=True, help_text='Exam time limit (in seconds)', null=True)), + ('author', models.ManyToManyField(blank=True, help_text='Author of exam', related_name='sba_exam_author', to=settings.AUTH_USER_MODEL)), + ('exam_questions', sortedm2m.fields.SortedManyToManyField(blank='true', help_text=None, related_name='exams', to='sbas.Question')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CidUserAnswer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('answer', models.CharField(choices=[('a', 'answer_a'), ('b', 'answer_b'), ('c', 'answer_c'), ('d', 'answer_d'), ('e', 'answer_e')], help_text='The correct answer', max_length=20)), + ('cid', models.IntegerField(blank=True, null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('exam', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cid_user_answers', to='sbas.exam')), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cid_user_answers', to='sbas.question')), + ], + ), + ] diff --git a/sbas/migrations/__init__.py b/sbas/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sbas/models.py b/sbas/models.py new file mode 100644 index 00000000..ab26fc8a --- /dev/null +++ b/sbas/models.py @@ -0,0 +1,184 @@ +from django.db import models +from django.utils import timezone + +from django.core.files.storage import FileSystemStorage +from django.conf import settings +from django.utils.html import format_html + +from django.urls import reverse + +from django.utils.translation import ugettext_lazy as _ + +from sortedm2m.fields import SortedManyToManyField + +from generic.models import ExamBase + +import reversion + +class Category(models.Model): + category = models.CharField(max_length=200) + + def __str__(self): + return self.category + +class Question(models.Model): + stem = models.TextField( + blank=False, + help_text="Stem of the question", + ) + + answer_help_text = "Answer text" + + BEST_ANSWER_CHOICES = ( + ('a', 'answer_a'), + ('b', 'answer_b'), + ('c', 'answer_c'), + ('d', 'answer_d'), + ('e', 'answer_e'), + ) + + # I doubt this is the best way to structure the model + # (but it should work) + a_answer = models.TextField( + help_text=answer_help_text, default=True + ) + a_feedback = models.TextField( + blank=True, + help_text="Feedback for answer", + ) + + b_answer = models.TextField( + help_text=answer_help_text, default=True + ) + b_feedback = models.TextField( + blank=True, + help_text="Feedback for answer", + ) + + c_answer = models.TextField( + help_text=answer_help_text, default=True + ) + c_feedback = models.TextField( + blank=True, + help_text="Feedback for answer", + ) + + d_answer = models.TextField( + help_text=answer_help_text, default=True + ) + d_feedback = models.TextField( + blank=True, + help_text="Feedback for answer", + ) + + e_answer = models.TextField( + help_text=answer_help_text, default=True + ) + e_feedback = models.TextField( + blank=True, + help_text="Feedback for answer", + ) + + feedback = models.TextField( + blank=True, + help_text="General question feedback", + ) + + best_answer = models.CharField(max_length=20, choices=BEST_ANSWER_CHOICES, help_text="The correct answer") + + created_date = models.DateTimeField(default=timezone.now) + + author = models.ManyToManyField( + settings.AUTH_USER_MODEL, + blank=True, + help_text="Author(s) of question", + related_name="sbas_authored_questions", + ) + + category = models.ForeignKey( + Category, on_delete=models.SET_NULL, null=True, blank=True + ) + + def __str__(self): + return self.stem + + def clean(self): + if self.a_answer: + self.a_answer = self.a_answer.strip() + if self.b_answer: + self.b_answer = self.b_answer.strip() + if self.c_answer: + self.c_answer = self.c_answer.strip() + if self.d_answer: + self.d_answer = self.d_answer.strip() + if self.e_answer: + self.e_answer = self.e_answer.strip() + + def get_absolute_url(self): + return reverse("sbas:question_detail", kwargs={"pk": self.pk}) + + def get_answers(self): + return (self.a_answer, self.b_answer, self.c_answer, self.d_answer, self.e_answer) + + def get_correct_answer(self): + if self.best_answer == "a": return self.a_answer + if self.best_answer == "b": return self.b_answer + if self.best_answer == "c": return self.c_answer + if self.best_answer == "d": return self.d_answer + if self.best_answer == "e": return self.e_answer + + +@reversion.register +class Exam(ExamBase): + app_name = "sbas" + + exam_questions = SortedManyToManyField( + Question, related_name="exams", blank="true" + ) + + time_limit = models.IntegerField( + help_text="Exam time limit (in seconds)", + blank=True, + null=True, + ) + + recreate_json = None + + author = models.ManyToManyField(settings.AUTH_USER_MODEL, + blank=True, + help_text='Author of exam', + related_name="sba_exam_author") + + + +class CidUserAnswer(models.Model): + """User answers by candidate""" + + question = models.ForeignKey( + Question, related_name="cid_user_answers", on_delete=models.CASCADE + ) + answer = models.CharField(max_length=20, choices=Question.BEST_ANSWER_CHOICES, help_text="The correct answer") + + cid = models.IntegerField(blank=True, null=True) + + # Each user answer is associated with a particular exam + exam = models.ForeignKey( + Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True + ) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return "{}/{}/{}".format(self.cid, self.exam, self.question.pk) + + def get_answer(self): + return self.answer + + def get_correct(self): + return self.answer == self.question.best_answer + + def get_score(self): + if self.get_correct: + return 1 + return 0 \ No newline at end of file diff --git a/sbas/templates/sbas/exam_take.html b/sbas/templates/sbas/exam_take.html new file mode 100755 index 00000000..45630ba3 --- /dev/null +++ b/sbas/templates/sbas/exam_take.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block content %} + +