Skip to content

Commit 4176893

Browse files
committed
Add included_languages M2M field to contentnodes.
Ignore constraints when generating sql alchemy schema.
1 parent af67515 commit 4176893

File tree

10 files changed

+601
-108
lines changed

10 files changed

+601
-108
lines changed

kolibri/core/content/api.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,19 @@ def list(self, request, *args, **kwargs):
258258
return super(RemoteViewSet, self).list(request, *args, **kwargs)
259259

260260

261+
class CharInFilter(BaseInFilter, CharFilter):
262+
pass
263+
264+
261265
class ChannelMetadataFilter(FilterSet):
262266
available = BooleanFilter(method="filter_available", label="Available")
263267
contains_exercise = BooleanFilter(
264268
method="filter_contains_exercise", label="Has exercises"
265269
)
266270
contains_quiz = BooleanFilter(method="filter_contains_quiz", label="Has quizzes")
271+
languages = CharInFilter(
272+
field_name="included_languages", label="Languages", distinct=True
273+
)
267274

268275
class Meta:
269276
model = models.ChannelMetadata
@@ -417,10 +424,6 @@ class UUIDInFilter(BaseInFilter, UUIDFilter):
417424
pass
418425

419426

420-
class CharInFilter(BaseInFilter, CharFilter):
421-
pass
422-
423-
424427
contentnode_filter_fields = [
425428
"parent",
426429
"parent__isnull",
@@ -470,7 +473,7 @@ class ContentNodeFilter(FilterSet):
470473
learner_needs = CharFilter(method="bitmask_contains_and")
471474
keywords = CharFilter(method="filter_keywords")
472475
channels = UUIDInFilter(field_name="channel_id")
473-
languages = CharInFilter(field_name="lang_id")
476+
languages = CharInFilter(field_name="included_languages")
474477
categories__isnull = BooleanFilter(field_name="categories", lookup_expr="isnull")
475478
lft__gt = NumberFilter(field_name="lft", lookup_expr="gt")
476479
rght__lt = NumberFilter(field_name="rght", lookup_expr="lt")
@@ -671,10 +674,11 @@ def get_queryset(self):
671674
return models.ContentNode.objects.filter(available=True)
672675

673676
def get_related_data_maps(self, items, queryset):
677+
ids = [item["id"] for item in items]
674678
assessmentmetadata_map = {
675679
a["contentnode"]: a
676680
for a in models.AssessmentMetaData.objects.filter(
677-
contentnode__in=queryset
681+
contentnode__in=ids
678682
).values(
679683
"assessment_item_ids",
680684
"number_of_assessments",
@@ -688,7 +692,7 @@ def get_related_data_maps(self, items, queryset):
688692
files_map = {}
689693

690694
files = list(
691-
models.File.objects.filter(contentnode__in=queryset).values(
695+
models.File.objects.filter(contentnode__in=ids).values(
692696
"id",
693697
"contentnode",
694698
"local_file__id",
@@ -723,7 +727,7 @@ def get_related_data_maps(self, items, queryset):
723727
tags_map = {}
724728

725729
for t in (
726-
models.ContentTag.objects.filter(tagged_content__in=queryset)
730+
models.ContentTag.objects.filter(tagged_content__in=ids)
727731
.values(
728732
"tag_name",
729733
"tagged_content",

kolibri/core/content/contentschema/versions/content_schema_current.py

Lines changed: 31 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
from sqlalchemy import BigInteger
33
from sqlalchemy import Boolean
44
from sqlalchemy import CHAR
5-
from sqlalchemy import CheckConstraint
65
from sqlalchemy import Column
76
from sqlalchemy import Float
8-
from sqlalchemy import ForeignKey
9-
from sqlalchemy import ForeignKeyConstraint
107
from sqlalchemy import Index
118
from sqlalchemy import Integer
129
from sqlalchemy import String
1310
from sqlalchemy import Text
1411
from sqlalchemy.ext.declarative import declarative_base
15-
from sqlalchemy.orm import relationship
1612

1713
Base = declarative_base()
1814
metadata = Base.metadata
@@ -47,34 +43,17 @@ class ContentLocalfile(Base):
4743
class ContentContentnode(Base):
4844
__tablename__ = "content_contentnode"
4945
__table_args__ = (
50-
CheckConstraint("lft >= 0"),
51-
CheckConstraint("tree_id >= 0"),
52-
CheckConstraint("level >= 0"),
53-
CheckConstraint("duration >= 0"),
54-
CheckConstraint("rght >= 0"),
55-
ForeignKeyConstraint(
56-
["lang_id"],
57-
["content_language.id"],
58-
deferrable=True,
59-
initially="DEFERRED",
60-
),
61-
ForeignKeyConstraint(
62-
["parent_id"],
63-
["content_contentnode.id"],
64-
deferrable=True,
65-
initially="DEFERRED",
66-
),
6746
Index(
68-
"content_contentnode_level_channel_id_available_29f0bb18_idx",
47+
"content_contentnode_level_channel_id_kind_fd732cc4_idx",
6948
"level",
7049
"channel_id",
71-
"available",
50+
"kind",
7251
),
7352
Index(
74-
"content_contentnode_level_channel_id_kind_fd732cc4_idx",
53+
"content_contentnode_level_channel_id_available_29f0bb18_idx",
7554
"level",
7655
"channel_id",
77-
"kind",
56+
"available",
7857
),
7958
)
8059

@@ -89,6 +68,7 @@ class ContentContentnode(Base):
8968
kind = Column(String(200), nullable=False)
9069
available = Column(Boolean, nullable=False)
9170
lft = Column(Integer, nullable=False)
71+
rght = Column(Integer, nullable=False)
9272
tree_id = Column(Integer, nullable=False, index=True)
9373
level = Column(Integer, nullable=False)
9474
lang_id = Column(String(14), index=True)
@@ -112,12 +92,8 @@ class ContentContentnode(Base):
11292
learning_activities_bitmask_0 = Column(BigInteger)
11393
ancestors = Column(Text)
11494
admin_imported = Column(Boolean)
115-
rght = Column(Integer, nullable=False)
11695
parent_id = Column(CHAR(32), index=True)
11796

118-
lang = relationship("ContentLanguage")
119-
parent = relationship("ContentContentnode", remote_side=[id])
120-
12197

12298
class ContentAssessmentmetadata(Base):
12399
__tablename__ = "content_assessmentmetadata"
@@ -128,22 +104,11 @@ class ContentAssessmentmetadata(Base):
128104
mastery_model = Column(Text, nullable=False)
129105
randomize = Column(Boolean, nullable=False)
130106
is_manipulable = Column(Boolean, nullable=False)
131-
contentnode_id = Column(
132-
ForeignKey("content_contentnode.id"), nullable=False, index=True
133-
)
134-
135-
contentnode = relationship("ContentContentnode")
107+
contentnode_id = Column(CHAR(32), nullable=False, index=True)
136108

137109

138110
class ContentChannelmetadata(Base):
139111
__tablename__ = "content_channelmetadata"
140-
__table_args__ = (
141-
CheckConstraint('"order" >= 0'),
142-
ForeignKeyConstraint(
143-
["root_id"],
144-
["content_contentnode.id"],
145-
),
146-
)
147112

148113
id = Column(CHAR(32), primary_key=True)
149114
name = Column(String(200), nullable=False)
@@ -163,8 +128,6 @@ class ContentChannelmetadata(Base):
163128
included_categories = Column(Text)
164129
included_grade_levels = Column(Text)
165130

166-
root = relationship("ContentContentnode")
167-
168131

169132
class ContentContentnodeHasPrerequisite(Base):
170133
__tablename__ = "content_contentnode_has_prerequisite"
@@ -178,22 +141,25 @@ class ContentContentnodeHasPrerequisite(Base):
178141
)
179142

180143
id = Column(Integer, primary_key=True)
181-
from_contentnode_id = Column(
182-
ForeignKey("content_contentnode.id"), nullable=False, index=True
183-
)
184-
to_contentnode_id = Column(
185-
ForeignKey("content_contentnode.id"), nullable=False, index=True
186-
)
144+
from_contentnode_id = Column(CHAR(32), nullable=False, index=True)
145+
to_contentnode_id = Column(CHAR(32), nullable=False, index=True)
187146

188-
from_contentnode = relationship(
189-
"ContentContentnode",
190-
primaryjoin="ContentContentnodeHasPrerequisite.from_contentnode_id == ContentContentnode.id",
191-
)
192-
to_contentnode = relationship(
193-
"ContentContentnode",
194-
primaryjoin="ContentContentnodeHasPrerequisite.to_contentnode_id == ContentContentnode.id",
147+
148+
class ContentContentnodeIncludedLanguages(Base):
149+
__tablename__ = "content_contentnode_included_languages"
150+
__table_args__ = (
151+
Index(
152+
"content_contentnode_included_languages_contentnode_id_language_id_7d14ec8b_uniq",
153+
"contentnode_id",
154+
"language_id",
155+
unique=True,
156+
),
195157
)
196158

159+
id = Column(Integer, primary_key=True)
160+
contentnode_id = Column(CHAR(32), nullable=False, index=True)
161+
language_id = Column(String(14), nullable=False, index=True)
162+
197163

198164
class ContentContentnodeRelated(Base):
199165
__tablename__ = "content_contentnode_related"
@@ -207,21 +173,8 @@ class ContentContentnodeRelated(Base):
207173
)
208174

209175
id = Column(Integer, primary_key=True)
210-
from_contentnode_id = Column(
211-
ForeignKey("content_contentnode.id"), nullable=False, index=True
212-
)
213-
to_contentnode_id = Column(
214-
ForeignKey("content_contentnode.id"), nullable=False, index=True
215-
)
216-
217-
from_contentnode = relationship(
218-
"ContentContentnode",
219-
primaryjoin="ContentContentnodeRelated.from_contentnode_id == ContentContentnode.id",
220-
)
221-
to_contentnode = relationship(
222-
"ContentContentnode",
223-
primaryjoin="ContentContentnodeRelated.to_contentnode_id == ContentContentnode.id",
224-
)
176+
from_contentnode_id = Column(CHAR(32), nullable=False, index=True)
177+
to_contentnode_id = Column(CHAR(32), nullable=False, index=True)
225178

226179

227180
class ContentContentnodeTags(Base):
@@ -236,15 +189,8 @@ class ContentContentnodeTags(Base):
236189
)
237190

238191
id = Column(Integer, primary_key=True)
239-
contentnode_id = Column(
240-
ForeignKey("content_contentnode.id"), nullable=False, index=True
241-
)
242-
contenttag_id = Column(
243-
ForeignKey("content_contenttag.id"), nullable=False, index=True
244-
)
245-
246-
contentnode = relationship("ContentContentnode")
247-
contenttag = relationship("ContentContenttag")
192+
contentnode_id = Column(CHAR(32), nullable=False, index=True)
193+
contenttag_id = Column(CHAR(32), nullable=False, index=True)
248194

249195

250196
class ContentFile(Base):
@@ -254,19 +200,11 @@ class ContentFile(Base):
254200
supplementary = Column(Boolean, nullable=False)
255201
thumbnail = Column(Boolean, nullable=False)
256202
priority = Column(Integer, index=True)
257-
contentnode_id = Column(
258-
ForeignKey("content_contentnode.id"), nullable=False, index=True
259-
)
260-
lang_id = Column(ForeignKey("content_language.id"), index=True)
261-
local_file_id = Column(
262-
ForeignKey("content_localfile.id"), nullable=False, index=True
263-
)
203+
contentnode_id = Column(CHAR(32), nullable=False, index=True)
204+
lang_id = Column(String(14), index=True)
205+
local_file_id = Column(String(32), nullable=False, index=True)
264206
preset = Column(String(150), nullable=False)
265207

266-
contentnode = relationship("ContentContentnode")
267-
lang = relationship("ContentLanguage")
268-
local_file = relationship("ContentLocalfile")
269-
270208

271209
class ContentChannelmetadataIncludedLanguages(Base):
272210
__tablename__ = "content_channelmetadata_included_languages"
@@ -280,11 +218,6 @@ class ContentChannelmetadataIncludedLanguages(Base):
280218
)
281219

282220
id = Column(Integer, primary_key=True)
283-
channelmetadata_id = Column(
284-
ForeignKey("content_channelmetadata.id"), nullable=False, index=True
285-
)
286-
language_id = Column(ForeignKey("content_language.id"), nullable=False, index=True)
287221
sort_value = Column(Integer, nullable=False)
288-
289-
channelmetadata = relationship("ContentChannelmetadata")
290-
language = relationship("ContentLanguage")
222+
channelmetadata_id = Column(CHAR(32), nullable=False, index=True)
223+
language_id = Column(String(14), nullable=False, index=True)

kolibri/core/content/management/commands/generate_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def handle(self, *args, **options):
134134
metadata.bind = engine
135135

136136
generator = CodeGenerator(
137-
metadata, False, False, True, True, False, nocomments=False
137+
metadata, False, True, True, True, False, nocomments=False
138138
)
139139

140140
with io.open(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 3.2.25 on 2024-12-18 00:14
2+
from django.db import migrations
3+
from django.db import models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("content", "0039_channelmetadata_ordered_fields"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="contentnode",
15+
name="included_languages",
16+
field=models.ManyToManyField(
17+
blank=True,
18+
related_name="contentnodes",
19+
to="content.Language",
20+
verbose_name="languages",
21+
),
22+
),
23+
]

kolibri/core/content/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ class ContentNode(base_models.ContentNode):
215215
# needs a subsequent Kolibri upgrade step to backfill these values.
216216
admin_imported = models.BooleanField(null=True)
217217

218+
# Languages that are in this node and/or any descendant nodes of this node
219+
# for non-topic nodes, this is the language of the node itself
220+
# for topic nodes, this is the union of all languages of all descendant nodes
221+
# and any language set on the topic node itself
222+
# We do this to allow filtering of a topic tree by a specific language for
223+
# multi-language channels.
224+
included_languages = models.ManyToManyField(
225+
"Language", related_name="contentnodes", verbose_name="languages", blank=True
226+
)
227+
218228
objects = ContentNodeManager()
219229

220230
class Meta:

0 commit comments

Comments
 (0)