Enhance create_anatomy_questions management command with optional image generation and improved argument handling
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
media/
|
media/
|
||||||
|
mediafiles/
|
||||||
|
|
||||||
__pycache__
|
__pycache__
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from anatomy.models import AnatomyQuestion, Exam, ExamQuestionDetail, QuestionType, Modality, Structure
|
from PIL import Image
|
||||||
|
|
||||||
|
from anatomy.models import AnatomyQuestion, Exam, ExamQuestionDetail, QuestionType, Structure
|
||||||
from generic.models import Modality as GenericModality # type: ignore
|
from generic.models import Modality as GenericModality # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@@ -18,9 +21,13 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
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("--exam-id", type=int, required=False, help="ID of the anatomy.Exam to assign questions to (if omitted a new exam will be created)")
|
||||||
|
parser.add_argument("--exam-name", type=str, default=None, help="Name to use when creating a new exam if --exam-id omitted")
|
||||||
parser.add_argument("--count", type=int, default=1, help="Number of questions to create")
|
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("--image-path", type=str, required=False, help="Path to image file to use for each created question")
|
||||||
|
parser.add_argument("--generate-image", action="store_true", help="Generate a placeholder image automatically if --image-path is not provided or not found")
|
||||||
|
parser.add_argument("--image-size", type=str, default="800x600", help="Generated image size WIDTHxHEIGHT, e.g. 800x600")
|
||||||
|
parser.add_argument("--image-color", type=str, default="#777777", help="Background color for generated image (hex) e.g. #ffffff")
|
||||||
parser.add_argument("--description", type=str, default="Generated question {i}", help="Description template; use {i} for index")
|
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("--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("--modality-id", type=int, default=None, help="Optional Modality id to assign (generic.Modality id)")
|
||||||
@@ -29,9 +36,12 @@ class Command(BaseCommand):
|
|||||||
parser.add_argument("--start-sort", type=int, default=1, help="Starting sort_order for the first question")
|
parser.add_argument("--start-sort", type=int, default=1, help="Starting sort_order for the first question")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
exam_id = options["exam_id"]
|
exam_id = options.get("exam_id")
|
||||||
count = options["count"]
|
count = options["count"]
|
||||||
image_path = options["image_path"]
|
image_path = options.get("image_path")
|
||||||
|
generate_image = options.get("generate_image")
|
||||||
|
image_size_str = options.get("image_size") or "800x600"
|
||||||
|
image_color = options.get("image_color") or "#777777"
|
||||||
description_template = options["description"]
|
description_template = options["description"]
|
||||||
qtype_id = options.get("question_type_id")
|
qtype_id = options.get("question_type_id")
|
||||||
modality_id = options.get("modality_id")
|
modality_id = options.get("modality_id")
|
||||||
@@ -39,13 +49,25 @@ class Command(BaseCommand):
|
|||||||
author_ids: List[int] = options.get("author_id") or []
|
author_ids: List[int] = options.get("author_id") or []
|
||||||
sort = options.get("start_sort") or 1
|
sort = options.get("start_sort") or 1
|
||||||
|
|
||||||
|
exam = None
|
||||||
|
if exam_id:
|
||||||
try:
|
try:
|
||||||
exam = Exam.objects.get(pk=exam_id)
|
exam = Exam.objects.get(pk=exam_id)
|
||||||
except Exam.DoesNotExist:
|
except Exam.DoesNotExist:
|
||||||
raise CommandError(f"Exam with id {exam_id} does not exist")
|
raise CommandError(f"Exam with id {exam_id} does not exist")
|
||||||
|
else:
|
||||||
|
# create new exam
|
||||||
|
exam_name = options.get("exam_name") or f"Auto Anatomy Exam {uuid4().hex[:8]}"
|
||||||
|
exam = Exam(name=exam_name, active=True, open_access=True)
|
||||||
|
exam.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Created exam id={exam.id} name='{exam.name}'"))
|
||||||
|
|
||||||
if not os.path.exists(image_path):
|
# validate image path if provided
|
||||||
|
if image_path and not os.path.exists(image_path):
|
||||||
|
if not generate_image:
|
||||||
raise CommandError(f"Image file not found at path: {image_path}")
|
raise CommandError(f"Image file not found at path: {image_path}")
|
||||||
|
else:
|
||||||
|
image_path = None
|
||||||
|
|
||||||
# Resolve optional foreign keys
|
# Resolve optional foreign keys
|
||||||
qtype = None
|
qtype = None
|
||||||
@@ -82,17 +104,50 @@ class Command(BaseCommand):
|
|||||||
raise CommandError(f"Author user id {uid} not found")
|
raise CommandError(f"Author user id {uid} not found")
|
||||||
authors.append(u)
|
authors.append(u)
|
||||||
|
|
||||||
|
# parse image size
|
||||||
|
try:
|
||||||
|
w, h = image_size_str.lower().split("x")
|
||||||
|
image_size: Tuple[int, int] = (int(w), int(h))
|
||||||
|
except Exception:
|
||||||
|
raise CommandError("Invalid --image-size; expected WIDTHxHEIGHT, e.g. 800x600")
|
||||||
|
|
||||||
created = []
|
created = []
|
||||||
|
|
||||||
for i in range(1, count + 1):
|
for i in range(1, count + 1):
|
||||||
desc = description_template.format(i=i)
|
desc = description_template.format(i=i)
|
||||||
|
|
||||||
# Prepare a unique filename to avoid collisions
|
# determine image source: either provided path or generated image
|
||||||
basename = os.path.basename(image_path)
|
unique_name = f"{uuid4().hex}_image.jpg"
|
||||||
unique_name = f"{uuid4().hex}_{basename}"
|
|
||||||
|
|
||||||
|
if image_path:
|
||||||
|
# use provided file
|
||||||
with open(image_path, "rb") as f:
|
with open(image_path, "rb") as f:
|
||||||
django_file = File(f, name=unique_name)
|
django_file = File(f, name=unique_name)
|
||||||
|
img_file_for_save = django_file
|
||||||
|
# create and save question
|
||||||
|
q = AnatomyQuestion(
|
||||||
|
question_type=qtype,
|
||||||
|
description=desc,
|
||||||
|
answer_help="",
|
||||||
|
)
|
||||||
|
|
||||||
|
if modality:
|
||||||
|
q.modality = modality
|
||||||
|
|
||||||
|
if structure:
|
||||||
|
q.structure = structure
|
||||||
|
|
||||||
|
q.image.save(unique_name, img_file_for_save, save=False)
|
||||||
|
q.save()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# generate an image in memory
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img = Image.new("RGB", image_size, image_color)
|
||||||
|
img.save(buf, format="JPEG")
|
||||||
|
buf.seek(0)
|
||||||
|
django_file = File(buf, name=unique_name)
|
||||||
|
|
||||||
q = AnatomyQuestion(
|
q = AnatomyQuestion(
|
||||||
question_type=qtype,
|
question_type=qtype,
|
||||||
description=desc,
|
description=desc,
|
||||||
@@ -105,7 +160,6 @@ class Command(BaseCommand):
|
|||||||
if structure:
|
if structure:
|
||||||
q.structure = structure
|
q.structure = structure
|
||||||
|
|
||||||
# Assign image then save
|
|
||||||
q.image.save(unique_name, django_file, save=False)
|
q.image.save(unique_name, django_file, save=False)
|
||||||
q.save()
|
q.save()
|
||||||
|
|
||||||
@@ -114,11 +168,9 @@ class Command(BaseCommand):
|
|||||||
q.author.set(authors)
|
q.author.set(authors)
|
||||||
|
|
||||||
# add to exam via the many-to-many through relationship
|
# add to exam via the many-to-many through relationship
|
||||||
# use through_defaults for sort_order when possible
|
|
||||||
try:
|
try:
|
||||||
exam.exam_questions.add(q, through_defaults={"sort_order": sort})
|
exam.exam_questions.add(q, through_defaults={"sort_order": sort})
|
||||||
except TypeError:
|
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)
|
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}"))
|
self.stdout.write(self.style.SUCCESS(f"Created question id={q.id} and added to exam id={exam.id} with sort_order={sort}"))
|
||||||
|
|||||||
Reference in New Issue
Block a user