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: achievement database model #867

Merged
merged 16 commits into from
Nov 14, 2023
Merged
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
4 changes: 4 additions & 0 deletions common/achievement/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Achievement

Achievements are a part of the Gamification.
Documentation follows...
7 changes: 7 additions & 0 deletions common/achievement/aggregator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AggregatorFunction } from './types';

type Aggregator = Record<string, AggregatorFunction>;

// Aggregators are needed to aggregate event values (achievement_event.value) or buckets for evaluation (like sum, count, max, min, avg)

export const aggregators: Aggregator = {};
6 changes: 6 additions & 0 deletions common/achievement/bucket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { BucketFormula } from './types';

type BucketCreatorDefs = Record<string, BucketFormula>;

// Buckets are needed to pre-sort and aggregate certain events by types / a certain time window (e.g. weekly) etc.
export const bucketCreatorDefs: BucketCreatorDefs = {};
4 changes: 4 additions & 0 deletions common/achievement/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Metric } from './types';

// Metric is the connection of the action that happens with the formula for calculating the value for the event
const metrics: Metric[] = [];
64 changes: 64 additions & 0 deletions common/achievement/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type Metric = {
id: number;
metricName: string;
onActions: string[];
formula: FormulaFunction;
};

export type EventValue = number | string | boolean;

export type FormulaContext = {
subcourse?: {
lectures: {
start: Date;
}[];
};
match?: {
lectures: {
start: Date;
}[];
};
appointment?: {
id: number;
duration?: number;
match?: number;
subcourse?: number;
};
};

export type FormulaFunction = (context: FormulaContext) => number;

// A bucket is seen as a period of time
export interface Bucket {
startTime: Date;
endTime: Date;
}

export interface BucketEvents extends Bucket {
events: TrackEvent[];
}
export interface BucketEventsWithAggr extends BucketEvents {
aggregation: EventValue;
}

type BucketFormulaFunction = (context: FormulaContext) => Bucket[];

export type BucketFormula = {
function: BucketFormulaFunction;
};

export type AggregatorFunction = {
function: (elements: EventValue[]) => number;
};

export type ConditionDataAggregations = {
[key: string]: {
metricId: number;
aggregator: string;
// These two are used to first create all the needed buckets and then aggregate the events that fall into these
// Default: count
bucketAggregator?: string;
// Default: one bucket / event
createBuckets?: string;
};
};
4 changes: 4 additions & 0 deletions graphql/authorizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Course_tag as CourseTag,
Concrete_notification,
Screener,
User_achievement,
} from './generated';
import { Authorized, createMethodDecorator } from 'type-graphql';
import { UNAUTHENTICATED_USER } from './authentication';
Expand Down Expand Up @@ -340,6 +341,9 @@ export const authorizationEnhanceMap: Required<ResolversEnhanceMap> = {
Message_translation: { _all: nobody },
Pupil_screening: allAdmin,
Waiting_list_enrollment: allAdmin,
Achievement_template: allAdmin,
User_achievement: allAdmin, // TODO change
Achievement_event: allAdmin,
};

