diff --git a/.gitignore b/.gitignore
index e28ab167..9c336bf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ __pycache__
.vscode/
.DS_Store
rad/settings_local.py
-venv
\ No newline at end of file
+venv
+.venv/
diff --git a/anatomy/admin.py b/anatomy/admin.py
index 11451f79..481e3792 100644
--- a/anatomy/admin.py
+++ b/anatomy/admin.py
@@ -44,6 +44,7 @@ class AnatomyAdmin(VersionAdmin):
#IncorrectAnatomyAnswersInline,
ExamInline,
]
+ view_on_site = True
exclude = [ "image_annotations" ]
diff --git a/anatomy/migrations/0040_alter_exam_exam_mode.py b/anatomy/migrations/0040_alter_exam_exam_mode.py
new file mode 100644
index 00000000..cdef9e6c
--- /dev/null
+++ b/anatomy/migrations/0040_alter_exam_exam_mode.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-06 19:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('anatomy', '0039_answer_proposed'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='exam_mode',
+ field=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)'),
+ ),
+ ]
diff --git a/anatomy/migrations/0041_alter_exam_name.py b/anatomy/migrations/0041_alter_exam_name.py
new file mode 100644
index 00000000..d0bca8fc
--- /dev/null
+++ b/anatomy/migrations/0041_alter_exam_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-09 11:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('anatomy', '0040_alter_exam_exam_mode'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='name',
+ field=models.CharField(help_text='Name of the exam', max_length=200),
+ ),
+ ]
diff --git a/anatomy/models.py b/anatomy/models.py
index 09daeb86..0ccb13a0 100644
--- a/anatomy/models.py
+++ b/anatomy/models.py
@@ -130,7 +130,7 @@ class AnatomyQuestion(models.Model):
def __str__(self):
# Get first answer
return "{} [{}, {}]".format(
- self.GetPrimaryAnswer(), self.modality, self.structure
+ self.get_primary_answer(), self.modality, self.structure
)
# Get associated exams
e = self.exams.all().values_list("name", flat=True)
@@ -146,19 +146,19 @@ class AnatomyQuestion(models.Model):
def get_absolute_url(self):
return reverse("anatomy:question_detail", kwargs={"pk": self.pk})
- def GetPrimaryAnswer(self):
+ def get_primary_answer(self):
if len(self.answers.filter(proposed=False)) > 0:
return self.answers.filter(proposed=False).first().answer
else:
return "None yet..."
- def GetExams(self):
+ def get_exams(self):
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
return exams
- def GetUnmarkedAnswersString(self):
- unmarked_answers = self.GetUnmarkedAnswers()
+ def get_unmarked_answer_string(self):
+ unmarked_answers = self.get_unmarked_answers()
if not unmarked_answers:
return "No answers to mark"
@@ -168,18 +168,18 @@ class AnatomyQuestion(models.Model):
)
)
- def GetUnmarkedAnswers(self):
+ def get_unmarked_answers(self):
user_answers = set(
[i.answer_compare for i in self.cid_user_answers.all()]
)
- unmarked_answers = user_answers - self.GetMarkedAnswers()
+ unmarked_answers = user_answers - self.get_marked_answers()
return unmarked_answers
- def GetUnmarkedAnswerCount(self):
- return len(self.GetUnmarkedAnswers())
+ def get_unmarked_answer_count(self):
+ return len(self.get_unmarked_answers())
- def GetMarkedAnswers(self):
+ def get_marked_answers(self):
return set(
[
i.answer_compare
@@ -205,6 +205,7 @@ class AnatomyQuestion(models.Model):
+@reversion.register
class Answer(models.Model):
question = models.ForeignKey(
AnatomyQuestion, related_name="answers", on_delete=models.CASCADE
@@ -350,7 +351,7 @@ class CidUserAnswer(models.Model):
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(
- exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
+ exam, self.cid, self.question.get_primary_answer(), self.answer
)
def save(self, *args, **kwargs):
diff --git a/anatomy/static/js/anatomy.js b/anatomy/static/js/anatomy.js
index 90ea91a0..c3e4b016 100644
--- a/anatomy/static/js/anatomy.js
+++ b/anatomy/static/js/anatomy.js
@@ -78,6 +78,7 @@ $(document).ready(function () {
var jqxhr = $.get(evt.target.dataset.exam_list_url, function (data) {
console.log(data);
+ $("#exam-options").empty();
data.forEach((obj, n) => {
$("#exam-options").append($(document.createElement('button')).prop({
type: 'button',
@@ -106,6 +107,12 @@ $(document).ready(function () {
$("table input:checked").each((n, el) => {
ids.push(el.value)
})
+
+ // if no question selected
+ if (ids.length < 1) {
+ toastr.info('No question selected.');
+ return
+ }
let post_url = document.getElementById("button-select-add-exam").dataset.exam_json_edit_url;
let type = document.getElementById("button-select-add-exam").dataset.type;
let csrf = document.getElementById("button-select-add-exam").dataset.csrf;
diff --git a/anatomy/templates/anatomy/exam_overview.html b/anatomy/templates/anatomy/exam_overview.html
index 842b3993..44ac9132 100644
--- a/anatomy/templates/anatomy/exam_overview.html
+++ b/anatomy/templates/anatomy/exam_overview.html
@@ -26,12 +26,14 @@
{% for question in questions.all %}
+
+
{{ question.description }}
- {{ question.question_type }}: {{ question.GetPrimaryAnswer }}
+ {{ question.question_type }}: {{ question.get_primary_answer }}
- Modality: {{ question.modality }}, View, Mark
+ Modality: {{ question.modality }}, View, Mark
{% endfor %}
diff --git a/anatomy/templates/anatomy/mark_overview.html b/anatomy/templates/anatomy/mark_overview.html
index ed2619b4..95afc5bd 100644
--- a/anatomy/templates/anatomy/mark_overview.html
+++ b/anatomy/templates/anatomy/mark_overview.html
@@ -12,8 +12,8 @@
diff --git a/anatomy/templates/anatomy/question_detail.html b/anatomy/templates/anatomy/question_detail.html
index 0f49631a..51c23cff 100644
--- a/anatomy/templates/anatomy/question_detail.html
+++ b/anatomy/templates/anatomy/question_detail.html
@@ -29,7 +29,7 @@
Created: {{ question.created_date }}
- {{ question.GetPrimaryAnswer }}
+ {{ question.get_primary_answer }}
{{question.question_type}}
Answers (score): {% for answer in question.answers.all %}
{{ answer }} ({{answer.status}}),
diff --git a/anatomy/views.py b/anatomy/views.py
index 98edc7de..8778bbc9 100644
--- a/anatomy/views.py
+++ b/anatomy/views.py
@@ -333,7 +333,7 @@ def mark(request, pk, sk):
# i.answer.lower() for i in question.incorrect_answers.all()
# ]
- unmarked_user_answers = question.GetUnmarkedAnswers()
+ unmarked_user_answers = question.get_unmarked_answers()
if request.method == "POST":
@@ -576,7 +576,7 @@ def exam_scores_cid_user(request, pk, sk):
answer_score = user_answer.get_answer_score()
- correct_answer = q.GetPrimaryAnswer()
+ correct_answer = q.get_primary_answer()
if not exam.publish_results:
correct_answer = "*****"
diff --git a/generic/models.py b/generic/models.py
index d662be79..af9b5e48 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -53,7 +53,7 @@ class Sign(tagulous.models.TagModel):
class ExamBase(models.Model):
- name = models.CharField(max_length=200)
+ name = models.CharField(max_length=200, help_text="Name of the exam")
#exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
active = models.BooleanField(
@@ -61,7 +61,7 @@ class ExamBase(models.Model):
)
exam_mode = models.BooleanField(
- help_text="If an exam should be taken in exam mode", default=True
+ help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)", default=False
)
publish_results = models.BooleanField(
diff --git a/generic/views.py b/generic/views.py
index 1cb8878f..e6ba3610 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -406,6 +406,9 @@ class ExamViews(View, LoginRequiredMixin):
if self.question_type == "long":
h = {}
for question in exam.exam_questions.all():
+ # Generate json if needed
+ if question.json_creation_time is None:
+ question.get_json(question.pk)
h[question.pk] = question.json_creation_time.isoformat()
obj["multi_question_json"] = h
active_exams["exams"].append( obj )
diff --git a/longs/migrations/0036_alter_exam_exam_mode.py b/longs/migrations/0036_alter_exam_exam_mode.py
new file mode 100644
index 00000000..a7c64a12
--- /dev/null
+++ b/longs/migrations/0036_alter_exam_exam_mode.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-06 19:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('longs', '0035_longseries_open_access'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='exam_mode',
+ field=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)'),
+ ),
+ ]
diff --git a/longs/migrations/0037_alter_exam_time_limit.py b/longs/migrations/0037_alter_exam_time_limit.py
new file mode 100644
index 00000000..ec3ad674
--- /dev/null
+++ b/longs/migrations/0037_alter_exam_time_limit.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-06 19:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('longs', '0036_alter_exam_exam_mode'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='time_limit',
+ field=models.IntegerField(default=4500, help_text='Exam time limit (in seconds). Default is 4500 seconds (75 minutes)'),
+ ),
+ ]
diff --git a/longs/migrations/0038_alter_exam_name.py b/longs/migrations/0038_alter_exam_name.py
new file mode 100644
index 00000000..a6ca76c7
--- /dev/null
+++ b/longs/migrations/0038_alter_exam_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-09 11:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('longs', '0037_alter_exam_time_limit'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='name',
+ field=models.CharField(help_text='Name of the exam', max_length=200),
+ ),
+ ]
diff --git a/longs/models.py b/longs/models.py
index 6312a505..1f030da7 100644
--- a/longs/models.py
+++ b/longs/models.py
@@ -145,12 +145,12 @@ class Long(models.Model):
examinations = [series.get_examination() for series in self.series.all()]
return "{} : {}".format(self.description, ", ".join(examinations))
- def GetExams(self):
+ def get_exams(self):
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
return exams
- def GetImages(self, feedback=False):
+ def get_images(self, feedback=False):
qs = self.images.all()
if not feedback:
@@ -160,16 +160,16 @@ class Long(models.Model):
return images
- def GetImageUrls(self):
+ def get_image_urls(self):
return ",".join(
- ["https://penracourses.org.uk{}".format(i.url) for i in self.GetImages()]
+ ["https://penracourses.org.uk{}".format(i.url) for i in self.get_images()]
)
- def GetUnmarkedAnswerCount(self):
+ def get_unmarked_answer_count(self):
answers = self.cid_user_answers.all()
return len([ans for ans in answers if not ans.is_marked()])
- def GetUnmarkedAnswers(self):
+ def get_unmarked_answers(self):
answers = self.cid_user_answers.all()
return [ans for ans in answers if not ans.is_marked()]
@@ -266,7 +266,7 @@ class Long(models.Model):
# def GetNonFeedbackQuestionImages(self):
- # return self.GetImages()
+ # return self.get_images()
def test_image_validator(file):
@@ -527,7 +527,7 @@ class Exam(ExamBase):
exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
time_limit = models.IntegerField(
- help_text="Exam time limit (in seconds). Default is 4500 secondse (75 minutes)",
+ help_text="Exam time limit (in seconds). Default is 4500 seconds (75 minutes)",
default=4500,
)
@@ -568,7 +568,7 @@ class Exam(ExamBase):
return exam_question
- def get_exam_json(self, based=False):
+ def get_exam_json(self, based=True):
questions = self.exam_questions.all()
exam_questions = defaultdict(dict)
@@ -619,6 +619,7 @@ class Exam(ExamBase):
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
+ "based": based,
#"question_requests": exam_questions,
}
diff --git a/longs/templates/longs/long_display_block.html b/longs/templates/longs/long_display_block.html
index 49771d4b..68f48bbc 100755
--- a/longs/templates/longs/long_display_block.html
+++ b/longs/templates/longs/long_display_block.html
@@ -40,7 +40,7 @@
Mark Scheme:
{{ question.mark_scheme }}
- Exam(s): {{question.GetExams}}
+ Exam(s): {{question.get_exams}}
Notes:
diff --git a/longs/templates/longs/mark_overview.html b/longs/templates/longs/mark_overview.html
index 3c27e55a..da27deb5 100644
--- a/longs/templates/longs/mark_overview.html
+++ b/longs/templates/longs/mark_overview.html
@@ -12,8 +12,8 @@
diff --git a/longs/views.py b/longs/views.py
index ec73369c..90b3cd07 100755
--- a/longs/views.py
+++ b/longs/views.py
@@ -655,7 +655,7 @@ def mark_answer(request, pk, sk, tk):
raise Http404("User answer does not exist")
try:
- unmarked = question.GetUnmarkedAnswers()
+ unmarked = question.get_unmarked_answers()
next_unmarked_id = unmarked[0].cid
if next_unmarked_id == tk:
next_unmarked_id = unmarked[1].cid
diff --git a/physics/migrations/0008_alter_exam_exam_mode.py b/physics/migrations/0008_alter_exam_exam_mode.py
new file mode 100644
index 00000000..4ff89b0f
--- /dev/null
+++ b/physics/migrations/0008_alter_exam_exam_mode.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-06 19:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('physics', '0007_exam_author'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='exam_mode',
+ field=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)'),
+ ),
+ ]
diff --git a/physics/migrations/0009_alter_exam_name.py b/physics/migrations/0009_alter_exam_name.py
new file mode 100644
index 00000000..212cf100
--- /dev/null
+++ b/physics/migrations/0009_alter_exam_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-09 11:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('physics', '0008_alter_exam_exam_mode'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='name',
+ field=models.CharField(help_text='Name of the exam', max_length=200),
+ ),
+ ]
diff --git a/rad/settings.py b/rad/settings.py
index 0830f20a..64f081ee 100644
--- a/rad/settings.py
+++ b/rad/settings.py
@@ -59,9 +59,9 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
- "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
diff --git a/rad/urls.py b/rad/urls.py
index 4d4ca23b..5feee79b 100644
--- a/rad/urls.py
+++ b/rad/urls.py
@@ -37,6 +37,7 @@ router.register(r'longs/exams', long_views.ExamViewSet, basename="long-exams")
router.register(r'rapids/answers', rapid_views.QuestionAnswerViewSet, basename="rapid-question-answers")
router.register(r'rapids/user_answer', rapid_views.UserAnswerViewSet, basename="rapid-question-answers")
router.register(r'rapids', rapid_views.RapidViewSet, basename="rapid")
+#router.register(r'rapids/laterality', rapid_views.RapidLateralityViewSet, basename="rapid")
urlpatterns = [
path("admin/", admin.site.urls, name="admin"),
@@ -60,6 +61,7 @@ urlpatterns = [
path("feedback/note//complete", views.feedback_mark_complete, name="feedback_mark_complete"),
#path("feedback/", views.AddQuestionNote.as_view(), name="feedback"),
path("feedback/answer", views.answer_suggestion_submit, name="answer_suggestion_submit"),
+ path("feedback/answer_submit", views.answer_submit, name="answer_submit"),
path("feedback/answer/confirm", views.answer_suggestion_confirm, name="answer_suggestion_confirm"),
path("feedback/view", views.view_feedback, name="view_feedback"),
diff --git a/rad/views.py b/rad/views.py
index 6936bdc9..2b858214 100644
--- a/rad/views.py
+++ b/rad/views.py
@@ -248,6 +248,37 @@ def answer_suggestion_submit(request):
# postExamAnswers
return JsonResponse({"success": False, "error": "Invalid data"})
+@login_required
+def answer_submit(request):
+ if request.is_ajax and request.method == "POST":
+ qid = request.POST.get("qid")
+ question_type = request.POST.get("question_type")
+ answer_string = request.POST.get("answer")
+ status = request.POST.get("status")
+
+ if str(status) not in "012":
+ return JsonResponse({"success": False, "error": "Invalid status"})
+
+ if question_type == "anatomy":
+ AnswerModel = AnatomyAnswer
+ question = AnatomyQuestion
+ elif question_type == "rapid":
+ AnswerModel = RapidAnswer
+ question = Rapid
+ else:
+ return JsonResponse({"success": False, "error": "Invalid question type"})
+
+ q = get_object_or_404(question, pk=qid)
+ if AnswerModel.objects.filter(answer=answer_string, question=q):
+ return JsonResponse({"success": False, "error": "Answer already exists"})
+
+
+ a = AnswerModel(question=q, answer=answer_string, status=status)
+ a.save()
+ return JsonResponse({"success": True, "error": "Answer added"})
+
+ return JsonResponse({"success": False, "error": "Invalid data"})
+
@login_required
def answer_suggestion_confirm(request):
if request.is_ajax and request.method == "POST":
diff --git a/rapids/admin.py b/rapids/admin.py
index da13665f..87aa9ace 100755
--- a/rapids/admin.py
+++ b/rapids/admin.py
@@ -41,7 +41,8 @@ class RapidAdminForm(ModelForm):
fields = [
"normal", "abnormality", "region", "laterality", "examination",
#"site",
- "sign", "condition", "feedback", "author"
+ #"sign", "condition",
+ "feedback", "author"
]
widgets = {
diff --git a/rapids/migrations/0024_alter_exam_exam_mode.py b/rapids/migrations/0024_alter_exam_exam_mode.py
new file mode 100644
index 00000000..1b502e5b
--- /dev/null
+++ b/rapids/migrations/0024_alter_exam_exam_mode.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2021-07-06 19:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('rapids', '0023_answer_proposed'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='exam',
+ name='exam_mode',
+ field=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)'),
+ ),
+ ]
diff --git a/rapids/migrations/0025_auto_20210709_1135.py b/rapids/migrations/0025_auto_20210709_1135.py
new file mode 100644
index 00000000..9e02078d
--- /dev/null
+++ b/rapids/migrations/0025_auto_20210709_1135.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.2.4 on 2021-07-09 11:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('rapids', '0024_alter_exam_exam_mode'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='rapid',
+ name='condition',
+ ),
+ migrations.RemoveField(
+ model_name='rapid',
+ name='sign',
+ ),
+ migrations.AlterField(
+ model_name='exam',
+ name='name',
+ field=models.CharField(help_text='Name of the exam', max_length=200),
+ ),
+ ]
diff --git a/rapids/models.py b/rapids/models.py
index 3cafa066..246ac752 100644
--- a/rapids/models.py
+++ b/rapids/models.py
@@ -37,6 +37,10 @@ def image_directory_path(instance, filename):
return u"rapids/picture/{0}".format(filename)
+def get_answer_compare(s):
+ s = s.strip().lower()
+ s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
+ return s
class Abnormality(models.Model):
name = models.CharField(max_length=200,
@@ -101,15 +105,9 @@ class Answer(models.Model):
def clean(self):
if self.answer:
- self.answer = self.answer.strip()
-
- s = self.answer.lower()
- s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
-
- self.answer_compare = s
+ self.answer_compare = get_answer_compare(self.answer)
-@reversion.register
class Rapid(models.Model):
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
#image = models.ImageField()
@@ -146,14 +144,14 @@ class Rapid(models.Model):
choices=LATERALITY_CHOICES,
default=NONE,
help_text="Applies to the answer, not the examination")
- condition = tagulous.models.TagField(
- to=Condition,
- blank=True,
- help_text=
- "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
- )
- sign = tagulous.models.TagField(
- to=Sign, blank=True, help_text='Radiological signs in the question')
+ #condition = tagulous.models.TagField(
+ # to=Condition,
+ # blank=True,
+ # help_text=
+ # "Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
+ #)
+ #sign = tagulous.models.TagField(
+ # to=Sign, blank=True, help_text='Radiological signs in the question')
DEFAULT_SITE_ID = 1
#site = models.ManyToManyField(
@@ -216,7 +214,7 @@ class Rapid(models.Model):
lat = self.laterality
return "{} / {} {}".format(exams, lat, n)
- def GetPrimaryAnswer(self):
+ def get_primary_answer(self):
if self.normal:
return "Normal"
elif len(self.answers.filter(proposed=False)) > 0:
@@ -224,13 +222,13 @@ class Rapid(models.Model):
else:
return "None yet..."
- def GetExams(self):
+ def get_exams(self):
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
return exams
- def GetUnmarkedAnswersString(self):
- unmarked_answers = self.GetUnmarkedAnswers()
+ def get_unmarked_answer_string(self):
+ unmarked_answers = self.get_unmarked_answers()
if not unmarked_answers:
return "No answers to mark"
@@ -240,7 +238,7 @@ class Rapid(models.Model):
)
)
- def GetUnmarkedAnswers(self):
+ def get_unmarked_answers(self):
# If normal no answers to mark
if self.normal:
return []
@@ -248,14 +246,22 @@ class Rapid(models.Model):
user_answers = set(
[i.answer_compare for i in self.cid_user_answers.all() if i.normal == False]
)
- unmarked_answers = user_answers - self.GetMarkedAnswers()
+ unmarked_answers = user_answers - self.get_marked_answers()
return unmarked_answers
- def GetUnmarkedAnswerCount(self):
- return len(self.GetUnmarkedAnswers())
+ def get_unmarked_answer_count(self):
+ return len(self.get_unmarked_answers())
- def GetMarkedAnswers(self):
+ def get_compare_answers(self):
+ return set(
+ [
+ i.answer_compare
+ for i in self.answers.filter()
+ ]
+ )
+
+ def get_marked_answers(self):
return set(
[
i.answer_compare
@@ -276,7 +282,7 @@ class Rapid(models.Model):
marked_answers = correct_answers | half_mark_answers | incorrect_answers
return marked_answers
- def GetCorrectUnstrippedAnswers(self):
+ def get_correct_unstripped_answers(self):
return set(
[
str(i)
@@ -285,7 +291,7 @@ class Rapid(models.Model):
]
)
- def GetImages(self, feedback=False):
+ def get_images(self, feedback=False):
qs = self.images.all()
if not feedback:
@@ -295,12 +301,42 @@ class Rapid(models.Model):
return images
- def GetImageUrls(self):
- return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.GetImages()])
+ def get_image_urls(self):
+ return ",".join(["https://penracourses.org.uk{}".format(i.url) for i in self.get_images()])
#def GetNonFeedbackQuestionImages(self):
- #return self.GetImages()
+ #return self.get_images()
+
+ def get_laterality_string(self):
+ if self.laterality == self.NONE:
+ s = ""
+ elif self.laterality == self.BILATERAL:
+ s = "bilateral "
+ elif self.laterality == self.RIGHT:
+ s = "right "
+ elif self.laterality == self.LEFT:
+ s = "left "
+
+ return s
+
+ def get_suggested_answers(self):
+ answers = []
+ for r in self.region.all():
+ laterality = self.get_laterality_string()
+ for a in self.abnormality.all():
+ answers.append("{}{} {}".format(laterality, r.name, a.name))
+ answers.append("{} {}{}".format(a.name, laterality, r.name))
+ answers.append("{} {}".format(r.name, a.name))
+ answers.append("{} {}".format(a.name, r.name))
+
+ compare_answers = self.get_compare_answers()
+
+ new_answers = [a for a in answers if get_answer_compare(a) not in compare_answers]
+
+ return new_answers
+#reversion.register(Rapid, follow=["images"])
+#@reversion.register
class RapidImage(models.Model):
rapid = models.ForeignKey(Rapid,
related_name="images",
@@ -357,7 +393,7 @@ class RapidCreationDefault(models.Model):
def get_absolute_url(self):
return reverse('rapids:rapid_create')
-@reversion.register
+#@reversion.register
class Exam(ExamBase):
app_name = "rapids"
@@ -419,15 +455,15 @@ class Exam(ExamBase):
exam_questions[q.id] = {
"images": images,
- "normal": q.normal,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
"type": "rapid",
}
if not self.exam_mode:
+ exam_questions[q.id]["normal"] = q.normal
exam_questions[q.id]["feedback_images"] = feedback_images
- exam_questions[q.id]["answers"] = list(q.GetCorrectUnstrippedAnswers())
+ exam_questions[q.id]["answers"] = list(q.get_correct_unstripped_answers())
#if feedback_images:
@@ -482,7 +518,7 @@ class CidUserAnswer(models.Model):
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(
- exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
+ exam, self.cid, self.question.get_primary_answer(), self.answer
)
def save(self, *args, **kwargs):
diff --git a/rapids/serializers.py b/rapids/serializers.py
index a528b2a3..785fde5a 100644
--- a/rapids/serializers.py
+++ b/rapids/serializers.py
@@ -29,5 +29,11 @@ class UserAnswerSerializer(serializers.HyperlinkedModelSerializer):
class RapidSerializer(serializers.ModelSerializer):
class Meta:
model = Rapid
- fields = ('id', 'question', 'normal', "abnormality", "region", "examination", "exams", "author", "open_access")
- view_name = "rapid-detail"
\ No newline at end of file
+ fields = ('id', 'question', 'normal', "abnormality", "region", "laterality", "examination", "exams", "author", "open_access")
+ view_name = "rapid-detail"
+
+class RapidLateralitySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Rapid
+ fields = ('id', "laterality")
+ view_name = "rapid-laterality"
\ No newline at end of file
diff --git a/rapids/tables.py b/rapids/tables.py
index a98ba283..c87e852f 100755
--- a/rapids/tables.py
+++ b/rapids/tables.py
@@ -62,6 +62,10 @@ class RapidUserAnswerTable(tables.Table):
delete = tables.LinkColumn(
"rapids:rapid_user_answer_delete", text="Delete", args=[A("pk")], orderable=False
)
+ view = tables.LinkColumn('rapids:rapid_user_answer_view',
+ text='View',
+ args=[A('pk')],
+ orderable=False)
class Meta:
model = CidUserAnswer
template_name = "django_tables2/bootstrap4.html"
diff --git a/rapids/templates/rapids/ciduseranswer_detail.html b/rapids/templates/rapids/ciduseranswer_detail.html
new file mode 100644
index 00000000..f59bb2ed
--- /dev/null
+++ b/rapids/templates/rapids/ciduseranswer_detail.html
@@ -0,0 +1,8 @@
+
+{% extends 'rapids/base.html' %}
+
+{% block content %}
+{{ciduseranswer.question}}
+{{ciduseranswer.normal}}
+{{ciduseranswer.cid}}
+{% endblock content %}
\ No newline at end of file
diff --git a/rapids/templates/rapids/exam_overview.html b/rapids/templates/rapids/exam_overview.html
index 1406608b..d4f1407f 100644
--- a/rapids/templates/rapids/exam_overview.html
+++ b/rapids/templates/rapids/exam_overview.html
@@ -26,18 +26,20 @@
{% for question in questions.all %}
- {% for image in question.GetImages %}
+
+ {% for image in question.get_images %}
{% endfor %}
+
{% if not question.normal %}
Abnormality: {{ question.get_abnormalities }} Region: {{ question.get_regions }}
- {{ question.GetPrimaryAnswer }}
+ {{ question.get_primary_answer }}
{% else %}
Normal
{% endif %}
- Examination: {{ question.get_examinations }}, View, Mark
+ Examination: {{ question.get_examinations }}, View, Mark
{% endfor %}
diff --git a/rapids/templates/rapids/mark.html b/rapids/templates/rapids/mark.html
index 0cf37d5f..b063b23a 100644
--- a/rapids/templates/rapids/mark.html
+++ b/rapids/templates/rapids/mark.html
@@ -13,7 +13,7 @@ Answers will be automatically marked.
Answers marked as normal will be automatically marked.
Region: {{ question.get_regions }}, Abnormalities: {{ question.get_abnormalities }}
{% endif %}
-
diff --git a/rapids/templates/rapids/mark_overview.html b/rapids/templates/rapids/mark_overview.html
index 37849233..f6bc4ed1 100644
--- a/rapids/templates/rapids/mark_overview.html
+++ b/rapids/templates/rapids/mark_overview.html
@@ -12,8 +12,8 @@
diff --git a/rapids/templates/rapids/question_display_block.html b/rapids/templates/rapids/question_display_block.html
index fc84ef9d..1ded5b97 100755
--- a/rapids/templates/rapids/question_display_block.html
+++ b/rapids/templates/rapids/question_display_block.html
@@ -4,22 +4,33 @@
Rapid: {{ question }}
+ Normal: {{ question.normal }}
Region: {{ question.get_regions }}
Examination: {{ question.get_examinations }}
- Laterality: {{ question.laterality }}
+ Laterality: {{ question.laterality }}
+
+
+
+
+
Abnormality: {{ question.get_abnormalities }}
- Images:
+
Images:
{% for image in question.images.all %}
- Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}:
-
+ Image {{ forloop.counter }}{% if image.feedback_image %} [feedback image]{% endif %}:
+
{% endfor %}
Open Access: {{ question.open_access }}
Feedback: {{ question.feedback }}
@@ -28,16 +39,17 @@
Checked by: {% for verified in question.verified.all %} {{verified}}, {% endfor %}
Scrapped: {{ question.scrapped }} (toggle)
-
- Answers (score): {% for answer in question.answers.all %}
-
- {{ answer }} ({{answer.status}}),
-
- {% endfor %}
-
+
+ Answers (score): {% for answer in question.answers.all %}
+
+ {{ answer }} ({{answer.status}}),
+
+ {% endfor %}
+
-
+
{% if view_feedback %}
Notes:
@@ -51,85 +63,266 @@
{% for note in question.anon_notes.all %}
-
- {{ note.created_on }} by {{ note.author }}: {{note.note_type}} / {{ note.note }}
- {% if not note.complete %}
- (Mark complete)
- {% endif %}
+ {{ note.created_on }} by {{ note.author }}: {{note.note_type}} / {{ note.note }}
+ {% if not note.complete %}
+ (Mark complete)
+ {% endif %}
{% endfor %}
{% endif %}
-
-
+
+
+ Suggested answers
+
+
+ {% for ans in question.get_suggested_answers %}
+ - {{ans}}
+ {% endfor %}
+
+
+
+
+ Image viewer
+
+
+
+
+
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/rapids/urls.py b/rapids/urls.py
index 3f123476..44ccbcec 100755
--- a/rapids/urls.py
+++ b/rapids/urls.py
@@ -66,6 +66,7 @@ urlpatterns = [
name="rapid_update",
),
path("/add_note", views.AddNote.as_view(), name="rapid_add_note"),
- path("user_answers/", views.RapidUserAnswerView.as_view(), name="rapid_user_answer_view"),
+ path("user_answers/", views.RapidUserAnswerTableView.as_view(), name="rapid_user_answer_table_view"),
+ path("user_answers/", views.RapidUserAnswerView.as_view(), name="rapid_user_answer_view"),
path("user_answers//delete", views.UserAnswerDelete.as_view(), name="rapid_user_answer_delete"),
]
diff --git a/rapids/views.py b/rapids/views.py
index 9b87b875..5faa5816 100755
--- a/rapids/views.py
+++ b/rapids/views.py
@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.contrib.auth.mixins import LoginRequiredMixin
+from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic import ListView
@@ -28,7 +29,7 @@ from .forms import (
ExaminationForm,
AnswerFormSet,
AnswerUpdateFormSet,
- ExamForm
+ ExamForm,
)
from .models import (
Rapid,
@@ -69,10 +70,16 @@ from rapids.forms import RapidCreationDefaultForm
from rapids.models import RapidCreationDefault
-from rest_framework import viewsets, permissions
+from rest_framework import viewsets, permissions, mixins
from rest_framework.pagination import PageNumberPagination
-from .serializers import ExamSerializer, QuestionAnswerSerializer, RapidSerializer, UserAnswerSerializer
+from .serializers import (
+ ExamSerializer,
+ QuestionAnswerSerializer,
+ RapidLateralitySerializer,
+ RapidSerializer,
+ UserAnswerSerializer,
+)
from generic.mixins import SuperuserRequiredMixin
@@ -89,32 +96,44 @@ class AuthorOrCheckerRequiredMixin(object):
return obj
-
-
@login_required
def question_detail(request, pk):
rapid = get_object_or_404(Rapid, pk=pk)
if not rapid.open_access:
- if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in rapid.author.all():
+ if (
+ not request.user.groups.filter(name="rapid_checker").exists()
+ and request.user not in rapid.author.all()
+ ):
raise PermissionDenied()
# if request.user not in rapid.author.all():
# raise PermissionDenied
view_feedback = False
- if request.user.groups.filter(name="rapid_checker").exists() or request.user.groups.filter(name="feedback_checker").exists() or request.user in rapid.author.all():
+ if (
+ request.user.groups.filter(name="rapid_checker").exists()
+ or request.user.groups.filter(name="feedback_checker").exists()
+ or request.user in rapid.author.all()
+ ):
view_feedback = True
# logging.debug(rapid.rapid_notes.first())
# logging.debug(rapid.subspecialty.first().name.all())
- return render(request, "rapids/question_detail.html", {"question": rapid, "view_feedback" : view_feedback})
+ return render(
+ request,
+ "rapids/question_detail.html",
+ {"question": rapid, "view_feedback": view_feedback},
+ )
@login_required
def rapid_split(request, pk):
rapid = get_object_or_404(Rapid, pk=pk)
- if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in question.author.all():
+ if (
+ not request.user.groups.filter(name="rapid_checker").exists()
+ and request.user not in question.author.all()
+ ):
raise PermissionDenied()
images = rapid.images.all()
@@ -281,7 +300,7 @@ class RapidCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
# Sending user object to the form, to verify which fields to display/remove (depending on group)
def get_form_kwargs(self):
kwargs = super(RapidCreateBase, self).get_form_kwargs()
- kwargs.update({'user': self.request.user})
+ kwargs.update({"user": self.request.user})
return kwargs
def get_context_data(self, **kwargs):
@@ -328,24 +347,24 @@ class RapidCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
# @login_required
class RapidCreate(RapidCreateBase):
- #initial = {"laterality": Rapid.NONE}
+ # initial = {"laterality": Rapid.NONE}
def get_initial(self):
if "pk" in self.kwargs:
initial = super().get_initial()
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
- initial['exams'] = [exam.id]
+ initial["exams"] = [exam.id]
return initial
## There has to be a better way...
- #try:
+ # try:
# s = (i.pk for i in self.request.user.rapid_default.site.all())
# self.initial.update({"site": s})
- #except AttributeError:
+ # except AttributeError:
# pass
- #return self.initial
+ # return self.initial
# fields = '__all__'
# #fields = [ 'condition' ]
@@ -359,7 +378,9 @@ class RapidCreate(RapidCreateBase):
# return super().form_valid(form)
-class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
+class RapidUpdate(
+ RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
+):
model = Rapid
form_class = RapidForm
@@ -369,7 +390,7 @@ class RapidUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixi
# exclude = [ 'created_date', 'published_date' ]
def get_form_kwargs(self):
kwargs = super(RapidUpdate, self).get_form_kwargs()
- kwargs.update({'user': self.request.user})
+ kwargs.update({"user": self.request.user})
return kwargs
def get_context_data(self, **kwargs):
@@ -445,7 +466,10 @@ def rapid_scrap(request, pk):
except Rapid.DoesNotExist:
raise Http404("Rapid does not exist")
- if not request.user.groups.filter(name="rapid_checker").exists() and request.user not in question.author.all():
+ if (
+ not request.user.groups.filter(name="rapid_checker").exists()
+ and request.user not in question.author.all()
+ ):
raise PermissionDenied()
rapid.scrapped = not rapid.scrapped
@@ -578,9 +602,9 @@ def loadJsonAnswer(answer):
posted_answer = answer["ans"]
- # qidn 1 does not hold answer data (but should always arrive prior to 2)
- if answer["qidn"] == "1":
- posted_answer = ""
+ ## qidn 1 does not hold answer data (but should always arrive prior to 2)
+ # if answer["qidn"] == "1":
+ # posted_answer = ""
# Normal answers are just posted with the answer "Normal"
normal = False
@@ -601,7 +625,8 @@ def loadJsonAnswer(answer):
ans = exiting_answers[0]
if answer["qidn"] == "1":
ans.normal = normal
- elif answer["qidn"] == "2":
+ ans.answer = ""
+ elif answer["qidn"] == "2" and not ans.normal:
ans.answer = posted_answer
ans.full_clean()
ans.save()
@@ -643,7 +668,7 @@ def mark(request, pk, sk):
# i.answer.lower() for i in question.incorrect_answers.all()
# ]
- unmarked_user_answers = question.GetUnmarkedAnswers()
+ unmarked_user_answers = question.get_unmarked_answers()
if request.method == "POST":
@@ -878,11 +903,10 @@ def exam_scores_cid_user(request, pk, sk):
# Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first()
-
if not user_answer or user_answer is None:
# skip if no answer
answers_marks.append(0)
- #answers.append("")
+ # answers.append("")
answer_score = 0
ans = "Not answered"
else:
@@ -892,7 +916,7 @@ def exam_scores_cid_user(request, pk, sk):
ans = user_answer.answer
answer_score = user_answer.get_answer_score()
- correct_answer = q.GetPrimaryAnswer()
+ correct_answer = q.get_primary_answer()
if not exam.publish_results:
correct_answer = "*****"
@@ -931,6 +955,7 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
model = Rapid
success_url = reverse_lazy("rapids:rapid_view")
+
RapidExamViews = ExamViews(Exam, Rapid, "rapids", "rapid", loadJsonAnswer)
@@ -945,7 +970,10 @@ class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
form.instance.author.add(self.request.user.id)
return super().form_valid(form)
-class ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
+
+class ExamUpdate(
+ RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
+):
model = Exam
form_class = ExamForm
@@ -956,28 +984,38 @@ class ExamUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin
form.instance.author.add(self.request.user.id)
return super().form_valid(form)
-class RapidUserAnswerView(LoginRequiredMixin, SingleTableMixin, FilterView):
+
+class RapidUserAnswerView(LoginRequiredMixin, DetailView):
+ model = CidUserAnswer
+
+
+class RapidUserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = CidUserAnswer
table_class = RapidUserAnswerTable
template_name = "rapids/rapid_question_view.html"
filterset_class = RapidUserAnswerFilter
+
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer
- success_url = reverse_lazy("rapids:rapid_user_answer_view")
+ success_url = reverse_lazy("rapids:rapid_user_answer_table_view")
+
class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000
- page_size_query_param = 'page_size'
+ page_size_query_param = "page_size"
max_page_size = 10000
+
class StandardResultsSetPagination(PageNumberPagination):
page_size = 100
- page_size_query_param = 'page_size'
+ page_size_query_param = "page_size"
max_page_size = 1000
+
+
class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
- #queryset = Exam.objects.all().order_by('name')
+ # queryset = Exam.objects.all().order_by('name')
serializer_class = ExamSerializer
def get_queryset(self):
@@ -987,35 +1025,62 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
user = self.request.user
if user.groups.filter(name="rapid_checker").exists():
return Exam.objects.all()
-
+
return Exam.objects.filter(author__id=user.id)
+
class QuestionAnswerViewSet(viewsets.ModelViewSet):
- queryset = Answer.objects.all()#.order_by('name')
+ queryset = Answer.objects.all() # .order_by('name')
serializer_class = QuestionAnswerSerializer
permission_classes = [permissions.IsAuthenticated]
+
class UserAnswerViewSet(viewsets.ModelViewSet):
- queryset = CidUserAnswer.objects.all()#.order_by('name')
+ queryset = CidUserAnswer.objects.all() # .order_by('name')
serializer_class = UserAnswerSerializer
permission_classes = [permissions.IsAuthenticated]
-class RapidViewSet(RevisionMixin, viewsets.ModelViewSet):
- queryset = Rapid.objects.all()#.order_by('name')
+class RapidViewSet(
+ RevisionMixin,
+ mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.ListModelMixin,
+ mixins.DestroyModelMixin,
+ viewsets.GenericViewSet,
+):
+ queryset = Rapid.objects.all() # .order_by('name')
serializer_class = RapidSerializer
permission_classes = [permissions.IsAuthenticated]
pagination_class = StandardResultsSetPagination
+
+class RapidLateralityViewSet(
+ RevisionMixin,
+ mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.ListModelMixin,
+ viewsets.GenericViewSet,
+):
+ queryset = Rapid.objects.all() # .order_by('name')
+ serializer_class = RapidLateralitySerializer
+ permission_classes = [permissions.IsAuthenticated]
+ pagination_class = StandardResultsSetPagination
+
+
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam
success_url = reverse_lazy("rapids:index")
-#class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
+
+
+# class AddSuggestedAnswer(LoginRequiredMixin, CreateView):
# model = QuestionNote
# form_class = QuestionNoteForm
#
# #fields = ("author", "note_type", "note")
-#
+#
#
# def get_form_kwargs(self):
# kwargs = super(AddQuestionNote, self).get_form_kwargs()
@@ -1037,7 +1102,7 @@ class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
# content_type = ContentType.objects.get(model="long")
# else:
# raise PermissionError()
-#
+#
# get_object_or_404(question, pk=pk)
# return { "content_type" : content_type,
-# "object_id" : pk }
\ No newline at end of file
+# "object_id" : pk }