Skip to content

Commit

Permalink
Add included_languages M2M field to contentnodes.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed Dec 17, 2024
1 parent c76c904 commit 012d8fd
Show file tree
Hide file tree
Showing 7 changed files with 628 additions and 12 deletions.
20 changes: 12 additions & 8 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,19 @@ def list(self, request, *args, **kwargs):
return super(RemoteViewSet, self).list(request, *args, **kwargs)


class CharInFilter(BaseInFilter, CharFilter):
pass


class ChannelMetadataFilter(FilterSet):
available = BooleanFilter(method="filter_available", label="Available")
contains_exercise = BooleanFilter(
method="filter_contains_exercise", label="Has exercises"
)
contains_quiz = BooleanFilter(method="filter_contains_quiz", label="Has quizzes")
languages = CharInFilter(
field_name="included_languages", label="Languages", distinct=True
)

class Meta:
model = models.ChannelMetadata
Expand Down Expand Up @@ -417,10 +424,6 @@ class UUIDInFilter(BaseInFilter, UUIDFilter):
pass


class CharInFilter(BaseInFilter, CharFilter):
pass


contentnode_filter_fields = [
"parent",
"parent__isnull",
Expand Down Expand Up @@ -470,7 +473,7 @@ class ContentNodeFilter(FilterSet):
learner_needs = CharFilter(method="bitmask_contains_and")
keywords = CharFilter(method="filter_keywords")
channels = UUIDInFilter(field_name="channel_id")
languages = CharInFilter(field_name="lang_id")
languages = CharInFilter(field_name="included_languages")
categories__isnull = BooleanFilter(field_name="categories", lookup_expr="isnull")
lft__gt = NumberFilter(field_name="lft", lookup_expr="gt")
rght__lt = NumberFilter(field_name="rght", lookup_expr="lt")
Expand Down Expand Up @@ -671,10 +674,11 @@ def get_queryset(self):
return models.ContentNode.objects.filter(available=True)

def get_related_data_maps(self, items, queryset):
ids = [item["id"] for item in items]
assessmentmetadata_map = {
a["contentnode"]: a
for a in models.AssessmentMetaData.objects.filter(
contentnode__in=queryset
contentnode__in=ids
).values(
"assessment_item_ids",
"number_of_assessments",
Expand All @@ -688,7 +692,7 @@ def get_related_data_maps(self, items, queryset):
files_map = {}

files = list(
models.File.objects.filter(contentnode__in=queryset).values(
models.File.objects.filter(contentnode__in=ids).values(
"id",
"contentnode",
"local_file__id",
Expand Down Expand Up @@ -723,7 +727,7 @@ def get_related_data_maps(self, items, queryset):
tags_map = {}

for t in (
models.ContentTag.objects.filter(tagged_content__in=queryset)
models.ContentTag.objects.filter(tagged_content__in=ids)
.values(
"tag_name",
"tagged_content",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sqlalchemy import BigInteger
from sqlalchemy import Boolean
from sqlalchemy import CHAR
from sqlalchemy import CheckConstraint
from sqlalchemy import Column
from sqlalchemy import Float
from sqlalchemy import ForeignKey
Expand Down Expand Up @@ -45,6 +46,36 @@ class ContentLocalfile(Base):
class ContentContentnode(Base):
__tablename__ = "content_contentnode"
__table_args__ = (
CheckConstraint(
"""
"lft" >= 0),
"rght" integer unsigned NOT NULL CHECK ("rght" >= 0),
"tree_id" integer unsigned NOT NULL CHECK ("tree_id" >= 0),
"level" integer unsigned NOT NULL CHECK ("level" >= 0),
"lang_id" varchar(14) NULL REFERENCES "content_language" ("id") DEFERRABLE INITIALLY DEFERRED,
"license_description" text NULL,
"license_name" varchar(50) NULL,
"coach_content" bool NOT NULL,
"num_coach_contents" integer NULL,
"on_device_resources" integer NULL,
"options" text NULL,
"accessibility_labels" text NULL,
"categories" text NULL,
"duration" integer unsigned NULL CHECK ("duration" >= 0),
"grade_levels" text NULL,
"learner_needs" text NULL,
"learning_activities" text NULL,
"resource_types" text NULL,
"accessibility_labels_bitmask_0" bigint NULL,
"categories_bitmask_0" bigint NULL,
"grade_levels_bitmask_0" bigint NULL,
"learner_needs_bitmask_0" bigint NULL,
"learning_activities_bitmask_0" bigint NULL,
"ancestors" text NULL,
"admin_imported" bool NULL,
"parent_id" char(32) NULL REFERENCES "content_contentnode" ("id") DEFERRABLE INITIALLY DEFERRED
"""
),
Index(
"content_contentnode_level_channel_id_available_29f0bb18_idx",
"level",
Expand All @@ -69,10 +100,10 @@ class ContentContentnode(Base):
author = Column(String(200), nullable=False)
kind = Column(String(200), nullable=False)
available = Column(Boolean, nullable=False)
lft = Column(Integer, nullable=False, index=True)
rght = Column(Integer, nullable=False, index=True)
lft = Column(Integer, nullable=False)
rght = Column(Integer, nullable=False)
tree_id = Column(Integer, nullable=False, index=True)
level = Column(Integer, nullable=False, index=True)
level = Column(Integer, nullable=False)
lang_id = Column(ForeignKey("content_language.id"), index=True)
license_description = Column(Text)
license_name = Column(String(50))
Expand Down Expand Up @@ -118,6 +149,11 @@ class ContentAssessmentmetadata(Base):

class ContentChannelmetadata(Base):
__tablename__ = "content_channelmetadata"
__table_args__ = (
CheckConstraint(
'"order" >= 0), "tagline" varchar(150) NULL, "partial" bool NULL, "public" bool NULL'
),
)

id = Column(CHAR(32), primary_key=True)
name = Column(String(200), nullable=False)
Expand All @@ -131,9 +167,9 @@ class ContentChannelmetadata(Base):
published_size = Column(BigInteger)
total_resource_count = Column(Integer)
order = Column(Integer)
public = Column(Boolean)
tagline = Column(String(150))
partial = Column(Boolean)
public = Column(Boolean)

root = relationship("ContentContentnode")

Expand Down Expand Up @@ -167,6 +203,27 @@ class ContentContentnodeHasPrerequisite(Base):
)


class ContentContentnodeIncludedLanguages(Base):
__tablename__ = "content_contentnode_included_languages"
__table_args__ = (
Index(
"content_contentnode_included_languages_contentnode_id_language_id_7d14ec8b_uniq",
"contentnode_id",
"language_id",
unique=True,
),
)

id = Column(Integer, primary_key=True)
contentnode_id = Column(
ForeignKey("content_contentnode.id"), nullable=False, index=True
)
language_id = Column(ForeignKey("content_language.id"), nullable=False, index=True)

contentnode = relationship("ContentContentnode")
language = relationship("ContentLanguage")


class ContentContentnodeRelated(Base):
__tablename__ = "content_contentnode_related"
__table_args__ = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2024-12-14 18:33
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("content", "0038_alter_localfile_extension"),
]

operations = [
migrations.AddField(
model_name="contentnode",
name="included_languages",
field=models.ManyToManyField(
blank=True,
related_name="contentnodes",
to="content.Language",
verbose_name="languages",
),
),
]
10 changes: 10 additions & 0 deletions kolibri/core/content/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ class ContentNode(base_models.ContentNode):
# needs a subsequent Kolibri upgrade step to backfill these values.
admin_imported = models.BooleanField(null=True)

# Languages that are in this node and/or any descendant nodes of this node
# for non-topic nodes, this is the language of the node itself
# for topic nodes, this is the union of all languages of all descendant nodes
# and any language set on the topic node itself
# We do this to allow filtering of a topic tree by a specific language for
# multi-language channels.
included_languages = models.ManyToManyField(
"Language", related_name="contentnodes", verbose_name="languages", blank=True
)

objects = ContentNodeManager()

class Meta:
Expand Down
Loading

0 comments on commit 012d8fd

Please sign in to comment.