Skip to content

Commit

Permalink
feat: add most popular courses statistics (#257)
Browse files Browse the repository at this point in the history
* feat: add most popular courses statistics

* feat: apply feedback, renaming of functions
  • Loading branch information
wielopolski authored Nov 28, 2024
1 parent 2c37a79 commit 121a8fc
Show file tree
Hide file tree
Showing 13 changed files with 2,296 additions and 33 deletions.
25 changes: 0 additions & 25 deletions apps/api/src/statistics/api/statistics.controller.ts

This file was deleted.

17 changes: 16 additions & 1 deletion apps/api/src/statistics/repositories/statistics.repository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Inject, Injectable } from "@nestjs/common";
import { eq, sql } from "drizzle-orm";
import { desc, eq, sql } from "drizzle-orm";

import { DatabasePg } from "src/common";
import { LessonProgress } from "src/lessons/schemas/lesson.types";
import {
courses,
courseStatisticsPerTeacherMaterialized,
lessons,
quizAttempts,
studentCourses,
Expand Down Expand Up @@ -131,6 +133,19 @@ export class StatisticsRepository {
return result;
}

async getFiveMostPopularCourses(userId: string) {
return await this.db
.select({
courseName: courses.title,
studentCount: courseStatisticsPerTeacherMaterialized.studentCount,
})
.from(courseStatisticsPerTeacherMaterialized)
.innerJoin(courses, eq(courseStatisticsPerTeacherMaterialized.courseId, courses.id))
.where(eq(courseStatisticsPerTeacherMaterialized.teacherId, userId))
.orderBy(desc(courseStatisticsPerTeacherMaterialized.studentCount))
.limit(5);
}

async createQuizAttempt(data: {
userId: string;
courseId: string;
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/statistics/schemas/userStats.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export const UserStatsSchema = Type.Object({
]),
});

export const PopularCourseStatsSchema = Type.Object({
courseName: Type.String(),
studentCount: Type.Number(),
});

export const TeacherStatsSchema = Type.Object({
fiveMostPopularCourses: Type.Array(PopularCourseStatsSchema),
});

const UserStatisticSchema = Type.Object({
currentStreak: Type.Number(),
longestStreak: Type.Number(),
Expand All @@ -67,3 +76,4 @@ const UserStatisticSchema = Type.Object({
export type UserStats = Static<typeof UserStatsSchema>;
export type StatsByMonth = Static<typeof StatsByMonthSchema>;
export type UserStatistic = Static<typeof UserStatisticSchema>;
export type TeacherStats = Static<typeof TeacherStatsSchema>;
40 changes: 40 additions & 0 deletions apps/api/src/statistics/statistics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Controller, Get, UseGuards } from "@nestjs/common";
import { Validate } from "nestjs-typebox";

import { baseResponse, BaseResponse, UUIDType } from "src/common";
import { Roles } from "src/common/decorators/roles.decorator";
import { CurrentUser } from "src/common/decorators/user.decorator";
import { RolesGuard } from "src/common/guards/roles.guard";
import { USER_ROLES } from "src/users/schemas/user-roles";

import { TeacherStatsSchema, UserStatsSchema } from "./schemas/userStats.schema";
import { StatisticsService } from "./statistics.service";

import type { TeacherStats, UserStats } from "./schemas/userStats.schema";

@UseGuards(RolesGuard)
@Controller("statistics")
export class StatisticsController {
constructor(private statisticsService: StatisticsService) {}

@Get("user-stats")
@Validate({
response: baseResponse(UserStatsSchema),
})
async getUserStatistics(
@CurrentUser("userId") currentUserId: UUIDType,
): Promise<BaseResponse<UserStats>> {
return new BaseResponse(await this.statisticsService.getUserStats(currentUserId));
}

@Get("teacher-stats")
@Roles(USER_ROLES.admin, USER_ROLES.tutor)
@Validate({
response: baseResponse(TeacherStatsSchema),
})
async getTeacherStats(
@CurrentUser("userId") currentUserId: UUIDType,
): Promise<BaseResponse<TeacherStats>> {
return new BaseResponse(await this.statisticsService.getTeacherStats(currentUserId));
}
}
2 changes: 1 addition & 1 deletion apps/api/src/statistics/statistics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { CqrsModule } from "@nestjs/cqrs";
import { LessonsRepository } from "src/lessons/repositories/lessons.repository";
import { StatisticsRepository } from "src/statistics/repositories/statistics.repository";

import { StatisticsController } from "./api/statistics.controller";
import { StatisticsHandler } from "./handlers/statistics.handler";
import { StatisticsController } from "./statistics.controller";
import { StatisticsService } from "./statistics.service";

@Module({
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/statistics/statistics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ export class StatisticsService {
};
}

async getTeacherStats(userId: string) {
const fiveMostPopularCourses =
await this.statisticsRepository.getFiveMostPopularCourses(userId);

return {
fiveMostPopularCourses,
};
}

async createQuizAttempt(data: {
userId: string;
courseId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ALTER TABLE "student_lessons_progress" ADD COLUMN "completed_as_freemium" boolean DEFAULT false NOT NULL;

CREATE MATERIALIZED VIEW
"course_statistics_per_teacher" AS
SELECT
c.author_id AS teacher_id,
c.id AS course_id,
COUNT(sc.id) AS student_count,
SUM(
CASE
WHEN sc.state = 'completed' THEN 1
ELSE 0
END
) AS completed_student_count,
COUNT(
DISTINCT CASE
WHEN slp.completed_as_freemium = true THEN sc.student_id
ELSE NULL
END
) AS purchased_after_freemium_count
FROM
courses c
LEFT JOIN student_courses sc ON c.id = sc.course_id
LEFT JOIN student_lessons_progress slp ON c.id = slp.course_id
AND slp.completed_at IS NOT NULL
GROUP BY
c.author_id,
c.id;
Loading

0 comments on commit 121a8fc

Please sign in to comment.