diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index e1b9485f8aa..18200e72c0d 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -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") + included_languages = CharInFilter( + field_name="included_languages", label="Languages", distinct=True + ) class Meta: model = models.ChannelMetadata @@ -419,10 +426,6 @@ class UUIDInFilter(BaseInFilter, UUIDFilter): pass -class CharInFilter(BaseInFilter, CharFilter): - pass - - contentnode_filter_fields = [ "parent", "parent__isnull", @@ -473,6 +476,7 @@ class ContentNodeFilter(FilterSet): keywords = CharFilter(method="filter_keywords") channels = UUIDInFilter(field_name="channel_id") languages = CharInFilter(field_name="lang_id") + included_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") @@ -673,10 +677,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", @@ -690,7 +695,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", @@ -725,7 +730,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", diff --git a/kolibri/core/content/contentschema/versions/content_schema_current.py b/kolibri/core/content/contentschema/versions/content_schema_current.py index 4fefd204b4c..8bdf71bdeb1 100644 --- a/kolibri/core/content/contentschema/versions/content_schema_current.py +++ b/kolibri/core/content/contentschema/versions/content_schema_current.py @@ -145,6 +145,22 @@ class ContentContentnodeHasPrerequisite(Base): to_contentnode_id = Column(CHAR(32), nullable=False, index=True) +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(CHAR(32), nullable=False, index=True) + language_id = Column(String(14), nullable=False, index=True) + + class ContentContentnodeRelated(Base): __tablename__ = "content_contentnode_related" __table_args__ = ( diff --git a/kolibri/core/content/migrations/0040_contentnode_included_languages.py b/kolibri/core/content/migrations/0040_contentnode_included_languages.py new file mode 100644 index 00000000000..0785e517964 --- /dev/null +++ b/kolibri/core/content/migrations/0040_contentnode_included_languages.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-12-18 00:14 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0039_channelmetadata_ordered_fields"), + ] + + operations = [ + migrations.AddField( + model_name="contentnode", + name="included_languages", + field=models.ManyToManyField( + blank=True, + related_name="contentnodes", + to="content.Language", + verbose_name="languages", + ), + ), + ] diff --git a/kolibri/core/content/models.py b/kolibri/core/content/models.py index 78e461fb865..6724b3354d5 100644 --- a/kolibri/core/content/models.py +++ b/kolibri/core/content/models.py @@ -215,6 +215,18 @@ 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 + # any language directly set on the topic nodes is ignored, + # as it is not meaningful to set a language on a topic node if it does not apply + # to any descendants. + # 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: diff --git a/kolibri/core/content/test/test_annotation.py b/kolibri/core/content/test/test_annotation.py index 2de250711eb..1d4123ecc5e 100644 --- a/kolibri/core/content/test/test_annotation.py +++ b/kolibri/core/content/test/test_annotation.py @@ -745,6 +745,268 @@ def test_two_channels_no_annotation_collision_child_true(self): self.assertFalse(root_node.available) self.assertFalse(root_node.coach_content) + def test_non_topic_node_included_languages(self): + """ + Test that non-topic nodes get their lang_id properly set as their included_language + """ + test_node = ContentNode.objects.exclude(kind=content_kinds.TOPIC).first() + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Set a language on our test node + test_node.lang = test_language + test_node.save() + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the node has exactly one included language matching its lang_id + self.assertEqual(test_node.included_languages.count(), 1) + self.assertEqual(test_node.included_languages.first(), test_language) + + def test_topic_node_not_includes_own_language(self): + """ + Test that topic nodes do not include their own language in included_languages + """ + topic_node = ContentNode.objects.filter(kind=content_kinds.TOPIC).first() + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Set a language on the topic + topic_node.lang = test_language + topic_node.save() + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the topic includes its own language + self.assertNotIn(test_language, topic_node.included_languages.all()) + + def test_topic_node_includes_child_languages(self): + """ + Test that topic nodes include languages from their child nodes + """ + topic_node = ContentNode.objects.filter(kind=content_kinds.TOPIC).first() + + # Create two test languages + lang1 = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language 1", + lang_direction="ltr", + ) + lang2 = Language.objects.create( + id="tt-se", + lang_code="tt", + lang_subcode="se", + lang_name="Test Language 2", + lang_direction="ltr", + ) + + # Create two child nodes with different languages + ContentNode.objects.create( + title="test1", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=topic_node.channel_id, + parent=topic_node, + kind=content_kinds.VIDEO, + available=True, + lang=lang1, + ) + + ContentNode.objects.create( + title="test2", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=topic_node.channel_id, + parent=topic_node, + kind=content_kinds.VIDEO, + available=True, + lang=lang2, + ) + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the topic includes both child languages + included_languages = topic_node.included_languages.all() + self.assertEqual(len(included_languages), 2) + self.assertIn(lang1, included_languages) + self.assertIn(lang2, included_languages) + + def test_topic_node_includes_grandchild_languages(self): + """ + Test that topic nodes include languages from their grandchild nodes + """ + root_topic = ContentNode.objects.filter(kind=content_kinds.TOPIC).first() + + # Create test language + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Create child topic + child_topic = ContentNode.objects.create( + title="test_topic", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=root_topic.channel_id, + parent=root_topic, + kind=content_kinds.TOPIC, + available=True, + ) + + # Create grandchild with language + ContentNode.objects.create( + title="test_content", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=root_topic.channel_id, + parent=child_topic, + kind=content_kinds.VIDEO, + available=True, + lang=test_language, + ) + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify both the child topic and root topic include the grandchild's language + self.assertIn(test_language, child_topic.included_languages.all()) + self.assertIn(test_language, root_topic.included_languages.all()) + + def test_topic_deduplicates_languages(self): + """ + Test that topic nodes don't duplicate languages when multiple children have the same language + """ + topic_node = ContentNode.objects.filter(kind=content_kinds.TOPIC).first() + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Create multiple children with the same language + for i in range(3): + ContentNode.objects.create( + title=f"test{i}", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=topic_node.channel_id, + parent=topic_node, + kind=content_kinds.VIDEO, + available=True, + lang=test_language, + ) + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the language only appears once + self.assertEqual(topic_node.included_languages.count(), 1) + self.assertEqual(topic_node.included_languages.first(), test_language) + + def test_non_available_child_languages_excluded(self): + """ + Test that languages from non-available children are not included in the topic's languages + """ + topic_node = ContentNode.objects.filter(kind=content_kinds.TOPIC).first() + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Create a non-available child with a language + ContentNode.objects.create( + title="test", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id=topic_node.channel_id, + parent=topic_node, + kind=content_kinds.VIDEO, + available=False, + lang=test_language, + ) + + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the topic doesn't include the language from the non-available child + self.assertEqual(topic_node.included_languages.count(), 0) + + def test_duplicate_language_handling_in_recursion(self): + """ + Test that the recursion handles cases where a topic might receive the same + language multiple times (from own lang_id and from children) + """ + # Create a topic with two levels of children + root_topic = ContentNode.objects.create( + title="root", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id="6199dde695db4ee4ab392222d5af1e5c", + kind=content_kinds.TOPIC, + available=True, + ) + + test_language = Language.objects.create( + id="te-st", + lang_code="te", + lang_subcode="st", + lang_name="Test Language", + lang_direction="ltr", + ) + + # Set the root topic's language + root_topic.lang = test_language + root_topic.save() + + # Create a child topic with the same language + child_topic = ContentNode.objects.create( + title="child", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id="6199dde695db4ee4ab392222d5af1e5c", + parent=root_topic, + kind=content_kinds.TOPIC, + available=True, + lang=test_language, + ) + + # Create a grandchild with the same language + ContentNode.objects.create( + title="grandchild", + id=uuid.uuid4().hex, + content_id=uuid.uuid4().hex, + channel_id="6199dde695db4ee4ab392222d5af1e5c", + parent=child_topic, + kind=content_kinds.VIDEO, + available=True, + lang=test_language, + ) + + # This should not raise an IntegrityError + recurse_annotation_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c") + + # Verify the relationships are correct + self.assertEqual(root_topic.included_languages.count(), 1) + self.assertEqual(child_topic.included_languages.count(), 1) + def tearDown(self): call_command("flush", interactive=False) super(AnnotationTreeRecursion, self).tearDown() diff --git a/kolibri/core/content/test/test_content_app.py b/kolibri/core/content/test/test_content_app.py index 8c5e6f46ad6..89ce4bd1bc4 100644 --- a/kolibri/core/content/test/test_content_app.py +++ b/kolibri/core/content/test/test_content_app.py @@ -480,6 +480,213 @@ def test_contentnode_list_long(self): self.assertEqual(len(response.data), expected_output) self._assert_nodes(response.data, nodes) + def test_filter_by_single_included_language(self): + """ + Test filtering ContentNodes by a single included language + """ + c1 = content.ContentNode.objects.get(title="c1") + language = content.Language.objects.create( + id="en", + lang_code="en", + lang_subcode="", + lang_name="English", + lang_direction="ltr", + ) + c1.included_languages.set([language]) + c1.save() + + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "en"} + ) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["title"], "c1") + + def test_filter_by_multiple_included_languages(self): + """ + Test filtering ContentNodes that match any of the provided languages + """ + c1 = content.ContentNode.objects.get(title="c1") + c2 = content.ContentNode.objects.get(title="c2") + english = content.Language.objects.create( + id="en", + lang_code="en", + lang_subcode="", + lang_name="English", + lang_direction="ltr", + ) + spanish = content.Language.objects.create( + id="es", + lang_code="es", + lang_subcode="", + lang_name="Spanish", + lang_direction="ltr", + ) + c1.included_languages.set([english]) + c2.included_languages.set([spanish]) + c1.save() + c2.save() + + response = self.client.get( + reverse("kolibri:core:contentnode-list"), + data={"included_languages": "en,es"}, + ) + self.assertEqual(len(response.data), 2) + titles = [node["title"] for node in response.data] + self.assertIn("c1", titles) + self.assertIn("c2", titles) + + def test_filter_by_non_existent_language(self): + """ + Test filtering by a language that no ContentNode has + """ + c1 = content.ContentNode.objects.get(title="c1") + english = content.Language.objects.create( + id="en", + lang_code="en", + lang_subcode="", + lang_name="English", + lang_direction="ltr", + ) + c1.included_languages.set([english]) + c1.save() + + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "fr"} + ) + self.assertEqual(len(response.data), 0) + + def test_filter_by_multiple_languages_per_node(self): + """ + Test filtering nodes that have multiple languages assigned + """ + c1 = content.ContentNode.objects.get(title="c1") + english = content.Language.objects.create( + id="en", + lang_code="en", + lang_subcode="", + lang_name="English", + lang_direction="ltr", + ) + spanish = content.Language.objects.create( + id="es", + lang_code="es", + lang_subcode="", + lang_name="Spanish", + lang_direction="ltr", + ) + c1.included_languages.set([english, spanish]) + c1.save() + + # Should match when searching for either language + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "en"} + ) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["title"], "c1") + + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "es"} + ) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["title"], "c1") + + def test_filter_by_empty_included_languages(self): + """ + Test that nodes with empty included_languages are not returned when filtering + """ + c1 = content.ContentNode.objects.get(title="c1") + c1.included_languages.set([]) + c1.save() + + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "en"} + ) + self.assertEqual(len(response.data), 0) + + def test_filter_by_language_subcodes(self): + """ + Test filtering by language codes with subcodes, ensuring that: + 1. Searching by base language code returns all variants + 2. Searching by specific subcode only returns exact matches + """ + nodes = { + "es_node": content.ContentNode.objects.get(title="c1"), + "es_es_node": content.ContentNode.objects.get(title="c2"), + "es_419_node": content.ContentNode.objects.get(title="c2c1"), + "en_gb_node": content.ContentNode.objects.get(title="c2c2"), + } + + english = content.Language.objects.create( + id="en-gb", + lang_code="en", + lang_subcode="gb", + lang_name="English", + lang_direction="ltr", + ) + spanish = content.Language.objects.create( + id="es", + lang_code="es", + lang_subcode="", + lang_name="Spanish", + lang_direction="ltr", + ) + + spanish_spanish = content.Language.objects.create( + id="es-es", + lang_code="es", + lang_subcode="es", + lang_name="Spanish", + lang_direction="ltr", + ) + + latin_american_spanish = content.Language.objects.create( + id="es-419", + lang_code="es", + lang_subcode="419", + lang_name="Spanish", + lang_direction="ltr", + ) + + # Set up nodes with different language codes + nodes["es_node"].included_languages.set([spanish]) + nodes["es_es_node"].included_languages.set([spanish_spanish]) + nodes["es_419_node"].included_languages.set([latin_american_spanish]) + nodes["en_gb_node"].included_languages.set([english]) + + for node in nodes.values(): + node.save() + + # Test that searching by 'es' returns only the unpreifxed spanish variaent + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "es"} + ) + self.assertEqual(len(response.data), 1) + title = response.data[0]["title"] + self.assertEqual(title, "c1") + + # Test that searching by specific Spanish variant only returns exact match + response = self.client.get( + reverse("kolibri:core:contentnode-list"), + data={"included_languages": "es-419"}, + ) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["title"], "c2c1") + + # Test that searching by 'en' returns nothing + response = self.client.get( + reverse("kolibri:core:contentnode-list"), data={"included_languages": "en"} + ) + self.assertEqual(len(response.data), 0) + + # Test searching for multiple specific variants + response = self.client.get( + reverse("kolibri:core:contentnode-list"), + data={"included_languages": "es-es,es-419"}, + ) + self.assertEqual(len(response.data), 2) + titles = {node["title"] for node in response.data} + self.assertEqual(titles, {"c2", "c2c1"}) + def _recurse_and_assert(self, data, nodes, recursion_depth=0): recursion_depths = [] for actual, expected in zip(data, nodes): diff --git a/kolibri/core/content/upgrade.py b/kolibri/core/content/upgrade.py index aa2b646ed92..43f7b4ff8da 100644 --- a/kolibri/core/content/upgrade.py +++ b/kolibri/core/content/upgrade.py @@ -23,6 +23,7 @@ from kolibri.core.content.utils.annotation import calculate_included_languages from kolibri.core.content.utils.annotation import calculate_ordered_categories from kolibri.core.content.utils.annotation import calculate_ordered_grade_levels +from kolibri.core.content.utils.annotation import recurse_annotation_up_tree from kolibri.core.content.utils.annotation import set_channel_ancestors from kolibri.core.content.utils.annotation import set_content_visibility_from_disk from kolibri.core.content.utils.channel_import import FutureSchemaError @@ -349,7 +350,7 @@ def synchronize_content_requests_upgrade(): @version_upgrade(old_version="<0.18.0") -def ordered_metadata_in_channels(): +def ordered_metadata_and_included_languages_in_channels(): """ Update the channel metadata to have grade_levels, categories, and included languages ordered by occurrence in the channel resources @@ -358,4 +359,5 @@ def ordered_metadata_in_channels(): calculate_ordered_categories(channel) calculate_ordered_grade_levels(channel) calculate_included_languages(channel) + recurse_annotation_up_tree(channel.id) ContentCacheKey.update_cache_key() diff --git a/kolibri/core/content/utils/annotation.py b/kolibri/core/content/utils/annotation.py index a9cba7f146e..7b36b5f9ff3 100644 --- a/kolibri/core/content/utils/annotation.py +++ b/kolibri/core/content/utils/annotation.py @@ -530,6 +530,7 @@ def recurse_annotation_up_tree(channel_id): bridge = Bridge(app_name=CONTENT_APP_NAME) ContentNodeTable = bridge.get_table(ContentNode) + IncludedLanguagesTable = bridge.get_table(ContentNode.included_languages.through) connection = bridge.get_connection() @@ -583,6 +584,31 @@ def recurse_annotation_up_tree(channel_id): ) ) + # First clear out all existing language relationships for nodes in this channel + connection.execute( + IncludedLanguagesTable.delete().where( + IncludedLanguagesTable.c.contentnode_id.in_( + select([ContentNodeTable.c.id]).where( + ContentNodeTable.c.channel_id == channel_id + ) + ) + ) + ) + + # For non-topic nodes only, set included_languages based on their lang_id + connection.execute( + IncludedLanguagesTable.insert().from_select( + ["contentnode_id", "language_id"], + select([ContentNodeTable.c.id, ContentNodeTable.c.lang_id]).where( + and_( + ContentNodeTable.c.channel_id == channel_id, + ContentNodeTable.c.kind != content_kinds.TOPIC, + ContentNodeTable.c.lang_id.isnot(None), + ) + ), + ) + ) + # Expression to capture all available child nodes of a contentnode available_nodes = select(child.c.available).where( and_( @@ -661,6 +687,36 @@ def recurse_annotation_up_tree(channel_id): ) ) + # Update included languages for all topics at this level by combining + # their own language with their children's languages in one query + connection.execute( + IncludedLanguagesTable.insert().from_select( + ["contentnode_id", "language_id"], + # Languages from children + select([ContentNodeTable.c.id, IncludedLanguagesTable.c.language_id]) + .select_from(ContentNodeTable) + .join( + child, + and_( + child.c.parent_id == ContentNodeTable.c.id, + child.c.available == True, # noqa + ), + ) + .join( + IncludedLanguagesTable, + IncludedLanguagesTable.c.contentnode_id == child.c.id, + ) + .where( + and_( + ContentNodeTable.c.level == level - 1, + ContentNodeTable.c.channel_id == channel_id, + ContentNodeTable.c.kind == content_kinds.TOPIC, + ) + ) + .distinct(), + ) + ) + # commit the transaction trans.commit() diff --git a/kolibri/core/content/utils/channel_import.py b/kolibri/core/content/utils/channel_import.py index 07d3f9ae294..b86b598b347 100644 --- a/kolibri/core/content/utils/channel_import.py +++ b/kolibri/core/content/utils/channel_import.py @@ -61,6 +61,7 @@ models_to_exclude = [ apps.get_model(CONTENT_APP_NAME, "ChannelMetadata_included_languages"), + apps.get_model(CONTENT_APP_NAME, "ContentNode_included_languages"), ] + no_schema_models diff --git a/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js b/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js index 35499f67bfc..8040c6f9435 100644 --- a/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js +++ b/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js @@ -53,7 +53,7 @@ import useFetch from './useFetch'; * @property {(resources: Array) => void} setSelectedResources Replaces the current * `selectedResources` array with the provided resources array. * @property {() => void} clearSearch Clears the current search terms and results. - * @property {(tag: Object) => void} removeSearchFilterTag Removes the specified tag from the + * @property {(tag: Object) => void} removeSearchTerm Removes the specified term from the * search terms. * * @returns {UseResourceSelectionResponse} @@ -250,6 +250,6 @@ export default function useResourceSelection({ deselectResources, setSelectedResources, clearSearch: useSearchObject.clearSearch, - removeSearchFilterTag: useSearchObject.removeFilterTag, + removeSearchTerm: useSearchObject.removeSearchTerm, }; } diff --git a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue b/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue index 452c0bb0548..fdc1ac5d7e2 100644 --- a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue +++ b/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue @@ -48,7 +48,7 @@ @selectResources="selectResources" @deselectResources="deselectResources" @setSelectedResources="setSelectedResources" - @removeSearchFilterTag="removeSearchFilterTag" + @removeSearchTerm="removeSearchTerm" /> Object.prototype.hasOwnProperty.call(value, k)); }, }, + showLanguages: { + type: Boolean, + default: true, + }, handleCategory: { type: Function, required: true, @@ -247,16 +251,6 @@ } return null; }, - languageOptionsList() { - return this.availableLanguages.map(language => { - return { - value: language.id, - disabled: - this.searchableLabels && !this.searchableLabels.languages.includes(language.id), - label: language.lang_name, - }; - }); - }, accessibilityOptionsList() { return this.availableAccessibilityOptions.map(key => { const value = AccessibilityCategories[key]; diff --git a/packages/kolibri-common/components/SearchFiltersPanel/LanguageSelector.vue b/packages/kolibri-common/components/SearchFiltersPanel/LanguageSelector.vue new file mode 100644 index 00000000000..80843435bec --- /dev/null +++ b/packages/kolibri-common/components/SearchFiltersPanel/LanguageSelector.vue @@ -0,0 +1,90 @@ + + + + diff --git a/packages/kolibri-common/components/SearchFiltersPanel/SelectGroup.vue b/packages/kolibri-common/components/SearchFiltersPanel/SelectGroup.vue index a1200ecb698..0eeec98104c 100644 --- a/packages/kolibri-common/components/SearchFiltersPanel/SelectGroup.vue +++ b/packages/kolibri-common/components/SearchFiltersPanel/SelectGroup.vue @@ -1,16 +1,9 @@