diff --git a/contentcuration/contentcuration/frontend/channelEdit/router.js b/contentcuration/contentcuration/frontend/channelEdit/router.js index 6ef3473fd4..1336a434b8 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/router.js +++ b/contentcuration/contentcuration/frontend/channelEdit/router.js @@ -38,6 +38,21 @@ const router = new VueRouter({ path: '/import/:destNodeId/browse/:channelId?/:nodeId?', component: SearchOrBrowseWindow, props: true, + beforeEnter: (to, from, next) => { + const promises = [ + // search recommendations require ancestors to be loaded + store.dispatch('contentNode/loadAncestors', { id: to.params.destNodeId }), + ]; + + if (!store.getters['currentChannel/currentChannel']) { + // ensure the current channel is loaded, in case of hard refresh on this route. + // alternatively, the page could be reactive to this getter's value, although that doesn't + // seem to work properly + promises.push(store.dispatch('currentChannel/loadChannel')); + } + + return Promise.all(promises).then(() => next()); + }, }, { name: RouteNames.IMPORT_FROM_CHANNELS_SEARCH, diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue index 632b0ec76b..88484cec8b 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue @@ -22,7 +22,7 @@ @@ -95,7 +95,7 @@ { + return `${err.instancePath}: ${err.message}`; + }), + ), + ), + ); + return false; + } + + return true; + }, loadMoreRecommendationsText() { let link = null; let description = null; @@ -329,11 +357,11 @@ }, browseWindowStyle() { return { - maxWidth: this.isAIFeatureEnabled ? '1200px' : '800px', + maxWidth: this.shouldShowRecommendations ? '1200px' : '800px', }; }, topicId() { - return this.importDestinationFolder?.id; + return this.$route.params.destNodeId; }, recommendationsSectionTitle() { return this.resourcesMightBeRelevantTitle$({ @@ -361,13 +389,14 @@ description: this.importDestinationFolder.description, language: this.recommendationsLanguage, ancestors: this.topicAncestors, + channel_id: formatUUID4(this.importDestinationFolder.channel_id), }, ], - metadata: { - channel_id: formatUUID4(this.importDestinationFolder.channel_id), - }, }; }, + importDestinationAncestors() { + return this.getContentNodeAncestors(this.topicId, true); + }, importDestinationFolder() { return this.importDestinationAncestors.slice(-1)[0]; }, @@ -405,14 +434,11 @@ }, mounted() { this.searchTerm = this.$route.params.searchTerm || ''; - this.loadAncestors({ id: this.$route.params.destNodeId }).then(ancestors => { - this.importDestinationAncestors = ancestors; - this.loadRecommendations(this.recommendationsBelowThreshold); - }); + this.loadRecommendations(this.recommendationsBelowThreshold); }, methods: { ...mapActions('clipboard', ['copy']), - ...mapActions('contentNode', ['loadAncestors', 'loadPublicContentNode']), + ...mapActions('contentNode', ['loadPublicContentNode']), ...mapActions('importFromChannels', ['fetchRecommendations']), ...mapMutations('importFromChannels', { selectNodes: 'SELECT_NODES', @@ -504,7 +530,7 @@ } }, async loadRecommendations(belowThreshold) { - if (this.isAIFeatureEnabled) { + if (this.shouldShowRecommendations) { this.recommendationsLoading = true; this.recommendationsLoadingError = false; try { diff --git a/contentcuration/contentcuration/frontend/RecommendedResourceCard/components/RecommendedResourceCard.vue b/contentcuration/contentcuration/frontend/shared/views/RecommendedResourceCard.vue similarity index 100% rename from contentcuration/contentcuration/frontend/RecommendedResourceCard/components/RecommendedResourceCard.vue rename to contentcuration/contentcuration/frontend/shared/views/RecommendedResourceCard.vue diff --git a/contentcuration/contentcuration/tests/viewsets/test_recommendations.py b/contentcuration/contentcuration/tests/viewsets/test_recommendations.py index 67c96b05b3..b335ecca62 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_recommendations.py +++ b/contentcuration/contentcuration/tests/viewsets/test_recommendations.py @@ -1,3 +1,5 @@ +import uuid + from automation.utils.appnexus import errors from django.urls import reverse from le_utils.constants import content_kinds @@ -9,19 +11,20 @@ from contentcuration.tests.base import StudioAPITestCase -class CRUDTestCase(StudioAPITestCase): +class RecommendationsCRUDTestCase(StudioAPITestCase): @property def topics(self): return { "topics": [ { - "id": "00000000000000000000000000000001", + "id": str(uuid.uuid4()), + "channel_id": str(uuid.uuid4()), "title": "Target topic", "description": "Target description", "language": "en", "ancestors": [ { - "id": "00000000000000000000000000000001", + "id": str(uuid.uuid4()), "title": "Parent topic", "description": "Parent description", "language": "en", @@ -55,7 +58,7 @@ def recommendations_list(self): ] def setUp(self): - super(CRUDTestCase, self).setUp() + super(RecommendationsCRUDTestCase, self).setUp() @patch( "contentcuration.utils.automation_manager.AutomationManager.load_recommendations" diff --git a/contentcuration/contentcuration/viewsets/recommendation.py b/contentcuration/contentcuration/viewsets/recommendation.py index 29483a70e1..7d1a3549ab 100644 --- a/contentcuration/contentcuration/viewsets/recommendation.py +++ b/contentcuration/contentcuration/viewsets/recommendation.py @@ -5,7 +5,7 @@ from automation.utils.appnexus import errors from django.http import HttpResponseServerError from django.http import JsonResponse -from le_utils.validators import embed_topics_request +from le_utils.constants import embed_topics_request from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView @@ -15,6 +15,10 @@ logger = logging.getLogger(__name__) +def validate_recommendations_request(data): + jsonschema.validate(instance=data, schema=embed_topics_request.SCHEMA) + + class RecommendationView(APIView): permission_classes = [ @@ -29,7 +33,7 @@ def post(self, request): # Remove and store override_threshold as it isn't defined in the schema override_threshold = request_data.pop("override_threshold", False) - embed_topics_request.validate(request_data) + validate_recommendations_request(request_data) except jsonschema.ValidationError as e: logger.error("Schema validation error: %s", str(e)) return JsonResponse( diff --git a/package.json b/package.json index 3cbd6cdb08..72f806ecd0 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "jquery": "^2.2.4", "jspdf": "https://github.com/parallax/jsPDF.git#b7a1d8239c596292ce86dafa77f05987bcfa2e6e", "jszip": "^3.10.1", - "kolibri-constants": "^0.2.0", + "kolibri-constants": "^0.2.12", "kolibri-design-system": "5.2.0", "lodash": "^4.17.21", "material-icons": "0.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc3c3cf8b7..314085a2ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^3.10.1 version: 3.10.1 kolibri-constants: - specifier: ^0.2.0 - version: 0.2.9 + specifier: ^0.2.12 + version: 0.2.12 kolibri-design-system: specifier: 5.2.0 version: 5.2.0 @@ -4583,6 +4583,9 @@ packages: known-css-properties@0.35.0: resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + kolibri-constants@0.2.12: + resolution: {integrity: sha512-ApVc/KLwEaDJohqKhQTdao4UdWmoyq2pQ5lk8ra+1rDpJvsFWsAOGVC4RTv4YEDlAYJzj2/QZlJQ91u5yUURSQ==} + kolibri-constants@0.2.9: resolution: {integrity: sha512-frLKQPA5bSQczVEh/lnfHeQcK9i4MvGzgl+KMqvP+ixh+VJxXjlpSG+k119ajQqgq9k+nJb/IY4HbiLTjBM6oQ==} @@ -12527,6 +12530,8 @@ snapshots: known-css-properties@0.35.0: {} + kolibri-constants@0.2.12: {} + kolibri-constants@0.2.9: {} kolibri-design-system@5.0.1: diff --git a/requirements.in b/requirements.in index 303cb688fc..cffdacedad 100644 --- a/requirements.in +++ b/requirements.in @@ -5,7 +5,7 @@ djangorestframework==3.15.1 psycopg2-binary==2.9.10 django-js-reverse==0.10.2 django-registration==3.4 -le-utils==0.2.10 +le-utils>=0.2.12 gunicorn==23.0.0 django-postmark==0.1.6 jsonfield==3.1.0 diff --git a/requirements.txt b/requirements.txt index e096b4e443..3ff533e5ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -154,7 +154,7 @@ jsonschema-specifications==2024.10.1 # via jsonschema kombu==5.5.2 # via celery -le-utils==0.2.10 +le-utils==0.2.12 # via -r requirements.in packaging==25.0 # via