Skip to content

Commit ea97494

Browse files
rtibblesbotclaude
andcommitted
Extract raw-Perseus asset writing into the base generator
Move the raw-Perseus image/graphie writing out of PerseusExerciseGenerator into ExerciseArchiveGenerator._write_raw_perseus_assets, parameterized on the target images directory and returning the package-relative paths it wrote, so the QTI generator can reuse it. Perseus output is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fabd6db commit ea97494

2 files changed

Lines changed: 65 additions & 49 deletions

File tree

contentcuration/contentcuration/utils/assessment/base.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.core.files import File
1414
from django.core.files.storage import default_storage as storage
1515
from le_utils.constants import exercises
16+
from le_utils.constants import format_presets
1617
from PIL import Image
1718

1819
from contentcuration import models
@@ -136,6 +137,67 @@ def add_file_to_write(self, filepath, content):
136137
f.write(content)
137138
self.files_to_write.append(full_path)
138139

140+
def _write_raw_perseus_assets(self, assessment_item, images_dir):
141+
"""Write a raw Perseus item's image and graphie assets into ``images_dir``
142+
and return the sorted list of package-relative paths written.
143+
144+
Graphie files are stored as a single delimited blob and are split into
145+
their ``.svg`` and ``-data.json`` parts here.
146+
"""
147+
# For raw perseus JSON questions, the files must be
148+
# specified in advance.
149+
150+
# Files have been prefetched when the assessment item was
151+
# queried, so take advantage of that.
152+
files = sorted(assessment_item.files.all(), key=lambda x: x.checksum)
153+
image_files = filter(
154+
lambda x: x.preset_id == format_presets.EXERCISE_IMAGE, files
155+
)
156+
graphie_files = filter(
157+
lambda x: x.preset_id == format_presets.EXERCISE_GRAPHIE, files
158+
)
159+
written_paths = []
160+
for image in image_files:
161+
image_name = "{}/{}.{}".format(
162+
images_dir, image.checksum, image.file_format_id
163+
)
164+
with storage.open(
165+
models.generate_object_storage_name(image.checksum, str(image)),
166+
"rb",
167+
) as content:
168+
self.add_file_to_write(image_name, content.read())
169+
written_paths.append(image_name)
170+
171+
for image in graphie_files:
172+
svg_name = "{}/{}.svg".format(images_dir, image.original_filename)
173+
json_name = "{}/{}-data.json".format(images_dir, image.original_filename)
174+
with storage.open(
175+
models.generate_object_storage_name(image.checksum, str(image)),
176+
"rb",
177+
) as content:
178+
content = content.read()
179+
# in Python 3, delimiter needs to be in bytes format
180+
content = content.split(exercises.GRAPHIE_DELIMITER.encode("ascii"))
181+
if len(content) != 2:
182+
raise ValueError(
183+
f"Graphie file '{image.original_filename}' "
184+
f"missing delimiter {exercises.GRAPHIE_DELIMITER!r}"
185+
)
186+
self.add_file_to_write(svg_name, content[0])
187+
self.add_file_to_write(json_name, content[1])
188+
written_paths.append(svg_name)
189+
written_paths.append(json_name)
190+
191+
return sorted(written_paths)
192+
193+
def _rewrite_content_storage_refs(self, raw_data, images_dir):
194+
"""Rewrite a raw item's content-storage references to the packaged
195+
``images_dir`` (under the ``IMG_PLACEHOLDER`` the renderer resolves)."""
196+
return raw_data.replace(
197+
exercises.CONTENT_STORAGE_PLACEHOLDER,
198+
f"{exercises.IMG_PLACEHOLDER}/{images_dir}",
199+
)
200+
139201
def _add_original_image(self, checksum, filename, new_file_path):
140202
"""Extract original image handling"""
141203
with storage.open(
@@ -314,15 +376,13 @@ def process_assessment_item(self, assessment_item):
314376
processed_answers = self._process_answers(assessment_item)
315377
processed_hints = self._process_hints(assessment_item)
316378

317-
new_file_path = self.get_image_file_path()
318-
new_image_path = f"{exercises.IMG_PLACEHOLDER}/{new_file_path}"
319379
context = {
320380
"question": question,
321381
"question_images": question_images,
322382
"answers": processed_answers,
323383
"multiple_select": assessment_item.type == exercises.MULTIPLE_SELECTION,
324-
"raw_data": assessment_item.raw_data.replace(
325-
exercises.CONTENT_STORAGE_PLACEHOLDER, new_image_path
384+
"raw_data": self._rewrite_content_storage_refs(
385+
assessment_item.raw_data, self.get_image_file_path()
326386
),
327387
"hints": processed_hints,
328388
"randomize": assessment_item.randomize,

contentcuration/contentcuration/utils/assessment/perseus.py

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22
import re
33
import zipfile
44

5-
from django.core.files.storage import default_storage as storage
65
from django.template.loader import render_to_string
76
from le_utils.constants import exercises
87
from le_utils.constants import file_formats
98
from le_utils.constants import format_presets
109

11-
from contentcuration import models
1210
from contentcuration.utils.assessment.base import ExerciseArchiveGenerator
1311
from contentcuration.utils.assessment.qti.perseus_derive import derive_perseus_item
1412
from contentcuration.utils.parser import extract_value
@@ -55,48 +53,6 @@ def _derived_items(self):
5553
}
5654
return self._derived_cache
5755

58-
def _write_raw_perseus_image_files(self, assessment_item):
59-
# For raw perseus JSON questions, the files must be
60-
# specified in advance.
61-
62-
# Files have been prefetched when the assessment item was
63-
# queried, so take advantage of that.
64-
files = sorted(assessment_item.files.all(), key=lambda x: x.checksum)
65-
image_files = filter(
66-
lambda x: x.preset_id == format_presets.EXERCISE_IMAGE, files
67-
)
68-
graphie_files = filter(
69-
lambda x: x.preset_id == format_presets.EXERCISE_GRAPHIE, files
70-
)
71-
images_path = self.get_image_file_path()
72-
for image in image_files:
73-
image_name = "{}/{}.{}".format(
74-
images_path, image.checksum, image.file_format_id
75-
)
76-
with storage.open(
77-
models.generate_object_storage_name(image.checksum, str(image)),
78-
"rb",
79-
) as content:
80-
self.add_file_to_write(image_name, content.read())
81-
82-
for image in graphie_files:
83-
svg_name = "{}/{}.svg".format(images_path, image.original_filename)
84-
json_name = "{}/{}-data.json".format(images_path, image.original_filename)
85-
with storage.open(
86-
models.generate_object_storage_name(image.checksum, str(image)),
87-
"rb",
88-
) as content:
89-
content = content.read()
90-
# in Python 3, delimiter needs to be in bytes format
91-
content = content.split(exercises.GRAPHIE_DELIMITER.encode("ascii"))
92-
if len(content) != 2:
93-
raise ValueError(
94-
f"Graphie file '{image.original_filename}' "
95-
f"missing delimiter {exercises.GRAPHIE_DELIMITER!r}"
96-
)
97-
self.add_file_to_write(svg_name, content[0])
98-
self.add_file_to_write(json_name, content[1])
99-
10056
def _process_formulas(self, content):
10157
return _DOUBLE_DOLLAR_RE.sub(r"$\1$", content)
10258

@@ -112,7 +68,7 @@ def process_assessment_item(self, assessment_item):
11268
return
11369
return super().process_assessment_item(derived)
11470
if assessment_item.type == exercises.PERSEUS_QUESTION:
115-
self._write_raw_perseus_image_files(assessment_item)
71+
self._write_raw_perseus_assets(assessment_item, self.get_image_file_path())
11672
return super().process_assessment_item(assessment_item)
11773

11874
def _process_input_answers(self, processed_data):

0 commit comments

Comments
 (0)