Skip to content

Commit 5a96a25

Browse files
authored
Merge pull request #5125 from bjester/recommendation-language
Add defensiveness against missing language for recommendations
2 parents ba7b466 + 0339896 commit 5a96a25

9 files changed

Lines changed: 79 additions & 26 deletions

File tree

contentcuration/contentcuration/frontend/channelEdit/router.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ const router = new VueRouter({
3838
path: '/import/:destNodeId/browse/:channelId?/:nodeId?',
3939
component: SearchOrBrowseWindow,
4040
props: true,
41+
beforeEnter: (to, from, next) => {
42+
const promises = [
43+
// search recommendations require ancestors to be loaded
44+
store.dispatch('contentNode/loadAncestors', { id: to.params.destNodeId }),
45+
];
46+
47+
if (!store.getters['currentChannel/currentChannel']) {
48+
// ensure the current channel is loaded, in case of hard refresh on this route.
49+
// alternatively, the page could be reactive to this getter's value, although that doesn't
50+
// seem to work properly
51+
promises.push(store.dispatch('currentChannel/loadChannel'));
52+
}
53+
54+
return Promise.all(promises).then(() => next());
55+
},
4156
},
4257
{
4358
name: RouteNames.IMPORT_FROM_CHANNELS_SEARCH,

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
<!-- Main panel >= 800px -->
2424
<KGridItem
25-
:layout12="{ span: isAIFeatureEnabled && layoutFitsTwoColumns ? 8 : 12 }"
25+
:layout12="{ span: shouldShowRecommendations && layoutFitsTwoColumns ? 8 : 12 }"
2626
:layout8="{ span: 8 }"
2727
:layout4="{ span: 4 }"
2828
>
@@ -95,7 +95,7 @@
9595

9696
<!-- Recommended resources panel >= 400px -->
9797
<KGridItem
98-
v-if="isAIFeatureEnabled"
98+
v-if="shouldShowRecommendations"
9999
:layout12="{ span: layoutFitsTwoColumns ? 4 : 12 }"
100100
:layout8="{ span: 8 }"
101101
:layout4="{ span: 4 }"
@@ -183,16 +183,21 @@
183183
import { mapActions, mapGetters, mapMutations, mapState } from 'vuex';
184184
import { computed } from 'vue';
185185
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
186+
import { SCHEMA } from 'kolibri-constants/EmbedTopicsRequest';
186187
import { RouteNames } from '../../constants';
187-
import RecommendedResourceCard from '../../../RecommendedResourceCard/components/RecommendedResourceCard';
188188
import ChannelList from './ChannelList';
189189
import ContentTreeList from './ContentTreeList';
190190
import SearchResultsList from './SearchResultsList';
191191
import SavedSearchesModal from './SavedSearchesModal';
192192
import ImportFromChannelsModal from './ImportFromChannelsModal';
193+
import logging from 'shared/logging';
194+
import RecommendedResourceCard from 'shared/views/RecommendedResourceCard';
193195
import { withChangeTracker } from 'shared/data/changes';
194196
import { formatUUID4 } from 'shared/data/resources';
195197
import { searchRecommendationsStrings } from 'shared/strings/searchRecommendationsStrings';
198+
import { compile } from 'shared/utils/jsonSchema';
199+
200+
const validateEmbedTopicRequest = compile(SCHEMA);
196201
197202
export default {
198203
name: 'SearchOrBrowseWindow',
@@ -249,7 +254,6 @@
249254
copyNode: null,
250255
languageFromChannelList: null,
251256
showSavedSearches: false,
252-
importDestinationAncestors: [],
253257
showAboutRecommendations: false,
254258
recommendations: [],
255259
otherRecommendations: [],
@@ -266,6 +270,7 @@
266270
};
267271
},
268272
computed: {
273+
...mapGetters('contentNode', ['getContentNodeAncestors']),
269274
...mapGetters('currentChannel', ['currentChannel']),
270275
...mapGetters('importFromChannels', ['savedSearchesExist']),
271276
...mapGetters(['isAIFeatureEnabled']),
@@ -291,6 +296,29 @@
291296
this.searchTerm.trim() !== this.$route.params.searchTerm
292297
);
293298
},
299+
shouldShowRecommendations() {
300+
if (!this.isAIFeatureEnabled) {
301+
return false;
302+
}
303+
304+
if (!validateEmbedTopicRequest(this.embedTopicRequest)) {
305+
// log to sentry-- this is unexpected, since we use the channel's language as a fallback
306+
// and channels are required to have a language
307+
logging.error(
308+
new Error(
309+
'Recommendation request is invalid: ' +
310+
JSON.stringify(
311+
validateEmbedTopicRequest.errors.map(err => {
312+
return `${err.instancePath}: ${err.message}`;
313+
}),
314+
),
315+
),
316+
);
317+
return false;
318+
}
319+
320+
return true;
321+
},
294322
loadMoreRecommendationsText() {
295323
let link = null;
296324
let description = null;
@@ -329,11 +357,11 @@
329357
},
330358
browseWindowStyle() {
331359
return {
332-
maxWidth: this.isAIFeatureEnabled ? '1200px' : '800px',
360+
maxWidth: this.shouldShowRecommendations ? '1200px' : '800px',
333361
};
334362
},
335363
topicId() {
336-
return this.importDestinationFolder?.id;
364+
return this.$route.params.destNodeId;
337365
},
338366
recommendationsSectionTitle() {
339367
return this.resourcesMightBeRelevantTitle$({
@@ -361,13 +389,14 @@
361389
description: this.importDestinationFolder.description,
362390
language: this.recommendationsLanguage,
363391
ancestors: this.topicAncestors,
392+
channel_id: formatUUID4(this.importDestinationFolder.channel_id),
364393
},
365394
],
366-
metadata: {
367-
channel_id: formatUUID4(this.importDestinationFolder.channel_id),
368-
},
369395
};
370396
},
397+
importDestinationAncestors() {
398+
return this.getContentNodeAncestors(this.topicId, true);
399+
},
371400
importDestinationFolder() {
372401
return this.importDestinationAncestors.slice(-1)[0];
373402
},
@@ -405,14 +434,11 @@
405434
},
406435
mounted() {
407436
this.searchTerm = this.$route.params.searchTerm || '';
408-
this.loadAncestors({ id: this.$route.params.destNodeId }).then(ancestors => {
409-
this.importDestinationAncestors = ancestors;
410-
this.loadRecommendations(this.recommendationsBelowThreshold);
411-
});
437+
this.loadRecommendations(this.recommendationsBelowThreshold);
412438
},
413439
methods: {
414440
...mapActions('clipboard', ['copy']),
415-
...mapActions('contentNode', ['loadAncestors', 'loadPublicContentNode']),
441+
...mapActions('contentNode', ['loadPublicContentNode']),
416442
...mapActions('importFromChannels', ['fetchRecommendations']),
417443
...mapMutations('importFromChannels', {
418444
selectNodes: 'SELECT_NODES',
@@ -504,7 +530,7 @@
504530
}
505531
},
506532
async loadRecommendations(belowThreshold) {
507-
if (this.isAIFeatureEnabled) {
533+
if (this.shouldShowRecommendations) {
508534
this.recommendationsLoading = true;
509535
this.recommendationsLoadingError = false;
510536
try {

contentcuration/contentcuration/frontend/RecommendedResourceCard/components/RecommendedResourceCard.vue renamed to contentcuration/contentcuration/frontend/shared/views/RecommendedResourceCard.vue

File renamed without changes.

contentcuration/contentcuration/tests/viewsets/test_recommendations.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
from automation.utils.appnexus import errors
24
from django.urls import reverse
35
from le_utils.constants import content_kinds
@@ -9,19 +11,20 @@
911
from contentcuration.tests.base import StudioAPITestCase
1012

1113

12-
class CRUDTestCase(StudioAPITestCase):
14+
class RecommendationsCRUDTestCase(StudioAPITestCase):
1315
@property
1416
def topics(self):
1517
return {
1618
"topics": [
1719
{
18-
"id": "00000000000000000000000000000001",
20+
"id": str(uuid.uuid4()),
21+
"channel_id": str(uuid.uuid4()),
1922
"title": "Target topic",
2023
"description": "Target description",
2124
"language": "en",
2225
"ancestors": [
2326
{
24-
"id": "00000000000000000000000000000001",
27+
"id": str(uuid.uuid4()),
2528
"title": "Parent topic",
2629
"description": "Parent description",
2730
"language": "en",
@@ -55,7 +58,7 @@ def recommendations_list(self):
5558
]
5659

5760
def setUp(self):
58-
super(CRUDTestCase, self).setUp()
61+
super(RecommendationsCRUDTestCase, self).setUp()
5962

6063
@patch(
6164
"contentcuration.utils.automation_manager.AutomationManager.load_recommendations"

contentcuration/contentcuration/viewsets/recommendation.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from automation.utils.appnexus import errors
66
from django.http import HttpResponseServerError
77
from django.http import JsonResponse
8-
from le_utils.validators import embed_topics_request
8+
from le_utils.constants import embed_topics_request
99
from rest_framework.permissions import IsAuthenticated
1010
from rest_framework.views import APIView
1111

@@ -15,6 +15,10 @@
1515
logger = logging.getLogger(__name__)
1616

1717

18+
def validate_recommendations_request(data):
19+
jsonschema.validate(instance=data, schema=embed_topics_request.SCHEMA)
20+
21+
1822
class RecommendationView(APIView):
1923

2024
permission_classes = [
@@ -29,7 +33,7 @@ def post(self, request):
2933
# Remove and store override_threshold as it isn't defined in the schema
3034
override_threshold = request_data.pop("override_threshold", False)
3135

32-
embed_topics_request.validate(request_data)
36+
validate_recommendations_request(request_data)
3337
except jsonschema.ValidationError as e:
3438
logger.error("Schema validation error: %s", str(e))
3539
return JsonResponse(

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"jquery": "^2.2.4",
7272
"jspdf": "https://github.com/parallax/jsPDF.git#b7a1d8239c596292ce86dafa77f05987bcfa2e6e",
7373
"jszip": "^3.10.1",
74-
"kolibri-constants": "^0.2.0",
74+
"kolibri-constants": "^0.2.12",
7575
"kolibri-design-system": "5.2.0",
7676
"lodash": "^4.17.21",
7777
"material-icons": "0.3.1",

pnpm-lock.yaml

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ djangorestframework==3.15.1
55
psycopg2-binary==2.9.10
66
django-js-reverse==0.10.2
77
django-registration==3.4
8-
le-utils==0.2.10
8+
le-utils>=0.2.12
99
gunicorn==23.0.0
1010
django-postmark==0.1.6
1111
jsonfield==3.1.0

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ jsonschema-specifications==2024.10.1
154154
# via jsonschema
155155
kombu==5.5.2
156156
# via celery
157-
le-utils==0.2.10
157+
le-utils==0.2.12
158158
# via -r requirements.in
159159
packaging==25.0
160160
# via

0 commit comments

Comments
 (0)