Skip to content

Commit e15d8b9

Browse files
nygrenhRedande
andauthored
Make popular courses only show courses in the user's current language (#1289)
* Make popular courses only show courses in the user's current language * Generate graphql queries --------- Co-authored-by: Antti Leinonen <[email protected]>
1 parent 7334d72 commit e15d8b9

File tree

7 files changed

+247
-13
lines changed

7 files changed

+247
-13
lines changed

backend/schema/Course/__test__/Course.queries.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,121 @@ describe("Course", () => {
323323
})
324324
})
325325

326+
describe("popularCourses", () => {
327+
beforeEach(async () => {
328+
await seed(ctx.prisma)
329+
330+
await ctx.prisma.course.update({
331+
where: { slug: "course2" },
332+
data: { status: "Ended" },
333+
})
334+
335+
await ctx.prisma.course.update({
336+
where: { slug: "handled" },
337+
data: {
338+
status: "Active",
339+
course_translations: {
340+
create: {
341+
language: "fi_FI",
342+
name: "handled_fi_FI",
343+
description: "handled course description",
344+
created_at: "1900-01-01T10:00:00.00+02:00",
345+
updated_at: "1900-01-01T10:00:00.00+02:00",
346+
},
347+
},
348+
},
349+
})
350+
})
351+
352+
it("returns courses filtered by language", async () => {
353+
const res = await ctx.client.request<any>(popularCoursesQuery, {
354+
popularCoursesForLanguage: "fi_FI",
355+
})
356+
357+
expect(res.popularCourses).toBeDefined()
358+
expect(Array.isArray(res.popularCourses)).toBe(true)
359+
expect(res.popularCourses.length).toBeGreaterThan(0)
360+
expect(res.popularCourses.length).toBeLessThanOrEqual(4)
361+
362+
res.popularCourses.forEach((course: any) => {
363+
expect(course).toHaveProperty("id")
364+
expect(course).toHaveProperty("slug")
365+
expect(course).toHaveProperty("name")
366+
expect(course).toHaveProperty("description")
367+
expect(course).toHaveProperty("link")
368+
})
369+
})
370+
371+
it("excludes ended courses", async () => {
372+
const res = await ctx.client.request<any>(popularCoursesQuery, {
373+
popularCoursesForLanguage: "fi_FI",
374+
})
375+
376+
const endedCourseSlugs = res.popularCourses
377+
.map((c: any) => c.slug)
378+
.filter((slug: string) => slug === "course2")
379+
380+
expect(endedCourseSlugs.length).toBe(0)
381+
})
382+
383+
it("excludes hidden courses", async () => {
384+
const res = await ctx.client.request<any>(popularCoursesQuery, {
385+
popularCoursesForLanguage: "fi_FI",
386+
})
387+
388+
const hiddenCourseSlugs = res.popularCourses
389+
.map((c: any) => c.slug)
390+
.filter((slug: string) => slug === "handled")
391+
392+
expect(hiddenCourseSlugs.length).toBe(0)
393+
})
394+
395+
it("only returns courses with translations in the specified language", async () => {
396+
const res = await ctx.client.request<any>(popularCoursesQuery, {
397+
popularCoursesForLanguage: "fi_FI",
398+
})
399+
400+
res.popularCourses.forEach((course: any) => {
401+
expect(course.name).toBeDefined()
402+
expect(course.description).toBeDefined()
403+
})
404+
})
405+
406+
it("returns at most 4 courses", async () => {
407+
const res = await ctx.client.request<any>(popularCoursesQuery, {
408+
popularCoursesForLanguage: "fi_FI",
409+
})
410+
411+
expect(res.popularCourses.length).toBeLessThanOrEqual(4)
412+
})
413+
414+
it("returns empty array for language with no courses", async () => {
415+
const res = await ctx.client.request<any>(popularCoursesQuery, {
416+
popularCoursesForLanguage: "sv_SE",
417+
})
418+
419+
expect(res.popularCourses).toBeDefined()
420+
expect(Array.isArray(res.popularCourses)).toBe(true)
421+
expect(res.popularCourses.length).toBe(0)
422+
})
423+
424+
it("applies correct translations for the requested language", async () => {
425+
const res = await ctx.client.request<any>(popularCoursesQuery, {
426+
popularCoursesForLanguage: "fi_FI",
427+
})
428+
429+
const course1 = res.popularCourses.find(
430+
(c: any) => c.slug === "course1",
431+
)
432+
433+
if (course1) {
434+
expect(course1.name).toBe("course1_fi_FI")
435+
expect(course1.description).toBe("course1_description_fi_FI")
436+
expect(course1.link).toBe("http:/link.fi.com")
437+
}
438+
})
439+
})
440+
326441
describe("handlerCourses", () => {
327442
beforeEach(async () => {
328443
await seed(ctx.prisma)
@@ -533,6 +648,20 @@ const courseExistsQuery = gql`
533648
}
534649
`
535650

651+
const popularCoursesQuery = gql`
652+
query popularCourses($popularCoursesForLanguage: String!) {
653+
popularCourses(popularCoursesForLanguage: $popularCoursesForLanguage) {
654+
id
655+
slug
656+
name
657+
description
658+
link
659+
status
660+
hidden
661+
}
662+
}
663+
`
664+
536665
const handlerCoursesQuery = gql`
537666
query handlerCourses {
538667
handlerCourses {

backend/schema/Course/queries.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { omit } from "lodash"
1+
import { omit, shuffle } from "lodash"
22
import {
33
arg,
44
booleanArg,
@@ -9,7 +9,7 @@ import {
99
stringArg,
1010
} from "nexus"
1111

12-
import { Course, CourseTranslation, Prisma } from "@prisma/client"
12+
import { Course, CourseStatus, CourseTranslation, Prisma } from "@prisma/client"
1313

1414
import { isAdmin, isUser, or } from "../../accessControl"
1515
import { GraphQLUserInputError } from "../../lib/errors"
@@ -308,6 +308,86 @@ export const CourseQueries = extendType({
308308
},
309309
})
310310

311+
t.list.nonNull.field("popularCourses", {
312+
type: "Course",
313+
args: {
314+
popularCoursesForLanguage: nonNull(stringArg()),
315+
},
316+
resolve: async (_, args, ctx) => {
317+
const { popularCoursesForLanguage } = args
318+
319+
const courses: Array<
320+
Course & {
321+
course_translations?: Array<CourseTranslation>
322+
}
323+
> = await ctx.prisma.course.findMany({
324+
where: {
325+
AND: [
326+
{
327+
OR: [
328+
{
329+
hidden: false,
330+
},
331+
{
332+
hidden: null,
333+
},
334+
],
335+
},
336+
{
337+
status: {
338+
not: CourseStatus.Ended,
339+
},
340+
},
341+
{
342+
course_translations: {
343+
some: {
344+
language: popularCoursesForLanguage,
345+
},
346+
},
347+
},
348+
{
349+
completions_handled_by_id: null,
350+
},
351+
],
352+
},
353+
include: {
354+
course_translations: true,
355+
},
356+
})
357+
358+
const filtered = courses
359+
.map((course) => {
360+
const translationForLanguage = course?.course_translations?.find(
361+
(t) => t.language === popularCoursesForLanguage,
362+
)
363+
const translation =
364+
translationForLanguage ?? course?.course_translations?.[0]
365+
366+
return {
367+
...omit(course, [
368+
"course_translations",
369+
"tags",
370+
"handles_completions_for",
371+
]),
372+
description: translation?.description ?? "",
373+
link: translation?.link ?? "",
374+
name: translation?.name ?? course?.name ?? "",
375+
}
376+
})
377+
.filter(isDefined)
378+
379+
const shuffled = shuffle(filtered)
380+
const sampled = shuffled.slice(0, 4)
381+
382+
return sampled as Array<
383+
Course & {
384+
description: string
385+
link: string
386+
}
387+
>
388+
},
389+
})
390+
311391
t.list.nonNull.field("handlerCourses", {
312392
type: "Course",
313393
authorize: isAdmin,

frontend/components/NewLayout/Frontpage/SelectedCourses.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useRouter } from "next/router"
2-
import { range, sample } from "remeda"
2+
import { range } from "remeda"
33

44
import { useQuery } from "@apollo/client"
55
import { Button, Typography, TypographyProps } from "@mui/material"
@@ -25,9 +25,8 @@ import { formatDateTime } from "/util/dataFormatFunctions"
2525
import { mapNextLanguageToLocaleCode } from "/util/moduleFunctions"
2626

2727
import {
28-
CourseStatus,
2928
NewCourseFieldsFragment,
30-
NewCoursesDocument,
29+
PopularCoursesDocument,
3130
} from "/graphql/generated"
3231

3332
const CardHeader = styled("div")`
@@ -105,14 +104,12 @@ function SelectedCourses() {
105104
const { locale = "fi" } = useRouter()
106105
const t = useTranslator(HomeTranslations)
107106
const language = mapNextLanguageToLocaleCode(locale)
108-
const { loading, data } = useQuery(NewCoursesDocument, {
109-
variables: { language },
107+
const { loading, data } = useQuery(PopularCoursesDocument, {
108+
variables: { popularCoursesForLanguage: language, language },
110109
ssr: false,
111110
})
112111

113-
const notEndedCourses =
114-
data?.courses?.filter((course) => course.status !== CourseStatus.Ended) ??
115-
[]
112+
const courses = data?.popularCourses ?? []
116113
return (
117114
<section id="courses">
118115
<Introduction title={t("popularCoursesTitle")} />
@@ -122,7 +119,7 @@ function SelectedCourses() {
122119
? range(0, 4).map((index) => (
123120
<CourseCardSkeleton key={`skeleton-${index}`} />
124121
))
125-
: sample(notEndedCourses, 4).map((course) => (
122+
: courses.map((course) => (
126123
<CommonCourseCard key={course.id} course={course} />
127124
))}
128125
</CoursesGrid>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable */
2+
import * as Types from "../types"
3+
4+
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
5+
6+
export const PopularCoursesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PopularCourses"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"popularCoursesForLanguage"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"language"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"popularCourses"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"popularCoursesForLanguage"},"value":{"kind":"Variable","name":{"kind":"Name","value":"popularCoursesForLanguage"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewCourseFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CourseKeyFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Course"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CourseCoreFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Course"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CourseKeyFields"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ects"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"created_at"}},{"kind":"Field","name":{"kind":"Name","value":"updated_at"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CourseTranslationCoreFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CourseTranslation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewFrontpageCourseFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Course"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CourseCoreFields"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"study_module_order"}},{"kind":"Field","name":{"kind":"Name","value":"promote"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"start_point"}},{"kind":"Field","name":{"kind":"Name","value":"study_module_start_point"}},{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"upcoming_active_link"}},{"kind":"Field","name":{"kind":"Name","value":"tier"}},{"kind":"Field","name":{"kind":"Name","value":"support_email"}},{"kind":"Field","name":{"kind":"Name","value":"teacher_in_charge_email"}},{"kind":"Field","name":{"kind":"Name","value":"teacher_in_charge_name"}},{"kind":"Field","name":{"kind":"Name","value":"start_date"}},{"kind":"Field","name":{"kind":"Name","value":"end_date"}},{"kind":"Field","name":{"kind":"Name","value":"has_certificate"}},{"kind":"Field","name":{"kind":"Name","value":"study_modules"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"StudyModuleCoreFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewCourseFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Course"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewFrontpageCourseFields"}},{"kind":"Field","name":{"kind":"Name","value":"course_translations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CourseTranslationCoreFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TagCoreFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sponsors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"language"},"value":{"kind":"Variable","name":{"kind":"Name","value":"language"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CourseSponsorFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SponsorFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sponsor"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SponsorTranslationFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorTranslation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sponsor_id"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"link_text"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SponsorImageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SponsorImage"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sponsor_id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"width"}},{"kind":"Field","name":{"kind":"Name","value":"height"}},{"kind":"Field","name":{"kind":"Name","value":"uri"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SponsorCoreFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sponsor"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SponsorFields"}},{"kind":"Field","name":{"kind":"Name","value":"translations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SponsorTranslationFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SponsorImageFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CourseSponsorFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sponsor"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SponsorCoreFields"}},{"kind":"Field","name":{"kind":"Name","value":"order"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StudyModuleKeyFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StudyModule"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"StudyModuleCoreFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StudyModule"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"StudyModuleKeyFields"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TagCoreFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Tag"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"types"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"abbreviation"}},{"kind":"Field","name":{"kind":"Name","value":"tag_translations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TagTranslationFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TagTranslationFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TagTranslation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tag_id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"abbreviation"}}]}}]} as unknown as DocumentNode<Types.PopularCoursesQuery, Types.PopularCoursesQueryVariables>;

frontend/graphql/generated/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export { OrganizationsDocument } from "./definitions/OrganizationsDocument"
7777
export { PaginatedCompletionsDocument } from "./definitions/PaginatedCompletionsDocument"
7878
export { PaginatedCompletionsPreviousPageDocument } from "./definitions/PaginatedCompletionsPreviousPageDocument"
7979
export { PointsByGroupFieldsFragmentDoc } from "./definitions/PointsByGroupFieldsFragmentDoc"
80+
export { PopularCoursesDocument } from "./definitions/PopularCoursesDocument"
8081
export { ProgressCoreFieldsFragmentDoc } from "./definitions/ProgressCoreFieldsFragmentDoc"
8182
export { ProgressExtraFieldsFragmentDoc } from "./definitions/ProgressExtraFieldsFragmentDoc"
8283
export { RecheckCompletionsDocument } from "./definitions/RecheckCompletionsDocument"

0 commit comments

Comments
 (0)