diff --git a/anatomy/models.py b/anatomy/models.py index f9bc4b56..25bb1af6 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -16,7 +16,7 @@ from sortedm2m.fields import SortedManyToManyField import string -from generic.models import CidUser, CidUserGroup, ExamUserStatus, Examination, ExamBase, QuestionNote, UserAnswerBase, UserUserGroup +from generic.models import CidUser, CidUserGroup, ExamUserStatus, Examination, ExamBase, QuestionBase, QuestionNote, UserAnswerBase, UserUserGroup from collections import defaultdict from helpers.images import image_as_base64 @@ -72,7 +72,7 @@ class QuestionType(models.Model): @reversion.register -class AnatomyQuestion(models.Model): +class AnatomyQuestion(QuestionBase): question_type = models.ForeignKey( QuestionType, on_delete=models.SET_NULL, null=True, default=1 ) @@ -114,11 +114,6 @@ class AnatomyQuestion(models.Model): structure = models.ForeignKey( Structure, on_delete=models.SET_NULL, null=True, blank=True ) - created_date = models.DateTimeField(default=timezone.now) - - open_access = models.BooleanField( - help_text="If a question should be freely available to browse", default=True - ) author = models.ManyToManyField( settings.AUTH_USER_MODEL, @@ -127,8 +122,6 @@ class AnatomyQuestion(models.Model): related_name="anatomy_authored_questions", ) - notes = GenericRelation(QuestionNote) - class Meta: permissions = () diff --git a/generic/models.py b/generic/models.py index bc81e96c..c723c3a9 100644 --- a/generic/models.py +++ b/generic/models.py @@ -24,6 +24,7 @@ from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver + class Plane(models.Model): plane = models.CharField(max_length=200, unique=True) @@ -71,15 +72,29 @@ class QuestionBase(models.Model): default=False, ) + created_date = models.DateTimeField(default=timezone.now) + + open_access = models.BooleanField( + help_text="If a question should be freely available to browse", default=True + ) + + feedback = models.TextField(null=True, blank=True, help_text="Question Feedback") + + notes = GenericRelation("generic.QuestionNote") + class Meta: abstract = True - def get_unanswered_mark_and_text(self) -> tuple(int, str): + def get_unanswered_mark_and_text(self) -> tuple[ int, str ]: """ Override in models if needed """ return (0, "Not answered") + def get_primary_answer(self): + """If this makes sense in the question/answer context override""" + return None + class ExamBase(models.Model): name = models.CharField(max_length=200, help_text="Name of the exam") @@ -436,6 +451,11 @@ class UserAnswerBase(models.Model): def get_absolute_url(self): return reverse(f"{self.app_name}:user_answer_view", kwargs={"pk": self.pk}) + def get_answer(self): + """might need overriding""" + return self.answer + + class NoteType(models.Model): note_type = models.CharField(max_length=200) diff --git a/generic/views.py b/generic/views.py index 90414fd0..b1e45a94 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1803,10 +1803,7 @@ class ExamViews(View, LoginRequiredMixin): answer_score = score ans = text else: - if exam.app_name == "rapids" and user_answer.normal: - ans = "Normal" - else: - ans = user_answer.answer + ans = user_answer.get_answer() answer_score = user_answer.get_answer_score() correct_answer = q.get_primary_answer() diff --git a/longs/models.py b/longs/models.py index 555a43b1..18809b7e 100644 --- a/longs/models.py +++ b/longs/models.py @@ -37,6 +37,7 @@ from generic.models import ( ExamBase, Plane, Contrast, + QuestionBase, QuestionNote, UserAnswerBase, UserUserGroup, @@ -79,7 +80,7 @@ def findMiddle(input_list): @reversion.register -class Long(models.Model): +class Long(QuestionBase): description = models.TextField( blank=True, help_text="Description of the case, for admin organisation, will not be visible when taking", @@ -87,8 +88,6 @@ class Long(models.Model): history = models.TextField(null=True, blank=True) - feedback = models.TextField(null=True, blank=True) - # TODO: merge with atlas condition / signs / finding # condition = tagulous.models.TagField( # to=Condition, @@ -117,8 +116,6 @@ class Long(models.Model): # help_text= # "If we know the source of the image") verified = models.BooleanField(default=False) - created_date = models.DateTimeField(default=timezone.now) - published_date = models.DateTimeField(blank=True, null=True) author = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, @@ -130,10 +127,6 @@ class Long(models.Model): default=False, help_text="Question has been scrapped and will not be shown" ) - open_access = models.BooleanField( - help_text="If a question should be freely available to browse", default=True - ) - recreate_json = models.BooleanField( help_text="If the json cache needs updating", default=False ) @@ -146,8 +139,6 @@ class Long(models.Model): series = SortedManyToManyField("LongSeries", related_name="long") - notes = GenericRelation(QuestionNote) - # question_file = models.FileField(upload_to=question_file_directory_path, blank=True, null=True) def get_absolute_url(self): @@ -354,6 +345,12 @@ class Long(models.Model): return url + def get_unanswered_mark_and_text(self) -> tuple[int, str]: + """ + Override in models if needed + """ + return (4, ("Not answered",) * 5) + # def GetNonFeedbackQuestionImages(self): # return self.get_images() @@ -431,14 +428,12 @@ class LongSeries(models.Model): ) def __str__(self): - #if self.long: + # if self.long: # long_id = ", ".format([long.pk for long in self.long.all()]) # # long_id = self.long.pk - #else: + # else: # long_id = "None" - return "{}/{} : {}".format( - self.pk, self.get_examination(), self.description - ) + return "{}/{} : {}".format(self.pk, self.get_examination(), self.description) def get_author_objects(self): """Returns a comma seperated text list of authors""" @@ -655,6 +650,35 @@ class Exam(ExamBase): exam_user_status = GenericRelation(ExamUserStatus) + def get_exam_question_json(self, question_id): + q = get_object_or_404(Long, pk=question_id) + + # exam_order.append(q.id) + + # Loop through longimage associations + images = [] + image_titles = [] + for series in q.series.all(): + # image_array = [] + # for i in series.images.all(): + # image_array.append(image_as_base64(i.image)) + # #image_array.append(i.image.url) + image_array = [image_as_base64(i.image) for i in series.images.all()] + images.append(image_array) + image_titles.append(series.get_examination()) + + exam_question = { + "title": q.history, + "images": images, + "image_titles": image_titles, + # "feedback_image": [], + # "annotations": [str(q.image_annotations)], + "type": "long", + "images_json": True, + } + + return exam_question + def get_exam_json(self, based=True): questions = self.exam_questions.all() @@ -849,6 +873,15 @@ class UserAnswer(UserAnswerBase): return True return False + def get_answer(self): + ( + self.answer_observations, + self.answer_interpretation, + self.answer_principle_diagnosis, + self.answer_differential_diagnosis, + self.answer_management, + ) + def get_answer_string(self) -> str: return f""" Observations: @@ -868,6 +901,7 @@ Management: """ + @reversion.register class AnswerMarks(models.Model): score = models.CharField(max_length=3, choices=UserAnswer.ScoreOptions.choices) diff --git a/longs/views.py b/longs/views.py index af014048..e8941930 100755 --- a/longs/views.py +++ b/longs/views.py @@ -978,97 +978,97 @@ def mark(request, exam_id, sk): # ) -def exam_scores_cid_user(request, pk, cid, passcode): - exam = get_object_or_404(Exam, pk=pk) - - # TODO:Need some kind of test for cid - - if not exam.exam_mode: - raise Http404("Packet not in exam mode") - - if not exam.check_cid_user(cid, passcode, request): - raise Http404("Error accessing exam") - - - questions = exam.exam_questions.all() - - # answers_and_marks = [] - answers_marks = [] - # answers = [] - answer_text = [] - - view_all_results = False - if request.user.groups.filter(name="view_all_results").exists(): - view_all_results = True - - for q in questions: - # Get user answer - user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() - - if not user_answer or user_answer is None: - # skip if no answer - # answers_marks.append("") - # answers.append("") - answer_score = 4 - # ans = "Not answered" - answer_text.append( - ( - ("Not answered"), - ("Not answered"), - ("Not answered"), - ("Not answered"), - ("Not answered"), - ) - ) - else: - answer_score = user_answer.get_answer_score() - - answer_text.append( - ( - user_answer.answer_observations, - user_answer.answer_interpretation, - user_answer.answer_principle_diagnosis, - user_answer.answer_differential_diagnosis, - user_answer.answer_management, - ) - ) - - if not exam.publish_results and not view_all_results: - answer_score = 0 - # answers.append(ans) - answers_marks.append(answer_score) - # answers_and_marks.append((ans, answer_score, correct_answer)) - - answered = [i for i in answers_marks if i != ""] - if "" in answers_marks: - total_score = sum(answered) - unmarked_number = len(answers_marks) - len(answered) - total_score = "{} ({} unmarked)".format(total_score, unmarked_number) - normalised_score = "Not available" - else: - total_score = sum(answered) - normalised_score = normaliseScore(total_score) - - max_score = len(questions) * 8 - - return render( - request, - "longs/exam_scores_user.html", - { - "exam": exam, - "cid": cid, - "passcode": passcode, - "questions": questions, - # "answers": answers, - "answers_marks": answers_marks, - "total_score": total_score, - "normalised_score": normalised_score, - "max_score": max_score, - "answer_text": answer_text, - # "answers_and_marks": answers_and_marks, - "view_all_results": view_all_results, - }, - ) +#def exam_scores_cid_user(request, pk, cid, passcode): +# exam = get_object_or_404(Exam, pk=pk) +# +# # TODO:Need some kind of test for cid +# +# if not exam.exam_mode: +# raise Http404("Packet not in exam mode") +# +# if not exam.check_cid_user(cid, passcode, request): +# raise Http404("Error accessing exam") +# +# +# questions = exam.exam_questions.all() +# +# # answers_and_marks = [] +# answers_marks = [] +# # answers = [] +# answer_text = [] +# +# view_all_results = False +# if request.user.groups.filter(name="view_all_results").exists(): +# view_all_results = True +# +# for q in questions: +# # Get user answer +# user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() +# +# if not user_answer or user_answer is None: +# # skip if no answer +# # answers_marks.append("") +# # answers.append("") +# answer_score = 4 +# # ans = "Not answered" +# answer_text.append( +# ( +# ("Not answered"), +# ("Not answered"), +# ("Not answered"), +# ("Not answered"), +# ("Not answered"), +# ) +# ) +# else: +# answer_score = user_answer.get_answer_score() +# +# answer_text.append( +# ( +# user_answer.answer_observations, +# user_answer.answer_interpretation, +# user_answer.answer_principle_diagnosis, +# user_answer.answer_differential_diagnosis, +# user_answer.answer_management, +# ) +# ) +# +# if not exam.publish_results and not view_all_results: +# answer_score = 0 +# # answers.append(ans) +# answers_marks.append(answer_score) +# # answers_and_marks.append((ans, answer_score, correct_answer)) +# +# answered = [i for i in answers_marks if i != ""] +# if "" in answers_marks: +# total_score = sum(answered) +# unmarked_number = len(answers_marks) - len(answered) +# total_score = "{} ({} unmarked)".format(total_score, unmarked_number) +# normalised_score = "Not available" +# else: +# total_score = sum(answered) +# normalised_score = normaliseScore(total_score) +# +# max_score = len(questions) * 8 +# +# return render( +# request, +# "longs/exam_scores_user.html", +# { +# "exam": exam, +# "cid": cid, +# "passcode": passcode, +# "questions": questions, +# # "answers": answers, +# "answers_marks": answers_marks, +# "total_score": total_score, +# "normalised_score": normalised_score, +# "max_score": max_score, +# "answer_text": answer_text, +# # "answers_and_marks": answers_and_marks, +# "view_all_results": view_all_results, +# }, +# ) @login_required diff --git a/physics/models.py b/physics/models.py index 6b3fcb9c..df5cccaa 100644 --- a/physics/models.py +++ b/physics/models.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _ from sortedm2m.fields import SortedManyToManyField -from generic.models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, QuestionNote, UserAnswerBase, UserUserGroup +from generic.models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, QuestionBase, QuestionNote, UserAnswerBase, UserUserGroup import reversion @@ -24,7 +24,7 @@ class Category(models.Model): return self.category -class Question(models.Model): +class Question(QuestionBase): stem = models.TextField( blank=False, help_text="Stem of the question", @@ -82,8 +82,6 @@ class Question(models.Model): help_text="Feedback for answer", ) - created_date = models.DateTimeField(default=timezone.now) - author = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, @@ -95,7 +93,7 @@ class Question(models.Model): Category, on_delete=models.SET_NULL, null=True, blank=True ) - notes = GenericRelation(QuestionNote) + #notes = GenericRelation(QuestionNote) def __str__(self): return self.stem diff --git a/rapids/models.py b/rapids/models.py index 03a3feb7..bd4dae2e 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -29,6 +29,7 @@ from generic.models import ( CidUser, CidUserGroup, ExamUserStatus, + QuestionBase, Site, #Condition, #Sign, @@ -148,7 +149,7 @@ class Answer(models.Model): self.answer_compare = get_answer_compare(self.answer) -class Rapid(models.Model): +class Rapid(QuestionBase): """ Django model that defines a rapid question. @@ -160,7 +161,6 @@ class Rapid(models.Model): question = models.TextField(null=True, blank=True) history = models.TextField(null=True, blank=True) - feedback = models.TextField(null=True, blank=True) normal = models.BooleanField(default=False, help_text="Tick if true") @@ -213,8 +213,7 @@ class Rapid(models.Model): # help_text= # "If we know the source of the image") verified = models.BooleanField(default=False) - created_date = models.DateTimeField(default=timezone.now) - published_date = models.DateTimeField(blank=True, null=True) + author = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, @@ -226,12 +225,6 @@ class Rapid(models.Model): default=False, help_text="Question has been scrapped and will not be shown" ) - open_access = models.BooleanField( - help_text="If a question should be freely available to browse", default=True - ) - - notes = GenericRelation(QuestionNote) - def get_absolute_url(self): return reverse("rapids:question_detail", kwargs={"pk": self.pk}) @@ -852,6 +845,9 @@ class UserAnswer(UserAnswerBase): # s = s.translate(str.maketrans('', '', string.punctuation)) # return s + def get_answer(self): + return self.get_answer_string() + def get_answer_string(self): if self.normal: return "Normal" diff --git a/sbas/models.py b/sbas/models.py index c6019d82..23a42d97 100644 --- a/sbas/models.py +++ b/sbas/models.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from sortedm2m.fields import SortedManyToManyField -from generic.models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, QuestionNote, UserAnswerBase, UserUserGroup +from generic.models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, QuestionBase, QuestionNote, UserAnswerBase, UserUserGroup import reversion @@ -25,7 +25,7 @@ class Category(models.Model): return self.category -class Question(models.Model): +class Question(QuestionBase): stem = models.TextField( blank=False, help_text="Stem of the question", @@ -82,8 +82,6 @@ class Question(models.Model): 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, @@ -95,7 +93,7 @@ class Question(models.Model): Category, on_delete=models.SET_NULL, null=True, blank=True ) - notes = GenericRelation(QuestionNote) + #notes = GenericRelation(QuestionNote) def __str__(self): return self.stem