diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/publish/PublishModal.vue b/contentcuration/contentcuration/frontend/channelEdit/components/publish/PublishModal.vue
index 446c3896d2..a8d8f0c456 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/publish/PublishModal.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/publish/PublishModal.vue
@@ -49,7 +49,7 @@
:label="$tr('versionDescriptionLabel')"
:invalid="!isDescriptionValid"
:invalidText="$tr('descriptionRequiredMessage')"
- :showInvalidText="showInvalidText"
+ :showInvalidText="showDescriptionInvalidText"
autofocus
textArea
/>
@@ -57,6 +57,20 @@
+
+
+
+
+
+
+
@@ -71,6 +85,7 @@
import { mapActions, mapGetters } from 'vuex';
import HelpTooltip from 'shared/views/HelpTooltip';
import { forceServerSync } from 'shared/data/serverSync';
+ import { LanguagesList } from 'shared/leUtils/Languages';
export default {
name: 'PublishModal',
@@ -88,9 +103,13 @@
step: 0,
publishDescription: '',
size: 0,
- showInvalidText: false, // lazy validation
+ showDescriptionInvalidText: false, // lazy validation
+ showLanguageInvalidText: false,
loading: false,
loadingTaskId: null,
+ language: {},
+ channelLanguages: [],
+ channelLanguageExists: true,
};
},
computed: {
@@ -112,9 +131,34 @@
isDescriptionValid() {
return this.publishDescription && this.publishDescription.trim();
},
+ isLanguageValid() {
+ return Object.keys(this.language).length > 0;
+ },
sizeCalculationTask() {
return this.loadingTaskId ? this.getAsyncTask(this.loadingTaskId) : null;
},
+ isCheffedChannel() {
+ return Boolean(this.currentChannel.ricecooker_version);
+ },
+ isPrivateChannel() {
+ return this.currentChannel.public;
+ },
+ isFirstPublish() {
+ return this.currentChannel.version === 0;
+ },
+ defaultLanguage() {
+ const channelLang = this.filterLanguages(l => l.id === this.currentChannel.language)[0];
+ return this.languages.some(lang => lang.value === channelLang.value) ? channelLang : {};
+ },
+ showLanguageDropdown() {
+ return (
+ ((this.isCheffedChannel || this.isPrivateChannel) && this.isFirstPublish) ||
+ !this.channelLanguageExists
+ );
+ },
+ languages() {
+ return this.filterLanguages(l => this.channelLanguages.includes(l.id));
+ },
},
watch: {
sizeCalculationTask(task) {
@@ -139,16 +183,35 @@
// this.loading = response.stale;
// this.loadingTaskId = response.changes.length ? response.changes[0].key : null;
// });
+ this.channelLanguageExistsInResources().then(exists => {
+ this.channelLanguageExists = exists;
+ if (!exists) {
+ this.getLanguagesInChannelResources().then(languages => {
+ this.channelLanguages = languages.length ? languages : [this.currentChannel.language];
+ this.language = this.defaultLanguage;
+ });
+ } else {
+ this.channelLanguages = [this.currentChannel.language];
+ this.language = this.defaultLanguage;
+ }
+ });
},
methods: {
- ...mapActions('currentChannel', ['publishChannel']),
+ ...mapActions('channel', ['updateChannel']),
+ ...mapActions('currentChannel', [
+ 'publishChannel',
+ 'channelLanguageExistsInResources',
+ 'getLanguagesInChannelResources',
+ ]),
close() {
this.publishDescription = '';
+ this.language = this.defaultLanguage;
this.dialog = false;
},
validate() {
- this.showInvalidText = true;
- return this.isDescriptionValid;
+ this.showDescriptionInvalidText = !this.isDescriptionValid;
+ this.showLanguageInvalidText = !this.isLanguageValid;
+ return !this.showDescriptionInvalidText && !this.showLanguageInvalidText;
},
async handlePublish() {
if (this.validate()) {
@@ -156,9 +219,20 @@
await forceServerSync();
}
- this.publishChannel(this.publishDescription).then(this.close);
+ this.updateChannel({
+ id: this.currentChannel.id,
+ language: this.language.value,
+ }).then(() => {
+ this.publishChannel(this.publishDescription).then(this.close);
+ });
}
},
+ filterLanguages(filterFn) {
+ return LanguagesList.filter(filterFn).map(l => ({
+ value: l.id,
+ label: l.native_name,
+ }));
+ },
},
$trs: {
// Incomplete channel window
@@ -174,8 +248,11 @@
descriptionRequiredMessage: "Please describe what's new in this version before publishing",
descriptionDescriptionTooltip:
'This description will be shown to Kolibri admins before they update channel versions',
+ languageDescriptionTooltip: 'The default language for a channel and its resources',
cancelButton: 'Cancel',
publishButton: 'Publish',
+ languageLabel: 'Language',
+ languageRequiredMessage: 'Please select a language for this channel',
},
};
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/publish/__tests__/publishModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/publish/__tests__/publishModal.spec.js
index 3027fae7a3..1de950ceec 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/publish/__tests__/publishModal.spec.js
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/publish/__tests__/publishModal.spec.js
@@ -85,17 +85,21 @@ describe('publishModal', () => {
});
});
describe('on publish step', () => {
+ const updateChannel = jest.fn();
const publishChannel = jest.fn();
+
beforeEach(() => {
wrapper.setData({ step: steps.PUBLISH });
wrapper.setMethods({
- publishChannel: () => {
+ updateChannel: () => {
return new Promise(resolve => {
+ updateChannel();
publishChannel();
resolve();
});
},
});
+ updateChannel.mockReset();
publishChannel.mockReset();
});
it('publish button should trigger form validation', () => {
@@ -110,20 +114,26 @@ describe('publishModal', () => {
"Please describe what's new in this version before publishing"
);
});
- it('publishing should be blocked if no description is given', () => {
+ it('publishing should be blocked if no description & language are given', () => {
wrapper
.find('[data-test="confirm-publish-modal"]')
.find('form')
.trigger('submit');
+ expect(updateChannel).not.toHaveBeenCalled();
expect(publishChannel).not.toHaveBeenCalled();
});
- it('publish button should call publishChannel if description is given', () => {
+ it('publish button should call updateChannel if description and language are given', () => {
const description = 'Version notes';
- wrapper.setData({ publishDescription: description });
+ const language = {
+ value: 'en',
+ label: 'English',
+ };
+ wrapper.setData({ publishDescription: description, language });
wrapper
.find('[data-test="confirm-publish-modal"]')
.find('form')
.trigger('submit');
+ expect(updateChannel).toHaveBeenCalled();
expect(publishChannel).toHaveBeenCalled();
});
it('cancel button on publish step should also close modal', () => {
diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/__tests__/module.spec.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/__tests__/module.spec.js
index f43043649a..cbad4da235 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/__tests__/module.spec.js
+++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/__tests__/module.spec.js
@@ -31,5 +31,23 @@ describe('currentChannel store', () => {
spy.mockRestore();
});
});
+ it('channelLanguageExistsInResources action should call `language_exists` endpoint', () => {
+ const spy = jest
+ .spyOn(Channel, 'languageExistsInResources')
+ .mockImplementation(() => Promise.resolve());
+ return store.dispatch('currentChannel/channelLanguageExistsInResources').then(() => {
+ expect(spy.mock.calls[0][0]).toBe(store.state.currentChannel.currentChannelId);
+ spy.mockRestore();
+ });
+ });
+ it('getLanguagesInChannelResources action should call `languages` endpoint', () => {
+ const spy = jest
+ .spyOn(Channel, 'languagesInResources')
+ .mockImplementation(() => Promise.resolve());
+ return store.dispatch('currentChannel/getLanguagesInChannelResources').then(() => {
+ expect(spy.mock.calls[0][0]).toBe(store.state.currentChannel.currentChannelId);
+ spy.mockRestore();
+ });
+ });
});
});
diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/actions.js
index 9dca09a0a2..d640b905af 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/actions.js
+++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/currentChannel/actions.js
@@ -54,3 +54,13 @@ export function deployCurrentChannel(context) {
export function publishChannel(context, version_notes) {
return Channel.publish(context.state.currentChannelId, version_notes);
}
+
+export function channelLanguageExistsInResources(context) {
+ const channelId = context.state.currentChannelId;
+ return Channel.languageExistsInResources(channelId);
+}
+
+export function getLanguagesInChannelResources(context) {
+ const channelId = context.state.currentChannelId;
+ return Channel.languagesInResources(channelId);
+}
diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js
index 1c684ee07a..f89845dd18 100644
--- a/contentcuration/contentcuration/frontend/shared/data/resources.js
+++ b/contentcuration/contentcuration/frontend/shared/data/resources.js
@@ -7,6 +7,7 @@ import isString from 'lodash/isString';
import matches from 'lodash/matches';
import overEvery from 'lodash/overEvery';
import sortBy from 'lodash/sortBy';
+import compact from 'lodash/compact';
import uniq from 'lodash/uniq';
import uniqBy from 'lodash/uniqBy';
@@ -1271,6 +1272,44 @@ export const Channel = new CreateModelResource({
// For channels, the appropriate channel_id for a change is just the key
return obj.id;
},
+ async languageExistsInResources(id) {
+ let langExists = await this.transaction(
+ { mode: 'r' },
+ TABLE_NAMES.CHANNEL,
+ TABLE_NAMES.CONTENTNODE,
+ () => {
+ return Channel.table.get(id).then(async channel => {
+ return (
+ (await ContentNode.table
+ .where({
+ channel_id: id,
+ language: channel.language,
+ })
+ .count()) > 0
+ );
+ });
+ }
+ );
+ if (!langExists) {
+ langExists = await client
+ .get(this.getUrlFunction('language_exists')(id))
+ .then(response => response.data.exists);
+ }
+ return langExists;
+ },
+ async languagesInResources(id) {
+ const localLanguages = await this.transaction({ mode: 'r' }, TABLE_NAMES.CONTENTNODE, () => {
+ return ContentNode.table
+ .where({ channel_id: id })
+ .filter(node => node.language !== null)
+ .toArray()
+ .then(nodes => nodes.map(node => node.language));
+ });
+ const remoteLanguages = await client
+ .get(this.getUrlFunction('languages')(id))
+ .then(response => response.data.languages);
+ return uniq(compact(localLanguages.concat(remoteLanguages)));
+ },
});
function getChannelFromChannelScope() {
diff --git a/contentcuration/contentcuration/tests/viewsets/test_channel.py b/contentcuration/contentcuration/tests/viewsets/test_channel.py
index 271367e58f..17549ab128 100644
--- a/contentcuration/contentcuration/tests/viewsets/test_channel.py
+++ b/contentcuration/contentcuration/tests/viewsets/test_channel.py
@@ -6,11 +6,13 @@
from django.db.models import Exists
from django.db.models import OuterRef
from django.urls import reverse
+from kolibri_public.models import ContentNode as PublicContentNode
from le_utils.constants import content_kinds
from contentcuration import models
from contentcuration import models as cc
from contentcuration.constants import channel_history
+from contentcuration.models import ContentNode
from contentcuration.tests import testdata
from contentcuration.tests.base import StudioAPITestCase
from contentcuration.tests.viewsets.base import generate_create_event
@@ -614,3 +616,93 @@ def test_unpublished_changes_query_no_publishable_since_publish_if_publish_fails
unpublished_changes = _unpublished_changes_query(outer_ref)
channels = models.Channel.objects.filter(pk=channel.pk).annotate(unpublished_changes=Exists(unpublished_changes))
self.assertFalse(channels[0].unpublished_changes)
+
+
+class ChannelLanguageTestCase(StudioAPITestCase):
+
+ def setUp(self):
+ super(ChannelLanguageTestCase, self).setUp()
+ self.channel = testdata.channel()
+ self.channel.language_id = 'en'
+ self.channel.save()
+
+ self.channel_id = self.channel.id
+ self.node_id = '00000000000000000000000000000003'
+ self.public_node = PublicContentNode.objects.create(
+ id=uuid.UUID(self.node_id),
+ title='Video 1',
+ content_id=uuid.uuid4(),
+ channel_id=uuid.UUID(self.channel.id),
+ lang_id='en',
+ )
+
+ def test_channel_language_exists_valid_channel(self):
+
+ ContentNode.objects.filter(node_id=self.public_node.id).update(language_id='en')
+ response = self._perform_action("channel-language-exists", self.channel.id)
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertTrue(response.json()["exists"])
+
+ def test_channel_language_doesnt_exists_valid_channel(self):
+
+ PublicContentNode.objects.filter(id=self.public_node.id).update(lang_id='es')
+ response = self._perform_action("channel-language-exists", self.channel.id)
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertFalse(response.json()["exists"])
+
+ def test_channel_language_exists_invalid_channel(self):
+
+ response = self._perform_action("channel-language-exists", 'unknown_channel_id')
+ self.assertEqual(response.status_code, 404, response.content)
+
+ def test_channel_language_exists_invalid_request(self):
+
+ response = self._perform_action("channel-language-exists", None)
+ self.assertEqual(response.status_code, 404, response.content)
+
+ def test_get_languages_in_channel_success_languages(self):
+ new_language = 'swa'
+ self.channel.language_id = new_language
+ self.channel.save()
+ PublicContentNode.objects.filter(id=self.public_node.id).update(lang_id=new_language)
+ ContentNode.objects.filter(node_id=self.public_node.id).update(language_id=new_language)
+
+ response = self._perform_action("channel-languages", self.channel.id)
+ languages = response.json()["languages"]
+
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertListEqual(languages, [new_language])
+
+ def test_get_languages_in_channel_success_channel_language_excluded(self):
+ new_language = 'fr'
+ channel_lang = 'en'
+ self.channel.language_id = channel_lang
+ self.channel.save()
+ PublicContentNode.objects.filter(id=self.public_node.id).update(lang_id=new_language)
+ ContentNode.objects.filter(node_id=self.public_node.id).update(language_id=new_language)
+
+ response = self._perform_action("channel-languages", self.channel.id)
+ languages = response.json()["languages"]
+
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertListEqual(languages, [new_language])
+ self.assertFalse(channel_lang in languages)
+
+ def test_get_languages_in_channel_success_no_languages(self):
+
+ response = self._perform_action("channel-languages", self.channel.id)
+ languages = response.json()["languages"]
+
+ self.assertEqual(response.status_code, 200, response.content)
+ self.assertListEqual(languages, [])
+
+ def test_get_languages_in_channel_invalid_request(self):
+
+ response = self._perform_action("channel-languages", None)
+ self.assertEqual(response.status_code, 404, response.content)
+
+ def _perform_action(self, url_path, channel_id):
+ user = testdata.user()
+ self.client.force_authenticate(user=user)
+ response = self.client.get(reverse(url_path, kwargs={"pk": channel_id}), format="json")
+ return response
diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py
index ee06244836..796f7999e8 100644
--- a/contentcuration/contentcuration/viewsets/channel.py
+++ b/contentcuration/contentcuration/viewsets/channel.py
@@ -1,23 +1,35 @@
import logging
+import uuid
from functools import reduce
from operator import or_
+from typing import Dict
+from typing import List
+from typing import Union
from django.conf import settings
from django.db import IntegrityError
from django.db.models import Exists
+from django.db.models import F
from django.db.models import OuterRef
from django.db.models import Q
from django.db.models import Subquery
+from django.db.models import UUIDField
from django.db.models import Value
+from django.db.models.functions import Cast
from django.db.models.functions import Coalesce
+from django.http import HttpResponse
+from django.http import HttpResponseNotFound
+from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django_cte import With
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import CharFilter
+from kolibri_public.models import ContentNode as PublicContentNode
from le_utils.constants import content_kinds
from le_utils.constants import roles
from rest_framework import serializers
+from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import AllowAny
from rest_framework.permissions import IsAuthenticated
@@ -85,7 +97,6 @@ class CatalogListPagination(CachedListPagination):
.order_by("-token")[:1]
)
-
base_channel_filter_fields = (
"keywords",
"published",
@@ -135,9 +146,11 @@ def filter_keywords(self, queryset, name, value):
dash_replaced_search_query = get_fts_search_query(value.replace("-", ""))
channel_keywords_query = (Exists(ChannelFullTextSearch.objects.filter(
- Q(keywords_tsvector=search_query) | Q(keywords_tsvector=dash_replaced_search_query), channel_id=OuterRef("id"))))
+ Q(keywords_tsvector=search_query) | Q(keywords_tsvector=dash_replaced_search_query),
+ channel_id=OuterRef("id"))))
contentnode_search_query = (Exists(ContentNodeFullTextSearch.objects.filter(
- Q(keywords_tsvector=search_query) | Q(author_tsvector=search_query), channel_id=OuterRef("id"))))
+ Q(keywords_tsvector=search_query) | Q(author_tsvector=search_query),
+ channel_id=OuterRef("id"))))
return queryset.filter(Q(channel_keywords_query) | Q(contentnode_search_query))
@@ -427,7 +440,9 @@ def create(self, request, *args, **kwargs):
except IntegrityError as e:
return Response({"error": str(e)}, status=409)
instance = serializer.instance
- Change.create_change(generate_create_event(instance.id, CHANNEL, request.data, channel_id=instance.id), applied=True, created_by_id=request.user.id)
+ Change.create_change(
+ generate_create_event(instance.id, CHANNEL, request.data, channel_id=instance.id),
+ applied=True, created_by_id=request.user.id)
return Response(self.serialize_object(pk=instance.pk), status=HTTP_201_CREATED)
def destroy(self, request, *args, **kwargs):
@@ -480,7 +495,8 @@ def annotate_queryset(self, queryset):
count=SQCount(non_topic_content_ids, field="content_id"),
)
- queryset = queryset.annotate(unpublished_changes=Exists(_unpublished_changes_query(OuterRef("id"))))
+ queryset = queryset.annotate(
+ unpublished_changes=Exists(_unpublished_changes_query(OuterRef("id"))))
return queryset
@@ -490,7 +506,8 @@ def publish_from_changes(self, changes):
# Publish change will have key, version_notes, and language.
try:
self.publish(
- publish["key"], version_notes=publish.get("version_notes"), language=publish.get("language")
+ publish["key"], version_notes=publish.get("version_notes"),
+ language=publish.get("language")
)
except Exception as e:
log_sync_exception(e, user=self.request.user, change=publish)
@@ -510,7 +527,8 @@ def publish(self, pk, version_notes="", language=None):
channel.mark_publishing(self.request.user)
- with create_change_tracker(pk, CHANNEL, channel.id, self.request.user, "export-channel") as progress_tracker:
+ with create_change_tracker(pk, CHANNEL, channel.id, self.request.user,
+ "export-channel") as progress_tracker:
try:
channel = publish_channel(
self.request.user.pk,
@@ -541,7 +559,8 @@ def publish(self, pk, version_notes="", language=None):
except Exception:
Change.create_changes([
generate_update_event(
- channel.id, CHANNEL, {"publishing": False, "unpublished_changes": True}, channel_id=channel.id
+ channel.id, CHANNEL, {"publishing": False, "unpublished_changes": True},
+ channel_id=channel.id
),
], applied=True, unpublishable=True)
raise
@@ -564,7 +583,8 @@ def sync_from_changes(self, changes):
errors.append(sync)
return errors
- def sync(self, pk, titles_and_descriptions=False, resource_details=False, files=False, assessment_items=False):
+ def sync(self, pk, titles_and_descriptions=False, resource_details=False, files=False,
+ assessment_items=False):
logging.debug("Entering the sync channel endpoint")
channel = self.get_edit_queryset().get(pk=pk)
@@ -574,18 +594,19 @@ def sync(self, pk, titles_and_descriptions=False, resource_details=False, files=
if (
not channel.main_tree.get_descendants()
- .filter(
+ .filter(
Q(original_node__isnull=False)
| Q(
original_channel_id__isnull=False,
original_source_node_id__isnull=False,
)
)
- .exists()
+ .exists()
):
raise ValidationError("Cannot sync a channel with no imported content")
- with create_change_tracker(pk, CHANNEL, channel.id, self.request.user, "sync-channel") as progress_tracker:
+ with create_change_tracker(pk, CHANNEL, channel.id, self.request.user,
+ "sync-channel") as progress_tracker:
sync_channel(
channel,
titles_and_descriptions,
@@ -641,6 +662,108 @@ def deploy(self, user, pk):
channel_id=channel.id,
), applied=True, created_by_id=user.id)
+ @action(detail=True, methods=["get"], url_path='language_exists', url_name='language-exists')
+ def channel_language_exists(self, request, pk=None) -> Union[JsonResponse, HttpResponse]:
+ """
+ Verify that the language set for a channel is present in at least one of its resources.
+
+ :param request: The request object
+ :param pk: The ID of the channel
+ :return: JsonResponse with exists=True if the language exists, False otherwise
+ :rtype: JsonResponse
+ """
+ if not self._channel_exists(pk):
+ return HttpResponseNotFound("No channel matching: {}".format(pk))
+
+ channel_lang, main_tree_id = self._get_channel_details(pk).values()
+ langs_in_channel = self._get_channel_content_languages(pk, main_tree_id)
+ lang_exists = channel_lang in langs_in_channel
+
+ return JsonResponse({"exists": lang_exists})
+
+ @action(detail=True, methods=["get"], url_path='languages', url_name='languages')
+ def get_languages_in_channel(self, request, pk=None) -> Union[JsonResponse, HttpResponse]:
+ """
+ Get all the languages present in a channel's resources.
+
+ :param request: The request object
+ :param pk: The ID of the channel
+ :return: JsonResponse with a list of languages present in the channel
+ :rtype: JsonResponse
+ """
+ if not self._channel_exists(pk):
+ return HttpResponseNotFound("No channel matching: {}".format(pk))
+
+ channel_details = self._get_channel_details(pk)
+ main_tree_id = channel_details.get("main_tree_id")
+ langs_in_content = self._get_channel_content_languages(pk, main_tree_id)
+ return JsonResponse({"languages": langs_in_content})
+
+ def _channel_exists(self, channel_id) -> bool:
+ """
+ Check if a channel exists.
+
+ :param channel_id: The ID of the channel
+ :return: True if the channel exists, False otherwise
+ :rtype: bool
+ """
+ try:
+ return Channel.objects.filter(pk=channel_id).exists()
+ except Exception as e:
+ logging.error(f"Error checking if channel exists: {e}")
+ return False
+
+ def _get_channel_details(self, channel_id) -> Dict[str, any]:
+ """
+ Get the language set for a channel.
+
+ :param channel_id: The ID of the channel
+ :return: The language code set for the channel
+ :rtype: str
+ """
+ try:
+ channel_details = (Channel.objects.filter(pk=channel_id)
+ .values("language_id", "main_tree_id").first())
+ except Channel.DoesNotExist as e:
+ logging.error(str(e))
+ channel_details = None
+
+ if not channel_details:
+ channel_details = dict(language_id=None, main_tree_id=None)
+
+ return channel_details
+
+ def _get_channel_content_languages(self, channel_id, main_tree_id=None) -> List[str]:
+ """
+ Get all the languages used in a channel's resources.
+
+ :param channel_id: The ID of the channel
+ :return: A list of language codes used in the channel
+ :rtype: List[str]
+ """
+ if not channel_id:
+ return []
+
+ try:
+ ids_to_exclude = [main_tree_id] if main_tree_id else []
+ node_ids_subquery = PublicContentNode.objects.filter(
+ channel_id=uuid.UUID(channel_id),
+ ).values_list("id", flat=True)
+ lang_ids = ContentNode.objects.filter(
+ language_id__isnull=False
+ ).annotate(
+ cast_node_id=Cast(F('node_id'), output_field=UUIDField())
+ ).filter(
+ cast_node_id__in=Subquery(node_ids_subquery)
+ ).exclude(
+ id__in=ids_to_exclude
+ ).values_list('language_id', flat=True).distinct()
+ unique_lang_ids = list(set(lang_ids))
+ except Exception as e:
+ logging.error(str(e))
+ unique_lang_ids = []
+ return unique_lang_ids
+
@method_decorator(
cache_page(
@@ -710,7 +833,7 @@ def filter_keywords(self, queryset, name, value):
editors_first_name = reduce(or_, (Q(editors__first_name__icontains=k) for k in keywords))
editors_last_name = reduce(or_, (Q(editors__last_name__icontains=k) for k in keywords))
editors_email = reduce(or_, (Q(editors__email__icontains=k) for k in keywords))
- return queryset.annotate(primary_token=primary_token_subquery,).filter(
+ return queryset.annotate(primary_token=primary_token_subquery, ).filter(
Q(name__icontains=value)
| Q(pk__istartswith=value)
| Q(primary_token=value.replace("-", ""))
@@ -727,6 +850,7 @@ class AdminChannelSerializer(ChannelSerializer):
This is a write only serializer - we leverage it to do create and update
operations, but read operations are handled by the Viewset.
"""
+
class Meta:
model = Channel
fields = (
@@ -785,7 +909,9 @@ def update(self, request, *args, **kwargs):
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
- Change.create_change(generate_update_event(instance.id, CHANNEL, request.data, channel_id=instance.id), applied=True, created_by_id=request.user.id)
+ Change.create_change(
+ generate_update_event(instance.id, CHANNEL, request.data, channel_id=instance.id),
+ applied=True, created_by_id=request.user.id)
return Response(self.serialize_object())