Skip to content

Commit

Permalink
Merge pull request #902 from parlemonde/ft/vil-318/add-activity-column
Browse files Browse the repository at this point in the history
Ft/vil 318/add activity column
  • Loading branch information
Neo-Ryo authored Apr 2, 2024
2 parents f77d4ee + c6eafa7 commit e851d7f
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 241 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"migration:create": "cd server && mkdir -p migrations && cd migrations && yarn typeorm migration:create",
"migration:run": "yarn typeorm migration:run -d dist/server/utils/data-source.js",
"migration:run-dev": "rm -rf dist && yarn build:server && yarn typeorm migration:run -d dist/server/utils/data-source.js || true",
"migration:revert-dev": "rm -rf dist && yarn build:server && npx typeorm migration:revert -d dist/server/utils/data-source.js || true",
"typeorm": "typeorm-ts-node-commonjs"
},
"jest": {
Expand Down
187 changes: 34 additions & 153 deletions server/controllers/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import type { JSONSchemaType } from 'ajv';
import type { NextFunction, Request, Response } from 'express';
import { IsNull } from 'typeorm';

import type { ActivityContent, AnyData } from '../../types/activity.type';
import { EPhase1Steps, ActivityStatus, ActivityType, EPhase2Steps, EPhase3Steps } from '../../types/activity.type';
import type { GameData, GamesData } from '../../types/game.type';
import type { StoriesData, StoryElement } from '../../types/story.type';
import { ImageType } from '../../types/story.type';
import type { AnyData, ActivityContent } from '../entities/activity';
import { Activity, ActivityType, ActivityStatus } from '../entities/activity';
import { Comment } from '../entities/comment';
import { Activity } from '../entities/activity';
import { Game } from '../entities/game';
import { Image } from '../entities/image';
import { UserType } from '../entities/user';
import { VillagePhase } from '../entities/village';
import { getActivities, getActivitiesCommentCount } from '../manager/activity';
import { AppError, ErrorCode } from '../middlewares/handleErrors';
import { getQueryString } from '../utils';
import { AppDataSource } from '../utils/data-source';
Expand All @@ -21,153 +22,6 @@ import { Controller } from './controller';

const activityController = new Controller('/activities');

type ActivityGetter = {
limit?: number;
page?: number;
phase?: number | null;
villageId?: number;
type?: string[];
subType?: number | null;
countries?: string[];
pelico?: boolean;
userId?: number;
status?: number;
responseActivityId?: number;
delayedDays?: number;
hasVisibilitySetToClass?: boolean;
teacherId?: number;
visibleToParent: boolean;
};

const getActivitiesCommentCount = async (ids: number[]): Promise<{ [key: number]: number }> => {
if (ids.length === 0) {
return {};
}
const queryBuilder = await AppDataSource.getRepository(Activity)
.createQueryBuilder('activity')
.select('activity.id')
.addSelect('IFNULL(`commentCount`, 0) + IFNULL(`activityCount`, 0)', 'comments')
.leftJoin(
(qb) => {
qb.select('comment.activityId', 'cid').addSelect('COUNT(comment.id)', 'commentCount').from(Comment, 'comment').groupBy('comment.activityId');
return qb;
},
'comments',
'`comments`.`cid` = activity.id',
)
.leftJoin(
(qb) => {
qb.select('activity.responseActivityId', 'aid')
.addSelect('COUNT(activity.id)', 'activityCount')
.from(Activity, 'activity')
.where('activity.status != :status', { status: `${ActivityStatus.DRAFT}` })
.groupBy('activity.responseActivityId');
return qb;
},
'activities',
'`activities`.`aid` = activity.id',
)
.where('activity.id in (:ids)', { ids })
.getRawMany();
return queryBuilder.reduce((acc, row) => {
acc[row.activity_id] = parseInt(row.comments, 10) || 0;
return acc;
}, {});
};

const getActivities = async ({
limit = 200,
page = 0,
villageId,
type = [],
subType = null,
countries,
phase = null,
pelico = true,
status = 0,
userId,
responseActivityId,
delayedDays,
hasVisibilitySetToClass,
teacherId,
visibleToParent,
}: ActivityGetter) => {
// get ids
let subQueryBuilder = AppDataSource.getRepository(Activity).createQueryBuilder('activity').where('activity.status = :status', { status });
if (villageId !== undefined) {
subQueryBuilder = subQueryBuilder.andWhere('activity.villageId = :villageId', { villageId });
}
if (type.length > 0) {
subQueryBuilder = subQueryBuilder.andWhere('activity.type IN (:type)', { type });
}
if (subType !== null) {
subQueryBuilder = subQueryBuilder.andWhere('activity.subType = :subType', { subType });
}
if (phase !== null) {
subQueryBuilder = subQueryBuilder.andWhere('activity.phase = :phase', { phase });
}
if (delayedDays !== undefined) {
// * Memo: we only select activity with updateDate + delayedDays lesser than current date
subQueryBuilder = subQueryBuilder.andWhere(`DATE_ADD(activity.updateDate, INTERVAL :delayedDays DAY) <= CURDATE()`, { delayedDays: delayedDays });
}
if (visibleToParent) {
// * Here if user is a parent, we only select activities with the attribute is set to true
subQueryBuilder = subQueryBuilder.andWhere('activity.isVisibleToParent = :visibleToParent', { visibleToParent });
}
if (hasVisibilitySetToClass !== undefined && teacherId !== undefined) {
subQueryBuilder = subQueryBuilder.andWhere('activity.user = :teacherId', { teacherId: teacherId });
}

if (responseActivityId !== undefined) {
subQueryBuilder = subQueryBuilder.andWhere('activity.responseActivityId = :responseActivityId', { responseActivityId });
} else if (userId !== undefined) {
subQueryBuilder = subQueryBuilder.innerJoin('activity.user', 'user').andWhere('user.id = :userId', {
userId,
});
} else if (pelico && countries !== undefined && countries.length > 0) {
subQueryBuilder = subQueryBuilder
.innerJoin('activity.user', 'user')
.andWhere('((user.countryCode IN (:countries) AND user.type >= :userType) OR user.type <= :userType2)', {
countries,
userType: UserType.TEACHER,
userType2: UserType.MEDIATOR,
});
} else if (pelico && countries !== undefined && countries.length === 0) {
subQueryBuilder = subQueryBuilder.innerJoin('activity.user', 'user').andWhere('user.type <= :userType2', {
userType2: UserType.MEDIATOR,
});
} else if (!pelico && countries !== undefined && countries.length > 0) {
subQueryBuilder = subQueryBuilder.innerJoin('activity.user', 'user').andWhere('user.countryCode IN (:countries) AND user.type >= :userType', {
countries,
userType: UserType.TEACHER,
});
} else if (!pelico && countries !== undefined) {
return [];
}

const activities = await subQueryBuilder
.orderBy('activity.isPinned', 'DESC')
.addOrderBy('activity.updateDate', 'DESC')
.limit(limit)
.offset(page * limit)
.getMany();

const ids = activities.map((a) => a.id);
if (ids.length === 0) {
return [];
}

const comments = await getActivitiesCommentCount(ids);
for (const activity of activities) {
if (comments[activity.id] !== undefined) {
activity.commentCount = comments[activity.id];
} else {
activity.commentCount = 0;
}
}
return activities;
};

// --- Get all activities. ---
activityController.get({ path: '', userType: UserType.OBSERVATOR }, async (req: Request, res: Response) => {
if (!req.user) throw new AppError('Forbidden', ErrorCode.UNKNOWN);
Expand All @@ -186,6 +40,7 @@ activityController.get({ path: '', userType: UserType.OBSERVATOR }, async (req:
type: req.query.type ? (getQueryString(req.query.type) || '').split(',') : undefined,
subType: req.query.subType ? Number(getQueryString(req.query.subType)) || 0 : undefined,
phase: req.query.phase ? Number(getQueryString(req.query.phase)) || 0 : undefined,
phaseStep: req.query.phaseStep ? String(req.query.phaseStep) : undefined,
status: req.query.status ? Number(getQueryString(req.query.status)) || 0 : undefined,
userId: req.query.userId ? Number(getQueryString(req.query.userId)) || 0 : undefined,
responseActivityId: req.query.responseActivityId ? Number(getQueryString(req.query.responseActivityId)) || 0 : undefined,
Expand Down Expand Up @@ -377,13 +232,14 @@ activityController.post({ path: '', userType: UserType.TEACHER }, async (req: Re
// --- Update activity ---
type UpdateActivity = {
status?: number;
phase?: number;
phase?: 1 | 2 | 3;
responseActivityId?: number;
responseType?: number;
isPinned?: boolean;
displayAsUser?: boolean;
data?: AnyData;
content?: ActivityContent[];
phaseStep?: string;
};

const UPDATE_A_SCHEMA: JSONSchemaType<UpdateActivity> = {
Expand All @@ -397,6 +253,10 @@ const UPDATE_A_SCHEMA: JSONSchemaType<UpdateActivity> = {
type: 'number',
nullable: true,
},
phaseStep: {
type: 'string',
nullable: true,
},
responseActivityId: { type: 'number', nullable: true },
responseType: {
type: 'number',
Expand Down Expand Up @@ -450,10 +310,31 @@ activityController.put({ path: '/:id', userType: UserType.TEACHER }, async (req:
next();
return;
}

if (activity.status !== ActivityStatus.PUBLISHED) {
activity.phase = data.phase || activity.phase;
if (data.phase) activity.phase = data.phase;
if (data.phaseStep) {
switch (activity.phase) {
case 1: {
if (Object.values(EPhase1Steps).includes(data.phaseStep as EPhase1Steps)) activity.phaseStep = data.phaseStep;
else throw new AppError(`Phase step: ${data.phaseStep} isn't part of phase 1`, ErrorCode.INVALID_DATA);
break;
}
case 2: {
if (Object.values(EPhase2Steps).includes(data.phaseStep as EPhase2Steps)) activity.phaseStep = data.phaseStep;
else throw new AppError(`Phase step: ${data.phaseStep} isn't part of phase 2`, ErrorCode.INVALID_DATA);
break;
}
case 3: {
if (Object.values(EPhase3Steps).includes(data.phaseStep as EPhase3Steps)) activity.phaseStep = data.phaseStep;
else throw new AppError(`Phase step: ${data.phaseStep} isn't part of phase 3`, ErrorCode.INVALID_DATA);
break;
}
default:
break;
}
}
}

activity.status = data.status ?? activity.status;
activity.responseActivityId = data.responseActivityId !== undefined ? data.responseActivityId : activity.responseActivityId ?? null;
activity.responseType = data.responseType !== undefined ? data.responseType : activity.responseType ?? null;
Expand Down
3 changes: 2 additions & 1 deletion server/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import type { NextFunction, Request, Response } from 'express';
import type { FindOperator } from 'typeorm';
import { In, IsNull, LessThan } from 'typeorm';

import { ActivityStatus, ActivityType } from '../../types/activity.type';
import { getAccessToken } from '../authentication/lib/tokens';
import { Email, sendMail } from '../emails';
import { Activity, ActivityType, ActivityStatus } from '../entities/activity';
import { Activity } from '../entities/activity';
import { Classroom } from '../entities/classroom';
import { FeatureFlag } from '../entities/featureFlag';
import { Student } from '../entities/student';
Expand Down
6 changes: 3 additions & 3 deletions server/entities/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ import { Image } from './image';
import { User } from './user';
import { Village } from './village';

export type { AnyData, ActivityContent };
export { ActivityType, ActivityStatus };

@Entity()
export class Activity implements ActivityInterface<AnyData> {
@PrimaryGeneratedColumn()
Expand All @@ -40,6 +37,9 @@ export class Activity implements ActivityInterface<AnyData> {
@Column({ type: 'tinyint', nullable: false, default: VillagePhase.DISCOVER })
public phase: number;

@Column({ type: 'text', nullable: true })
public phaseStep: string | null;

@Column({
type: 'tinyint',
nullable: false,
Expand Down
Loading

0 comments on commit e851d7f

Please sign in to comment.