diff --git a/backend/graphql/RequiredForCompletion.ts b/backend/graphql/RequiredForCompletion.ts
new file mode 100644
index 000000000..b08c58427
--- /dev/null
+++ b/backend/graphql/RequiredForCompletion.ts
@@ -0,0 +1,34 @@
+import { enumType, objectType } from "nexus"
+
+import { RequiredForCompletionEnum } from "../typeDefs"
+
+function convertTsEnum(toConvert: any) {
+ const converted: { [key: string]: number } = {}
+
+ Object.keys(toConvert).forEach((key) => {
+ if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(key)) {
+ converted[key] = toConvert[key]
+ }
+ })
+
+ return converted
+}
+
+export const RequiredForCompletionType = enumType({
+ name: "RequiredForCompletionType",
+ members: convertTsEnum(RequiredForCompletionEnum),
+})
+
+export const RequiredForCompletion = objectType({
+ name: "RequiredForCompletion",
+ definition(t) {
+ t.nonNull.field("type", {
+ type: "RequiredForCompletionType",
+ })
+ t.int("current_amount")
+ t.int("required_amount")
+ t.list.field("required_actions", {
+ type: "ExerciseCompletionRequiredAction",
+ })
+ },
+})
diff --git a/backend/graphql/User/model.ts b/backend/graphql/User/model.ts
index 11589156a..048cc0fbd 100644
--- a/backend/graphql/User/model.ts
+++ b/backend/graphql/User/model.ts
@@ -249,16 +249,28 @@ export const User = objectType({
t.list.field("user_course_summary", {
type: "UserCourseSummary",
- resolve: async (parent, _, ctx) => {
+ args: {
+ include_hidden_exercises: booleanArg({ default: false }),
+ },
+ resolve: async (parent, { include_hidden_exercises }, ctx) => {
// not very optimal, as the exercise completions will be queried twice if that field is selected
const exerciseCompletionCourses = await ctx.prisma.user
.findUnique({
where: { id: parent.id },
})
.exercise_completions({
- select: {
+ where: {
+ ...(!include_hidden_exercises
+ ? {
+ exercise: {
+ deleted: { not: true },
+ },
+ }
+ : {}),
+ },
+ include: {
exercise: {
- select: {
+ include: {
course: {
select: {
id: true,
diff --git a/backend/graphql/UserCourseSummary.ts b/backend/graphql/UserCourseSummary.ts
index 5c03b983b..cd6e9858d 100644
--- a/backend/graphql/UserCourseSummary.ts
+++ b/backend/graphql/UserCourseSummary.ts
@@ -1,6 +1,10 @@
import { UserInputError } from "apollo-server-express"
+import { uniqBy } from "lodash"
import { objectType } from "nexus"
+import { RequiredForCompletionEnum } from "../typeDefs"
+import { RequiredForCompletion } from "./RequiredForCompletion"
+
export const UserCourseSummary = objectType({
name: "UserCourseSummary",
definition(t) {
@@ -9,6 +13,101 @@ export const UserCourseSummary = objectType({
t.id("inherit_settings_from_id")
t.id("completions_handled_by_id")
+ t.nonNull.list.nonNull.field("required_for_completion", {
+ type: RequiredForCompletion,
+ resolve: async ({ course_id, user_id }, _, ctx) => {
+ if (!course_id || !user_id) {
+ throw new UserInputError("must provide course_id and user_id")
+ }
+
+ const completion = await ctx.prisma.completion.findMany({
+ where: {
+ user_id,
+ course_id,
+ },
+ })
+
+ if (completion.length) {
+ return []
+ }
+
+ const result = []
+
+ const { points_needed, exercise_completions_needed } =
+ (await ctx.prisma.course.findUnique({
+ where: {
+ id: course_id,
+ },
+ })) ?? {}
+
+ const { user_course_progresses, exercise_completions } =
+ (await ctx.prisma.user.findUnique({
+ where: {
+ id: user_id,
+ },
+ include: {
+ user_course_progresses: {
+ where: {
+ course_id,
+ },
+ select: {
+ n_points: true,
+ },
+ orderBy: {
+ created_at: "asc",
+ },
+ take: 1,
+ },
+ exercise_completions: {
+ where: {
+ exercise: {
+ course_id,
+ deleted: { not: true },
+ },
+ },
+ include: {
+ exercise_completion_required_actions: true,
+ },
+ },
+ },
+ })) ?? {}
+
+ const n_points = user_course_progresses?.[0]?.n_points ?? 0
+ const uniqueExerciseCompletions = uniqBy(exercise_completions, "id")
+ const requiredActions = uniqueExerciseCompletions.flatMap(
+ (ec) => ec.exercise_completion_required_actions,
+ )
+
+ if (points_needed && n_points < points_needed) {
+ result.push({
+ type: RequiredForCompletionEnum.NOT_ENOUGH_POINTS,
+ current_amount: n_points,
+ needed_amount: points_needed,
+ })
+ }
+
+ if (
+ exercise_completions_needed &&
+ uniqueExerciseCompletions.length < exercise_completions_needed
+ ) {
+ result.push({
+ type: RequiredForCompletionEnum.NOT_ENOUGH_EXERCISE_COMPLETIONS,
+ current_amount: uniqueExerciseCompletions.length,
+ required_amount: exercise_completions_needed,
+ })
+ }
+
+ if (requiredActions.length) {
+ result.push({
+ type: RequiredForCompletionEnum.REQUIRED_ACTIONS,
+ required_actions: requiredActions,
+ })
+ }
+
+ return result
+ },
+ })
+
t.field("course", {
type: "Course",
resolve: async ({ course_id }, _, ctx) => {
diff --git a/backend/graphql/index.ts b/backend/graphql/index.ts
index 000ff1567..3de36f9f5 100644
--- a/backend/graphql/index.ts
+++ b/backend/graphql/index.ts
@@ -23,6 +23,7 @@ export * from "./Organization"
export * from "./OrganizationTranslation"
export * from "./PointsByGroup"
export * from "./Progress"
+export * from "./RequiredForCompletion"
export * from "./Service"
export * from "./StoredData"
export * from "./StudyModule"
diff --git a/backend/schema.ts b/backend/schema.ts
index 37cc1f533..fe5ae0bec 100644
--- a/backend/schema.ts
+++ b/backend/schema.ts
@@ -68,6 +68,10 @@ export default makeSchema({
module: require.resolve(".prisma/client/index.d.ts"),
alias: "prisma",
},
+ {
+ module: path.join(__dirname, "typeDefs.ts"),
+ alias: "t",
+ },
],
},
plugins: createPlugins(),
@@ -80,4 +84,5 @@ export default makeSchema({
},
shouldGenerateArtifacts: true,
shouldExitAfterGenerateArtifacts: Boolean(NEXUS_REFLECTION),
+ prettierConfig: require.resolve("../.prettierrc.yaml"),
})
diff --git a/backend/typeDefs.ts b/backend/typeDefs.ts
new file mode 100644
index 000000000..f294fd2d0
--- /dev/null
+++ b/backend/typeDefs.ts
@@ -0,0 +1,17 @@
+import { ExerciseCompletionRequiredAction } from "@prisma/client"
+
+export enum RequiredForCompletionEnum {
+ NOT_ENOUGH_POINTS = "NOT_ENOUGH_POINTS",
+ NOT_ENOUGH_EXERCISE_COMPLETIONS = "NOT_ENOUGH_EXERCISE_COMPLETIONS",
+ REQUIRED_ACTIONS = "REQUIRED_ACTIONS",
+}
+
+export type RequiredForCompletion = {
+ type:
+ | RequiredForCompletionEnum.NOT_ENOUGH_POINTS
+ | RequiredForCompletionEnum.NOT_ENOUGH_EXERCISE_COMPLETIONS
+ | RequiredForCompletionEnum.REQUIRED_ACTIONS
+ current_amount?: number
+ required_amount?: number
+ required_actions?: ExerciseCompletionRequiredAction[]
+}
diff --git a/frontend/components/Dashboard/PointsListItemCard.tsx b/frontend/components/Dashboard/PointsListItemCard.tsx
index ed20ee905..ad73bad33 100644
--- a/frontend/components/Dashboard/PointsListItemCard.tsx
+++ b/frontend/components/Dashboard/PointsListItemCard.tsx
@@ -1,19 +1,22 @@
import { useState } from "react"
-import { Grid } from "@mui/material"
-import PointsItemTable from "./PointsItemTable"
-import styled from "@emotion/styled"
-import { gql } from "@apollo/client"
-import formatPointsData, {
- formattedGroupPointsDictionary,
-} from "/util/formatPointsData"
-import { CardTitle, CardSubtitle } from "/components/Text/headers"
+
import { FormSubmitButton } from "/components/Buttons/FormSubmitButton"
import PointsProgress from "/components/Dashboard/PointsProgress"
+import { CardSubtitle, CardTitle } from "/components/Text/headers"
import { ProgressUserCourseProgressFragment } from "/graphql/fragments/userCourseProgress"
import { ProgressUserCourseServiceProgressFragment } from "/graphql/fragments/userCourseServiceProgress"
import { UserCourseProgressFragment } from "/static/types/generated/UserCourseProgressFragment"
import { UserCourseServiceProgressFragment } from "/static/types/generated/UserCourseServiceProgressFragment"
import { UserPoints_currentUser_progresses_course } from "/static/types/generated/UserPoints"
+import formatPointsData, {
+ formattedGroupPointsDictionary,
+} from "/util/formatPointsData"
+
+import { gql } from "@apollo/client"
+import styled from "@emotion/styled"
+import { Grid } from "@mui/material"
+
+import PointsItemTable from "./PointsItemTable"
const UserFragment = gql`
fragment UserPointsFragment on User {
@@ -104,11 +107,11 @@ function PointsListItemCard(props: Props) {
{showProgress ? (
<>