diff --git a/anatomy/migrations/0026_add_primary_answer.py b/anatomy/migrations/0026_add_primary_answer.py new file mode 100644 index 00000000..4240161b --- /dev/null +++ b/anatomy/migrations/0026_add_primary_answer.py @@ -0,0 +1,43 @@ +"""Add primary_answer FK to AnatomyQuestion and backfill existing values. + +This migration adds a nullable FK `primary_answer` on AnatomyQuestion +pointing to Answer, then backfills it using the current logic of +selecting the first non-proposed CORRECT answer (status '2'). +""" +from django.db import migrations, models +import django.db.models.deletion + + +def forwards(apps, schema_editor): + AnatomyQuestion = apps.get_model('anatomy', 'AnatomyQuestion') + Answer = apps.get_model('anatomy', 'Answer') + + # Select first non-proposed CORRECT answer (status '2') per question + for q in AnatomyQuestion.objects.all(): + ans = Answer.objects.filter(question_id=q.pk, proposed=False, status='2').first() + if ans: + q.primary_answer_id = ans.pk + q.save(update_fields=['primary_answer']) + + +def backwards(apps, schema_editor): + AnatomyQuestion = apps.get_model('anatomy', 'AnatomyQuestion') + for q in AnatomyQuestion.objects.all(): + q.primary_answer = None + q.save(update_fields=['primary_answer']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('anatomy', '0025_exam_created_exam_updated'), + ] + + operations = [ + migrations.AddField( + model_name='anatomyquestion', + name='primary_answer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_for_questions', to='anatomy.answer'), + ), + migrations.RunPython(forwards, backwards), + ] diff --git a/anatomy/models.py b/anatomy/models.py index 79c21472..026fcd3e 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -127,6 +127,15 @@ class AnatomyQuestion(QuestionBase): related_name="anatomy_authored_questions", ) + primary_answer = models.ForeignKey( + 'Answer', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='primary_for_questions', + help_text="Optional explicit primary answer for this question", + ) + class Meta: permissions = () @@ -151,33 +160,32 @@ class AnatomyQuestion(QuestionBase): def get_absolute_url(self): return reverse("anatomy:question_detail", kwargs={"pk": self.pk}) - def get_primary_answer(self): - if hasattr(self, "prefetched_primary_answer") and self.prefetched_primary_answer: - ans = self.prefetched_primary_answer[0] - else: - ans = self.answers.filter( - proposed=False, status=Answer.MarkOptions.CORRECT - ).first() - # ans = self.answers.first() + def get_primary_answer_obj(self): + """Return the Answer instance considered the primary answer, or None. + Preference order: + - explicit `primary_answer` FK if set + - `prefetched_primary_answer` if provided by a queryset + - first non-proposed Answer with status CORRECT + """ + if getattr(self, "primary_answer", None): + return self.primary_answer + + if hasattr(self, "prefetched_primary_answer") and self.prefetched_primary_answer: + return self.prefetched_primary_answer[0] + + return self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).first() + + def get_primary_answer(self): + """Return the primary answer text (string) for templates and display. + + Keeps backward compatibility by returning the answer string when + available, or "None yet..." if no primary answer is defined. + """ + ans = self.get_primary_answer_obj() if ans is None: return "None yet..." - else: - return ans.answer - - if ( - self.answers.filter( - proposed=False, status=Answer.MarkOptions.CORRECT - ).count() - > 0 - ): - return ( - self.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT) - .first() - .answer - ) - else: - return "None yet..." + return ans.answer def get_exams(self): e = self.exams.all().values_list("name", flat=True)