/* Some entities are generally accessible by multiple users, however some fields of them are
Expand Down
66 changes: 66 additions & 0 deletions prisma/migrations/20231113070308_add_achievements/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
-- CreateEnum
CREATE TYPE "achievement_template_for_enum" AS ENUM ('Course', 'Global_Courses', 'Match', 'Global_Matches', 'Global');

-- CreateEnum
CREATE TYPE "achievement_type_enum" AS ENUM ('SEQUENTIAL', 'TIERED', 'STREAK');

-- CreateEnum
CREATE TYPE "achievement_action_type_enum" AS ENUM ('Appointment', 'Action', 'Wait', 'Info');

-- CreateTable
CREATE TABLE "achievement_template" (
"id" SERIAL NOT NULL,
"name" VARCHAR NOT NULL,
"metrics" TEXT[],
"templateFor" "achievement_template_for_enum" NOT NULL,
"group" VARCHAR NOT NULL,
"groupOrder" INTEGER NOT NULL,
"stepName" VARCHAR NOT NULL,
"type" "achievement_type_enum" NOT NULL,
"subtitle" VARCHAR NOT NULL,
"description" VARCHAR NOT NULL,
"image" VARCHAR NOT NULL,
"actionName" VARCHAR,
"actionRedirectLink" VARCHAR,
"actionType" "achievement_action_type_enum",
"achievedText" VARCHAR,
"condition" VARCHAR NOT NULL,
"conditionDataAggregations" JSON NOT NULL,
"isActive" BOOLEAN NOT NULL,

CONSTRAINT "achievement_template_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "user_achievement" (
"id" SERIAL NOT NULL,
"templateId" INTEGER NOT NULL,
"userId" TEXT NOT NULL,
"group" VARCHAR NOT NULL,
"groupOrder" INTEGER NOT NULL,
"isSeen" BOOLEAN NOT NULL DEFAULT false,
"achievedAt" TIMESTAMP(6),
"recordValue" INTEGER,
"context" JSON NOT NULL,

CONSTRAINT "user_achievement_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "achievement_event" (
"id" SERIAL NOT NULL,
"userId" VARCHAR NOT NULL,
"metric" VARCHAR NOT NULL,
"value" INTEGER NOT NULL,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"action" VARCHAR NOT NULL,
"relation" VARCHAR NOT NULL,

CONSTRAINT "achievement_event_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "user_achievement_group_key" ON "user_achievement"("group");

-- AddForeignKey
ALTER TABLE "user_achievement" ADD CONSTRAINT "user_achievement_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "achievement_template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
113 changes: 105 additions & 8 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,82 @@ datasource db {
url = env("DATABASE_URL")
}

// Achievement
// A final achievement can consist of several sub-achievements
model achievement_template {
id Int @id @default(autoincrement())
name String @db.VarChar
metrics String[]
templateFor achievement_template_for_enum
// Achievements can
// - cross-platform (i.e. for one user)
// - across all offers (globally for all courses or for all matches)
// - or specifically for one offer (eg `match/{{matchId}}`)
// We need the group for this in order to be able to assign it correctly later.
// e.g. "onboarding", "regular_learning/{{matchId}}", "course_completed/{{courseId}}" ""
group String @db.VarChar
// An achievement can also consist of sub-achievements,
// e.g. in the case of sequential achievements, each step is seen as a separate achievement (sub-achievement).
// In the case of tiered achievements, for example, each tier is a separate achievement but belongs to a group.
// The groupOrder therefore determines the order of the sub-achievements.
groupOrder Int
// Sequential: each step is seen as a separate achievement and each step has its own name
stepName String @db.VarChar
// what type of achievement is it
type achievement_type_enum
subtitle String @db.VarChar
description String @db.VarChar
image String @db.VarChar
// some achievements show actions that a user can/must perform next.
// therefore we need the action name, type (for the icon) and a redirect link
actionName String? @db.VarChar
actionRedirectLink String? @db.VarChar
actionType achievement_action_type_enum?
// Tiered: If a tiered achievement has been achieved, the text changes
achievedText String? @db.VarChar
// defines what value must be reached to receive the achievement,
// e.g. "duration == 60", "count == 5", "count == 3 & duration == 60", "series >= 5" (https://github.com/onlabsorg/swan-js)
condition String @db.VarChar
// defines how values must be aggregated to evaluate the condition
// it also contains bucket aggregators (buckets are used for aggregate values for a specific time of period)
conditionDataAggregations Json @db.Json
isActive Boolean
user_achievement user_achievement[]
}

model user_achievement {
Jonasdoubleyou marked this conversation as resolved.
Show resolved Hide resolved
id Int @id @default(autoincrement())
// the id of the relating achievement_template
templateId Int
template achievement_template @relation(fields: [templateId], references: [id])
userId String
// the description of group and groupOrder can be found in the achievement_template.
// It needs to be stored in user_achievement, as the group contains templating.
// The correctly assigned achievement for a match / a course is therefore stored in the user_achievement.
group String @unique @db.VarChar
groupOrder Int
isSeen Boolean @default(false)
// achievedAt == null => not achieved, achievedAt != null => achieved
achievedAt DateTime? @db.Timestamp(6)
// Streaks: we have to store the highest record for streaks
recordValue Int?
context Json @db.Json
}

// Save (tracked) event as raw data in DB and evaluate at runtime
model achievement_event {
id Int @id @default(autoincrement())
userId String @db.VarChar
metric String @db.VarChar
// the value is calculated with the formula given in the metric
value Int
createdAt DateTime @default(now()) @db.Timestamp(6)
// what action triggered the event
action String @db.VarChar
// the relation is like a unique id so that we can later assign the event uniquely, e.g. "course/10", "match/12", "user/10"
relation String @db.VarChar
}

// An attachment is a file uploaded to an object storage and attached to a notification
// We also store it in the DB to be able to track the file's lifecycle, i.e. delete it when the notification is archived
model attachment {
Expand Down Expand Up @@ -285,7 +361,7 @@ model log {
}

model match {
id Int @id(map: "PK_92b6c3a6631dd5b24a67c69f69d") @default(autoincrement())
id Int @id(map: "PK_92b6c3a6631dd5b24a67c69f69d") @default(autoincrement())
// DEPRECATED: The uuid was used to generate non-guessable meeting room links, replaced by Appointments/Zoom
uuid String @unique(map: "IDX_65a3ec8c0aa6c3c9c04f5b53e3") @db.VarChar
dissolved Boolean @default(false)
Expand All @@ -300,15 +376,15 @@ model match {
dissolveReasonEnum dissolve_reason?
// DEPRECATED: At some point students could suggest a first meeting time to pupils when they got the match
// The functionality was removed a long time ago, and is generally replaced with Appointments
proposedTime DateTime? @db.Timestamp(6)
createdAt DateTime @default(now()) @db.Timestamp(6)
updatedAt DateTime @default(now()) @db.Timestamp(6)
proposedTime DateTime? @db.Timestamp(6)
createdAt DateTime @default(now()) @db.Timestamp(6)
updatedAt DateTime @default(now()) @db.Timestamp(6)
// DEPRECATED: With the Notification System we now use Concrete Notifications to track
// whether a notification was already sent. These booleans were used a long time ago by some cron jobs:
feedbackToPupilMail Boolean @default(false)
feedbackToStudentMail Boolean @default(false)
followUpToPupilMail Boolean @default(false)
followUpToStudentMail Boolean @default(false)
feedbackToPupilMail Boolean @default(false)
feedbackToStudentMail Boolean @default(false)
followUpToPupilMail Boolean @default(false)
followUpToStudentMail Boolean @default(false)

// DEPRECATED: In the early days, matching was done offline and the data was imported into the DB
source match_source_enum @default(matchedinternal)
Expand Down Expand Up @@ -1143,6 +1219,27 @@ enum notification_sender_enum {
CERTIFICATE_OF_CONDUCT
}

enum achievement_template_for_enum {
Course
Global_Courses
Match
Global_Matches
Global
}

enum achievement_type_enum {
LomyW marked this conversation as resolved.
Show resolved Hide resolved
SEQUENTIAL
TIERED
STREAK
}

enum achievement_action_type_enum {
Appointment
Action
Wait
Info
}

enum secret_type_enum {
PASSWORD
TOKEN
Expand Down
Loading