Skip to content

Commit da92f80

Browse files
authored
Merge pull request learningequality#6040 from rtibblesbot/issue-6004-1973c5
feat: export included_presets renderable bitmask for content-schema import gating
2 parents 07b4f58 + 21edf6f commit da92f80

9 files changed

Lines changed: 108 additions & 3 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 3.2.25 on 2026-07-12 22:39
2+
from django.db import migrations
3+
from django.db import models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("contentcuration", "0167_file_size_bigint_expand"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="assessmentitem",
15+
name="type",
16+
field=models.CharField(
17+
choices=[
18+
("input_question", "Input Question"),
19+
("multiple_selection", "Multiple Selection"),
20+
("single_selection", "Single Selection"),
21+
("free_response", "Free Response"),
22+
("perseus_question", "Perseus Question"),
23+
("QTI", "QTI"),
24+
("true_false", "True/False"),
25+
],
26+
default="multiple_selection",
27+
max_length=50,
28+
),
29+
),
30+
]

contentcuration/contentcuration/tests/test_exportchannel.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from le_utils.constants import exercises
2020
from le_utils.constants import format_presets
2121
from le_utils.constants import modalities
22+
from le_utils.constants.format_presets import RENDERABLE_PRESETS_ORDER
2223
from le_utils.constants.labels import accessibility_categories
2324
from le_utils.constants.labels import learning_activities
2425
from le_utils.constants.labels import levels
@@ -505,6 +506,21 @@ def test_contentnode_file_size_data(self):
505506
for file in files.prefetch_related("local_file"):
506507
self.assertEqual(file.file_size, file.local_file.file_size)
507508

509+
def test_file_included_presets_renderable(self):
510+
# Every non-supplementary (renderable) exported file carries its own preset bit.
511+
files = kolibri_models.File.objects.filter(supplementary=False)
512+
assert files.count() > 0
513+
for file in files:
514+
expected = 2 ** RENDERABLE_PRESETS_ORDER.index(file.preset)
515+
self.assertEqual(file.included_presets, expected)
516+
517+
def test_file_included_presets_supplementary_null(self):
518+
# Supplementary files (e.g. thumbnails) leave included_presets NULL.
519+
files = kolibri_models.File.objects.filter(supplementary=True)
520+
assert files.count() > 0
521+
for file in files:
522+
self.assertIsNone(file.included_presets)
523+
508524
def test_channel_icon_encoding(self):
509525
self.assertIsNotNone(self.content_channel.icon_encoding)
510526

contentcuration/contentcuration/utils/publish.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from le_utils.constants import licenses
3939
from le_utils.constants import modalities
4040
from le_utils.constants import roles
41+
from le_utils.constants.format_presets import RENDERABLE_PRESETS_ORDER
4142
from search.models import ChannelFullTextSearch
4243
from search.models import ContentNodeFullTextSearch
4344
from search.utils import get_fts_annotated_channel_qs
@@ -666,6 +667,21 @@ def create_associated_file_objects(kolibrinode, ccnode):
666667
},
667668
)
668669

670+
included_presets = None
671+
if not preset.supplementary:
672+
try:
673+
included_presets = 2 ** RENDERABLE_PRESETS_ORDER.index(preset.pk)
674+
except ValueError:
675+
# Renderable preset not in the (append-only) ordering — e.g. a newer
676+
# le-utils preset. Log and leave included_presets NULL for this file
677+
# rather than aborting the whole channel publish.
678+
logging.warning(
679+
"Preset %s missing from RENDERABLE_PRESETS_ORDER; leaving "
680+
"included_presets NULL for file %s",
681+
preset.pk,
682+
ccfilemodel.pk,
683+
)
684+
669685
kolibrimodels.File.objects.create(
670686
pk=ccfilemodel.pk,
671687
checksum=ccfilemodel.checksum,
@@ -679,6 +695,7 @@ def create_associated_file_objects(kolibrinode, ccnode):
679695
thumbnail=preset.thumbnail,
680696
priority=preset.order,
681697
local_file=kolibrilocalfilemodel,
698+
included_presets=included_presets,
682699
)
683700

684701

contentcuration/kolibri_content/base_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class File(models.Model):
132132
supplementary = models.BooleanField(default=False)
133133
thumbnail = models.BooleanField(default=False)
134134
priority = models.IntegerField(blank=True, null=True, db_index=True)
135+
# Bitmask of the renderable presets a device needs to render this file,
136+
# including the file's own preset. NULL for supplementary files.
137+
included_presets = models.IntegerField(blank=True, null=True)
135138

136139
class Meta:
137140
abstract = True

contentcuration/kolibri_content/constants/schema_versions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
VERSION_5 = "5"
2121

22+
VERSION_6 = "6"
23+
2224
# List of the content db schema versions, ordered from most recent to least recent.
2325
# When a new schema version is generated, it should be added here, at the top of the list.
2426
CONTENT_DB_SCHEMA_VERSIONS = [
27+
VERSION_6,
2528
VERSION_5,
2629
VERSION_4,
2730
VERSION_3,
@@ -33,7 +36,7 @@
3336
]
3437

3538
# The latest compatible exported schema version for this version of Kolibri
36-
CONTENT_SCHEMA_VERSION = VERSION_5
39+
CONTENT_SCHEMA_VERSION = VERSION_6
3740

3841
# The version name for the current content schema,
3942
# which may have schema modifications not present in the export schema
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.25 on 2026-07-12 22:27
2+
from django.db import migrations
3+
from django.db import models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("content", "0023_auto_20250417_1516"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="file",
15+
name="included_presets",
16+
field=models.IntegerField(blank=True, null=True),
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.25 on 2026-07-12 22:27
2+
from django.db import migrations
3+
from django.db import models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("kolibri_public", "0008_channelmetadata_categories_bitmask_0"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="file",
15+
name="included_presets",
16+
field=models.IntegerField(blank=True, null=True),
17+
),
18+
]

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ djangorestframework==3.15.1
55
psycopg2-binary==2.9.11
66
django-js-reverse==0.10.2
77
django-registration==3.4
8-
le-utils==0.2.17
8+
le-utils==0.2.18
99
gunicorn==25.1.0
1010
django-postmark==0.1.6
1111
jsonfield==3.1.0

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ langcodes==3.5.1
180180
# via -r requirements.in
181181
latex2mathml==3.78.1
182182
# via -r requirements.in
183-
le-utils==0.2.17
183+
le-utils==0.2.18
184184
# via -r requirements.in
185185
markdown-it-py==4.0.0
186186
# via -r requirements.in

0 commit comments

Comments
 (0)