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 %}
  • + thumbail + {{ 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 %} thumbail {% 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 %}
    Exams: {% for exam in question.exams.all %} - {{ exam.name }}, + {{ exam.name }}, {% 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:
    +
    + + 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 }