From e39539fb99dc381b31c09f23bf1664aa94705ecf Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 10 Nov 2025 22:24:44 +0000 Subject: [PATCH] Add management commands for creating anatomy questions with customizable options --- anatomy/management/__init__.py | 1 + anatomy/management/commands/__init__.py | 1 + .../commands/create_anatomy_questions.py | 128 ++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 anatomy/management/__init__.py create mode 100644 anatomy/management/commands/__init__.py create mode 100644 anatomy/management/commands/create_anatomy_questions.py diff --git a/anatomy/management/__init__.py b/anatomy/management/__init__.py new file mode 100644 index 00000000..e826feb9 --- /dev/null +++ b/anatomy/management/__init__.py @@ -0,0 +1 @@ +"""Management package for anatomy app.""" diff --git a/anatomy/management/commands/__init__.py b/anatomy/management/commands/__init__.py new file mode 100644 index 00000000..2cdb518f --- /dev/null +++ b/anatomy/management/commands/__init__.py @@ -0,0 +1 @@ +"""Management commands package for anatomy app.""" diff --git a/anatomy/management/commands/create_anatomy_questions.py b/anatomy/management/commands/create_anatomy_questions.py new file mode 100644 index 00000000..208337e2 --- /dev/null +++ b/anatomy/management/commands/create_anatomy_questions.py @@ -0,0 +1,128 @@ +from __future__ import annotations +import os +from uuid import uuid4 +from typing import List + +from django.core.management.base import BaseCommand, CommandError +from django.core.files import File +from django.contrib.auth import get_user_model + +from anatomy.models import AnatomyQuestion, Exam, ExamQuestionDetail, QuestionType, Modality, Structure +from generic.models import Modality as GenericModality # type: ignore + + +class Command(BaseCommand): + help = ( + "Create anatomy questions and assign them to an exam." + "Requires --exam-id and --image-path (path readable inside the container)." + ) + + def add_arguments(self, parser): + parser.add_argument("--exam-id", type=int, required=True, help="ID of the anatomy.Exam to assign questions to") + parser.add_argument("--count", type=int, default=1, help="Number of questions to create") + parser.add_argument("--image-path", type=str, required=True, help="Path to image file to use for each created question") + parser.add_argument("--description", type=str, default="Generated question {i}", help="Description template; use {i} for index") + parser.add_argument("--question-type-id", type=int, default=None, help="Optional QuestionType id to assign") + parser.add_argument("--modality-id", type=int, default=None, help="Optional Modality id to assign (generic.Modality id)") + parser.add_argument("--structure-id", type=int, default=None, help="Optional Structure id to assign") + parser.add_argument("--author-id", type=int, action="append", help="Author user id; repeatable") + parser.add_argument("--start-sort", type=int, default=1, help="Starting sort_order for the first question") + + def handle(self, *args, **options): + exam_id = options["exam_id"] + count = options["count"] + image_path = options["image_path"] + description_template = options["description"] + qtype_id = options.get("question_type_id") + modality_id = options.get("modality_id") + structure_id = options.get("structure_id") + author_ids: List[int] = options.get("author_id") or [] + sort = options.get("start_sort") or 1 + + try: + exam = Exam.objects.get(pk=exam_id) + except Exam.DoesNotExist: + raise CommandError(f"Exam with id {exam_id} does not exist") + + if not os.path.exists(image_path): + raise CommandError(f"Image file not found at path: {image_path}") + + # Resolve optional foreign keys + qtype = None + if qtype_id: + qtype = QuestionType.objects.filter(pk=qtype_id).first() + if not qtype: + raise CommandError(f"QuestionType id {qtype_id} not found") + + modality = None + if modality_id: + # The project stores Modality in generic.models.Modality + try: + from generic.models import Modality as GModality + + modality = GModality.objects.filter(pk=modality_id).first() + if not modality: + raise CommandError(f"Modality id {modality_id} not found in generic.Modality") + except Exception as e: + raise CommandError(str(e)) + + structure = None + if structure_id: + from atlas.models import Structure as AtlasStructure + + structure = AtlasStructure.objects.filter(pk=structure_id).first() + if not structure: + raise CommandError(f"Structure id {structure_id} not found") + + User = get_user_model() + authors = [] + for uid in author_ids: + u = User.objects.filter(pk=uid).first() + if not u: + raise CommandError(f"Author user id {uid} not found") + authors.append(u) + + created = [] + + for i in range(1, count + 1): + desc = description_template.format(i=i) + + # Prepare a unique filename to avoid collisions + basename = os.path.basename(image_path) + unique_name = f"{uuid4().hex}_{basename}" + + with open(image_path, "rb") as f: + django_file = File(f, name=unique_name) + q = AnatomyQuestion( + question_type=qtype, + description=desc, + answer_help="", + ) + + if modality: + q.modality = modality + + if structure: + q.structure = structure + + # Assign image then save + q.image.save(unique_name, django_file, save=False) + q.save() + + # authors + if authors: + q.author.set(authors) + + # add to exam via the many-to-many through relationship + # use through_defaults for sort_order when possible + try: + exam.exam_questions.add(q, through_defaults={"sort_order": sort}) + except TypeError: + # older Django or in case add doesn't support through_defaults, create through model + ExamQuestionDetail.objects.create(exam=exam, anatomyquestion=q, sort_order=sort) + + self.stdout.write(self.style.SUCCESS(f"Created question id={q.id} and added to exam id={exam.id} with sort_order={sort}")) + created.append(q) + sort += 1 + + self.stdout.write(self.style.SUCCESS(f"Finished: created {len(created)} question(s)"))