Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add course instance to completion #1032

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/api/routes/__test__/__snapshots__/api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Object {
"completion_language": null,
"completion_registration_attempt_date": "2022-01-01T08:00:00.000Z",
"course_id": "00000000-0000-0000-0000-000000000001",
"course_instance_id": null,
"created_at": "2000-01-01T08:00:00.000Z",
"eligible_for_ects": true,
"email": "[email protected]",
Expand Down Expand Up @@ -57,6 +58,7 @@ Object {
"completion_language": null,
"completion_registration_attempt_date": "2022-01-01T08:00:00.000Z",
"course_id": "00000000-0000-0000-0000-000000000001",
"course_instance_id": null,
"created_at": "2000-01-01T08:00:00.000Z",
"eligible_for_ects": true,
"email": "[email protected]",
Expand Down Expand Up @@ -115,6 +117,7 @@ Object {
"completion_language": null,
"completion_registration_attempt_date": "2022-01-01T08:00:00.000Z",
"course_id": "00000000-0000-0000-0000-000000000001",
"course_instance_id": null,
"created_at": "2000-01-01T08:00:00.000Z",
"eligible_for_ects": true,
"email": "[email protected]",
Expand Down Expand Up @@ -163,6 +166,7 @@ Object {
"completion_language": null,
"completion_registration_attempt_date": "2022-01-01T08:00:00.000Z",
"course_id": "00000000-0000-0000-0000-000000000001",
"course_instance_id": null,
"created_at": "2000-01-01T08:00:00.000Z",
"eligible_for_ects": true,
"email": "[email protected]",
Expand Down Expand Up @@ -249,6 +253,7 @@ Array [
"completion_language": null,
"completion_registration_attempt_date": "2022-01-01T08:00:00.000Z",
"course_id": "00000000-0000-0000-0000-000000000001",
"course_instance_id": null,
"created_at": "2000-01-01T08:00:00.000Z",
"eligible_for_ects": true,
"email": "[email protected]",
Expand Down
34 changes: 17 additions & 17 deletions backend/api/routes/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export class CompletionController extends Controller {
return res.status(404).json({ message: "Course not found" })
}

const sql = knex
.select<any, Completion[]>("completion.*")
const sql = knex("completion")
.select<any, Completion & { passed_course_instance_id: string }>([
"completion.*",
knex.raw(`'${course.id}' as passed_course_instance_id`),
])
.distinctOn("completion.user_id", "completion.course_id")
.from("completion")
.fullOuterJoin(
"completion_registered",
"completion.id",
Expand Down Expand Up @@ -97,9 +99,8 @@ export class CompletionController extends Controller {
}

const instructions = (
await knex
.select<any, CourseTranslation[]>("instructions")
.from("course_translation")
await knex<CourseTranslation>("course_translation")
.select("instructions")
.where("course_id", course.id)
.where("language", languageMap[language] ?? "fi_FI")
)[0]?.instructions
Expand Down Expand Up @@ -132,9 +133,8 @@ export class CompletionController extends Controller {
}

const completion = (
await knex
.select<any, Completion[]>("tier")
.from("completion")
await knex<Completion>("completion")
.select("tier")
.where("course_id", course.completions_handled_by_id ?? course.id)
.andWhere("user_id", user.id)
.orderBy("created_at", "asc")
Expand All @@ -147,9 +147,10 @@ export class CompletionController extends Controller {
// TODO/FIXME: note - this now happily ignores completion_language and just gets the first one
// - as it's now only used in BAI, shouldn't be a problem?
const tiers = (
await knex
.select<any, OpenUniversityRegistrationLink[]>("tiers")
.from("open_university_registration_link")
await knex<OpenUniversityRegistrationLink>(
"open_university_registration_link",
)
.select("tiers")
.where("course_id", course.id)
)?.[0].tiers

Expand Down Expand Up @@ -247,13 +248,12 @@ export class CompletionController extends Controller {
},
})

return res.status(200).json(updatedCompletion)
return res
.status(200)
.json({ ...updatedCompletion, course_instance_id: course.id })
}

private getCompletion = async (
course: Course,
user: User,
): Promise<Completion | null> => {
private getCompletion = async (course: Course, user: User) => {
return (
await this.ctx.prisma.user
.findUnique({
Expand Down
12 changes: 4 additions & 8 deletions backend/api/routes/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ export class ProgressController extends Controller {

const exercise_completions = await exerciseCompletionQuery

const completions = await knex("completion as c")
.select<any, Completion[]>("*")
const completions = await knex<Completion>(knex.ref("completion").as("c"))
.select("*")
.where("course_id", course.completions_handled_by_id ?? course.id)
.andWhere("user_id", user.id)
.orderBy("created_at", "asc")
Expand Down Expand Up @@ -227,12 +227,8 @@ export class ProgressController extends Controller {
id = course.id
}

const data = await knex
.select<any, Pick<UserCourseProgress, "course_id" | "extra">[]>(
"course_id",
"extra",
)
.from("user_course_progress")
const data = await knex<UserCourseProgress>("user_course_progress")
.select("course_id", "extra")
.where("user_course_progress.course_id", id)
.andWhere("user_course_progress.user_id", user.id)
.orderBy("created_at", "asc")
Expand Down
6 changes: 4 additions & 2 deletions backend/bin/kafkaConsumer/common/getUserWithRaceCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ export async function getUserWithRaceCondition(
let user: User | null

const { knex, logger, prisma } = context
user = (await knex("user").where("upstream_id", user_id).limit(1))[0]
user = (await knex<User>("user").where("upstream_id", user_id).limit(1))[0]

if (!user) {
try {
user = await getUserFromTMCAndCreate(prisma, user_id)
} catch (e) {
try {
user = (await knex("user").where("upstream_id", user_id).limit(1))[0]
user = (
await knex<User>("user").where("upstream_id", user_id).limit(1)
)[0]
} catch {}

if (!user) {
Expand Down
28 changes: 16 additions & 12 deletions backend/bin/kafkaConsumer/common/userFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,14 @@ export const getExerciseCompletionsForCourses = async ({
}: GetExerciseCompletionsForCoursesArgs) => {
// picks only one exercise completion per exercise/user:
// the one with the latest timestamp and latest updated_at
const exercise_completions: ExerciseCompletionPart[] = await knex(
"exercise_completion as ec",
)
.select("course_id", "custom_id", "max_points", "exercise_id", "n_points")
const exercise_completions = await knex("exercise_completion as ec")
.select<any, ExerciseCompletionPart[]>(
"course_id",
"custom_id",
"max_points",
"exercise_id",
"n_points",
)
.distinctOn("ec.exercise_id")
.join("exercise as e", { "ec.exercise_id": "e.id" })
.whereIn("e.course_id", courseIds)
Expand Down Expand Up @@ -170,9 +174,7 @@ export const pruneDuplicateExerciseCompletions = async ({
.returning("id")*/

// we probably can just delete all even if they have required actions
const deleted: Array<Pick<ExerciseCompletion, "id">> = await knex(
"exercise_completion",
)
const deleted = await knex<ExerciseCompletion>("exercise_completion")
.whereIn(
"id",
knex
Expand Down Expand Up @@ -207,11 +209,12 @@ export const pruneDuplicateExerciseCompletions = async ({
export const pruneOrphanedExerciseCompletionRequiredActions = async ({
context: { knex },
}: WithBaseContext) => {
const deleted: Array<Pick<ExerciseCompletionRequiredAction, "id">> =
await knex("exercise_completion_required_actions")
.whereNull("exercise_completion_id")
.delete()
.returning("id")
const deleted = await knex<ExerciseCompletionRequiredAction>(
"exercise_completion_required_actions",
)
.whereNull("exercise_completion_id")
.delete()
.returning("id")

return deleted
}
Expand Down Expand Up @@ -356,6 +359,7 @@ export const createCompletion = async ({
const newCompletion = await prisma.completion.create({
data: {
course: { connect: { id: handlerCourse.id } },
course_instance: { connect: { id: course.id } },
email: user.email,
user: { connect: { id: user.id } },
user_upstream_id: user.upstream_id,
Expand Down
10 changes: 6 additions & 4 deletions backend/bin/sendAiStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Course } from "@prisma/client"

import { AI_SLACK_URL } from "../config"
import { languageInfo, LanguageInfo } from "../config/languageConfig"
import prisma from "../prisma"
Expand Down Expand Up @@ -80,8 +82,8 @@ const getDataByLanguage = async (langInfo: LanguageInfo) => {
// }

const getGlobalStats = async (): Promise<string> => {
const course = await Knex.select("id")
.from("course")
const course = await Knex<Course>("course")
.select("id")
.where({ slug: "elements-of-ai" })
const totalUsers = (
await Knex.countDistinct("user_id")
Expand All @@ -105,8 +107,8 @@ const getGlobalStats = async (): Promise<string> => {
}

const getGlobalStatsBAI = async (): Promise<string> => {
const course = await Knex.select("id")
.from("course")
const course = await Knex<Course>("course")
.select("id")
.where({ slug: "building-ai" })

const totalUsers = (
Expand Down
6 changes: 2 additions & 4 deletions backend/bin/updateBAICompletionTiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ const updateBAICompletionTiers = async () => {

logger.info("Getting completions")

const userIdsWithoutTiers = await knex<any, Pick<Completion, "user_id">[]>(
"completion",
)
const userIdsWithoutTiers = await knex<Completion>("completion")
.select("user_id")
.distinctOn("user_id", "course_id")
.where("course_id", PARENT_COURSE_ID)
.andWhere("tier", "is", null)
.orderBy(["user_id", "course_id", { column: "created_at", order: "asc" }])

logger.info("Getting users")
const usersWithoutTiers = await knex<any, User[]>("user")
const usersWithoutTiers = await knex<User>("user")
.select("*")
.whereIn(
"id",
Expand Down
35 changes: 33 additions & 2 deletions backend/graphql/Completion/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,26 @@ export const Completion = objectType({
t.model.completion_date()
t.model.tier()
t.model.completion_registration_attempt_date()
t.model.course_instance_id()

t.id("passed_course_instance_id")
t.field("course_instance", {
type: "Course",
resolve: async (
// @ts-ignore: passed_course_instance_id exists even if TS says it doesn't
{ course_instance_id, passed_course_instance_id },
_,
ctx,
) => {
if (!course_instance_id) {
return null
}

return ctx.prisma.course.findUnique({
where: { id: course_instance_id ?? passed_course_instance_id },
})
},
})
// we're not querying completion course languages for now, and this was buggy
/* t.field("course", {
type: "Course",
Expand Down Expand Up @@ -151,13 +170,25 @@ export const Completion = objectType({

t.field("certificate_availability", {
type: "CertificateAvailability",
resolve: async ({ course_id, user_upstream_id }, _, ctx) => {
resolve: async (
{
course_id,
course_instance_id,
// @ts-ignore: passed_course_instance_id exists even if TS says it doesn't
passed_course_instance_id,
user_upstream_id,
},
_,
ctx,
) => {
if (!course_id) {
return null
}

const course = await ctx.prisma.course.findUnique({
where: { id: course_id },
where: {
id: course_instance_id ?? passed_course_instance_id ?? course_id,
},
})

if (!course) {
Expand Down
7 changes: 7 additions & 0 deletions backend/graphql/Completion/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const CompletionMutations = extendType({
student_number: stringArg(),
user: nonNull(idArg()),
course: nonNull(idArg()),
course_instance: idArg(),
completion_language: stringArg(),
tier: nullable(intArg()),
},
Expand All @@ -40,13 +41,17 @@ export const CompletionMutations = extendType({
student_number,
user,
course,
course_instance,
completion_language,
tier,
} = args

return ctx.prisma.completion.create({
data: {
course: { connect: { id: course } },
...(course_instance && {
course_instance: { connect: { id: course_instance } },
}),
user: { connect: { id: user } },
email: email ?? "",
student_number,
Expand Down Expand Up @@ -98,6 +103,7 @@ export const CompletionMutations = extendType({

const newCompletions: Completion[] = completions.map((o) => {
const databaseUser = databaseUsersByUpstreamId[o.user_id][0]

return {
id: uuidv4(),
created_at: new Date(),
Expand All @@ -108,6 +114,7 @@ export const CompletionMutations = extendType({
databaseUser.real_student_number || databaseUser.student_number,
completion_language: null,
course_id: course.completions_handled_by_id ?? course_id,
course_instance_id: course_id,
user_id: databaseUser.id,
grade: o.grade ?? null,
completion_date: o.completion_date,
Expand Down
Loading