Skip to content

Commit

Permalink
Merge branch 'master' into bugfix/fix-event-cover-flow
Browse files Browse the repository at this point in the history
  • Loading branch information
maxwn04 authored Apr 2, 2024
2 parents f93595a + c3d599c commit 699d758
Show file tree
Hide file tree
Showing 27 changed files with 794 additions and 230 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ FROM node:14.15.3-alpine
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["yarn", "start"]
CMD ["yarn", "release"]
13 changes: 8 additions & 5 deletions api/controllers/FeedbackController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Body, ForbiddenError, Get, JsonController, Params, Patch, Post, UseBefore } from 'routing-controllers';
import { Body, ForbiddenError, Get, JsonController, Params,
Patch, Post, UseBefore, QueryParams } from 'routing-controllers';
import { AuthenticatedUser } from '../decorators/AuthenticatedUser';
import { UserModel } from '../../models/UserModel';
import PermissionsService from '../../services/PermissionsService';
Expand All @@ -9,6 +10,7 @@ import { UserAuthentication } from '../middleware/UserAuthentication';
import {
SubmitFeedbackRequest,
UpdateFeedbackStatusRequest,
FeedbackSearchOptions,
} from '../validators/FeedbackControllerRequests';

@UseBefore(UserAuthentication)
Expand All @@ -21,9 +23,10 @@ export class FeedbackController {
}

@Get()
async getFeedback(@AuthenticatedUser() user: UserModel): Promise<GetFeedbackResponse> {
const canSeeAllFeedback = PermissionsService.canRespondToFeedback(user);
const feedback = await this.feedbackService.getFeedback(canSeeAllFeedback, user);
async getFeedback(@QueryParams() options: FeedbackSearchOptions,
@AuthenticatedUser() user: UserModel): Promise<GetFeedbackResponse> {
const canSeeAllFeedback = PermissionsService.canSeeAllFeedback(user);
const feedback = await this.feedbackService.getFeedback(canSeeAllFeedback, user, options);
return { error: null, feedback };
}

Expand All @@ -39,7 +42,7 @@ export class FeedbackController {
async updateFeedbackStatus(@Params() params: UuidParam,
@Body() updateFeedbackStatusRequest: UpdateFeedbackStatusRequest,
@AuthenticatedUser() user: UserModel): Promise<UpdateFeedbackStatusResponse> {
if (!PermissionsService.canRespondToFeedback(user)) throw new ForbiddenError();
if (!PermissionsService.canSeeAllFeedback(user)) throw new ForbiddenError();
const feedback = await this.feedbackService.updateFeedbackStatus(params.uuid, updateFeedbackStatusRequest.status);
return { error: null, feedback };
}
Expand Down
6 changes: 4 additions & 2 deletions api/controllers/MerchStoreController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
CompleteOrderPickupEventResponse,
GetOrderPickupEventResponse,
CancelOrderPickupEventResponse,
CancelMerchOrderResponse,
} from '../../types';
import { UuidParam } from '../validators/GenericRequests';
import { AuthenticatedUser } from '../decorators/AuthenticatedUser';
Expand Down Expand Up @@ -309,10 +310,11 @@ export class MerchStoreController {
}

@Post('/order/:uuid/cancel')
async cancelMerchOrder(@Params() params: UuidParam, @AuthenticatedUser() user: UserModel) {
async cancelMerchOrder(@Params() params: UuidParam,
@AuthenticatedUser() user: UserModel): Promise<CancelMerchOrderResponse> {
if (!PermissionsService.canAccessMerchStore(user)) throw new ForbiddenError();
const order = await this.merchStoreService.cancelMerchOrder(params.uuid, user);
return { error: null, order };
return { error: null, order: order.getPublicOrderWithItems() };
}

@Post('/order/:uuid/fulfill')
Expand Down
11 changes: 5 additions & 6 deletions api/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,15 @@ export class UserController {
@AuthenticatedUser() user: UserModel): Promise<InsertSocialMediaResponse> {
const userSocialMedia = await this.userSocialMediaService
.insertSocialMediaForUser(user, insertSocialMediaRequest.socialMedia);
return { error: null, userSocialMedia: userSocialMedia.getPublicSocialMedia() };
return { error: null, userSocialMedia: userSocialMedia.map((socialMedia) => socialMedia.getPublicSocialMedia()) };
}

@Patch('/socialMedia/:uuid')
async updateSocialMediaForUser(@Params() params: UuidParam,
@Body() updateSocialMediaRequest: UpdateSocialMediaRequest,
@Patch('/socialMedia')
async updateSocialMediaForUser(@Body() updateSocialMediaRequest: UpdateSocialMediaRequest,
@AuthenticatedUser() user: UserModel): Promise<UpdateSocialMediaResponse> {
const userSocialMedia = await this.userSocialMediaService
.updateSocialMediaByUuid(user, params.uuid, updateSocialMediaRequest.socialMedia);
return { error: null, userSocialMedia: userSocialMedia.getPublicSocialMedia() };
.updateSocialMediaByUuid(user, updateSocialMediaRequest.socialMedia);
return { error: null, userSocialMedia: userSocialMedia.map((socialMedia) => socialMedia.getPublicSocialMedia()) };
}

@Delete('/socialMedia/:uuid')
Expand Down
25 changes: 22 additions & 3 deletions api/validators/FeedbackControllerRequests.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { Type } from 'class-transformer';
import { IsDefined, IsNotEmpty, MinLength, ValidateNested } from 'class-validator';
import { Allow, IsDefined, IsNotEmpty, MinLength, ValidateNested } from 'class-validator';
import { IsValidFeedbackType, IsValidFeedbackStatus } from '../decorators/Validators';
import {
SubmitEventFeedbackRequest as ISubmitEventFeedbackRequest,
SubmitFeedbackRequest as ISubmitFeedbackRequest,
UpdateFeedbackStatusRequest as IUpdateFeedbackStatusRequest,
FeedbackSearchOptions as IFeedbackSearchOptions,
Feedback as IFeedback,
FeedbackType,
FeedbackStatus,
Uuid,
} from '../../types';

export class Feedback implements IFeedback {
@IsDefined()
@IsNotEmpty()
title: string;
event: Uuid;

@IsDefined()
@MinLength(100)
source: string;

@IsDefined()
@MinLength(20)
description: string;

@IsDefined()
Expand All @@ -41,3 +46,17 @@ export class UpdateFeedbackStatusRequest implements IUpdateFeedbackStatusRequest
@IsValidFeedbackStatus()
status: FeedbackStatus;
}

export class FeedbackSearchOptions implements IFeedbackSearchOptions {
@Allow()
event?: string;

@Allow()
status?: string;

@Allow()
type?: string;

@Allow()
user?: string;
}
11 changes: 8 additions & 3 deletions api/validators/UserSocialMediaControllerRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,25 @@ export class SocialMedia implements ISocialMedia {
}

export class SocialMediaPatches implements ISocialMediaPatches {
@IsDefined()
@IsNotEmpty()
url?: string;
uuid: string;

@IsDefined()
@IsNotEmpty()
url: string;
}

export class InsertSocialMediaRequest implements IInsertUserSocialMediaRequest {
@Type(() => SocialMedia)
@ValidateNested()
@IsDefined()
socialMedia: SocialMedia;
socialMedia: SocialMedia[];
}

export class UpdateSocialMediaRequest implements IUpdateUserSocialMediaRequest {
@Type(() => SocialMediaPatches)
@ValidateNested()
@IsDefined()
socialMedia: SocialMediaPatches;
socialMedia: SocialMediaPatches[];
}
17 changes: 17 additions & 0 deletions migrations/0042-remove-users-lastLogin-column.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

const TABLE_NAME = 'Users';

export class RemoveUsersLastLoginColumn1711518997063 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn(TABLE_NAME, 'lastLogin');
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(TABLE_NAME, new TableColumn({
name: 'lastLogin',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP(6)',
}));
}
}
54 changes: 54 additions & 0 deletions migrations/0043-add-feedback-event-colum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { MigrationInterface, QueryRunner, TableColumn, TableIndex, TableForeignKey } from 'typeorm';

const TABLE_NAME = 'Feedback';
const OLD_NAME = 'title';
const NEW_NAME = 'source';

export class AddFeedbackEventColum1711860173561 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" RENAME COLUMN "${OLD_NAME}" TO "${NEW_NAME}"`);
await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" ALTER COLUMN "${NEW_NAME}" TYPE text`);

await queryRunner.addColumn(
TABLE_NAME,
new TableColumn({
name: 'event',
type: 'uuid',
isNullable: true,
}),
);

await queryRunner.createIndex(
TABLE_NAME,
new TableIndex({
name: 'feedback_by_event_index',
columnNames: ['event'],
}),
);

await queryRunner.createForeignKey(
TABLE_NAME,
new TableForeignKey({
columnNames: ['event'],
referencedTableName: 'Events',
referencedColumnNames: ['uuid'],
onDelete: 'CASCADE',
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropForeignKey(TABLE_NAME, new TableForeignKey({
columnNames: ['event'],
referencedTableName: 'Events',
referencedColumnNames: ['uuid'],
onDelete: 'CASCADE',
}));

await queryRunner.dropIndex(TABLE_NAME, 'feedback_by_event_index');
await queryRunner.dropColumn(TABLE_NAME, 'event');

await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" ALTER COLUMN "${NEW_NAME}" TYPE varchar(255)`);
await queryRunner.query(`ALTER TABLE "${TABLE_NAME}" RENAME COLUMN "${NEW_NAME}" TO "${OLD_NAME}"`);
}
}
4 changes: 4 additions & 0 deletions models/EventModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as moment from 'moment';
import { BaseEntity, Column, Entity, Index, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { PublicEvent, Uuid } from '../types';
import { AttendanceModel } from './AttendanceModel';
import { FeedbackModel } from './FeedbackModel';
import { ExpressCheckinModel } from './ExpressCheckinModel';

@Entity('Events')
Expand Down Expand Up @@ -60,6 +61,9 @@ export class EventModel extends BaseEntity {
@OneToMany((type) => AttendanceModel, (attendance) => attendance.event, { cascade: true })
attendances: AttendanceModel[];

@OneToMany((type) => FeedbackModel, (feedback) => feedback.event, { cascade: true })
feedback: FeedbackModel[];

@OneToMany((type) => ExpressCheckinModel, (expressCheckin) => expressCheckin.event, { cascade: true })
expressCheckins: ExpressCheckinModel[];

Expand Down
15 changes: 12 additions & 3 deletions models/FeedbackModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseEntity, Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { FeedbackStatus, FeedbackType, PublicFeedback, Uuid } from '../types';
import { UserModel } from './UserModel';
import { EventModel } from './EventModel';

@Entity('Feedback')
export class FeedbackModel extends BaseEntity {
Expand All @@ -12,8 +13,15 @@ export class FeedbackModel extends BaseEntity {
@Index('feedback_by_user_index')
user: UserModel;

@Column('varchar', { length: 255 })
title: string;
@ManyToOne((type) => EventModel, (event) => event.feedback, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'event' })
@Index('feedback_by_event_index')
event: EventModel;

// source refers to how the user heard about the event
// ie, Instagram, Portal, Discord, etc.
@Column('text')
source: string;

@Column('text')
description: string;
Expand All @@ -31,7 +39,8 @@ export class FeedbackModel extends BaseEntity {
return {
uuid: this.uuid,
user: this.user.getPublicProfile(),
title: this.title,
event: this.event.getPublicEvent(),
source: this.source,
description: this.description,
timestamp: this.timestamp,
status: this.status,
Expand Down
3 changes: 0 additions & 3 deletions models/UserModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ export class UserModel extends BaseEntity {
@Column('integer', { default: 0 })
credits: number;

@Column('timestamptz', { default: () => 'CURRENT_TIMESTAMP(6)' })
lastLogin: Date;

@OneToMany((type) => ActivityModel, (activity) => activity.user, { cascade: true })
activities: ActivityModel[];

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@acmucsd/membership-portal",
"version": "3.4.2",
"version": "3.5.1",
"description": "REST API for ACM UCSD's membership portal.",
"main": "index.d.ts",
"files": [
Expand All @@ -12,6 +12,7 @@
"start": "node build/index.js",
"test": "jest --config jest.config.json --runInBand",
"dev": "nodemon -L -e ts -i node_modules -i .git -V index.ts",
"release": "ts-node ./node_modules/typeorm/cli.js migration:run && node build/index.js",
"lint": "eslint ./ --ext .ts",
"lint:fix": "eslint ./ --ext .ts --fix",
"db:migrate:create": "ts-node ./node_modules/typeorm/cli.js migration:create -n",
Expand Down
50 changes: 41 additions & 9 deletions repositories/FeedbackRepository.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { EntityRepository } from 'typeorm';
import { EntityRepository, SelectQueryBuilder } from 'typeorm';
import { FeedbackModel } from '../models/FeedbackModel';
import { UserModel } from '../models/UserModel';
import { EventModel } from '../models/EventModel';
import { BaseRepository } from './BaseRepository';
import { Uuid } from '../types';
import { FeedbackSearchOptions, Uuid } from '../types';

@EntityRepository(FeedbackModel)
export class FeedbackRepository extends BaseRepository<FeedbackModel> {
public async getAllFeedback(): Promise<FeedbackModel[]> {
return this.getBaseFindQuery().getMany();
public async getAllFeedback(options: FeedbackSearchOptions): Promise<FeedbackModel[]> {
return this.getBaseFindQuery(options).getMany();
}

public async findByUuid(uuid: Uuid): Promise<FeedbackModel> {
return this.getBaseFindQuery().where({ uuid }).getOne();
return this.getBaseFindQuery({}).where({ uuid }).getOne();
}

public async getAllFeedbackForUser(user: UserModel): Promise<FeedbackModel[]> {
return this.getBaseFindQuery().where({ user }).getMany();
// temporary fix for getting feedback for a user for an event
public async getStandardUserFeedback(user: UserModel, options: FeedbackSearchOptions): Promise<FeedbackModel[]> {
let query = this.getBaseFindQuery(options);
query = query.andWhere('feedback.user = :user', { user: user.uuid });

return query.getMany();
}

public async getAllFeedbackForUser(user: UserModel, options: FeedbackSearchOptions): Promise<FeedbackModel[]> {
return this.getBaseFindQuery(options).where({ user }).getMany();
}

public async upsertFeedback(feedback: FeedbackModel,
Expand All @@ -24,9 +33,32 @@ export class FeedbackRepository extends BaseRepository<FeedbackModel> {
return this.repository.save(feedback);
}

private getBaseFindQuery() {
return this.repository.createQueryBuilder('feedback')
public async hasUserSubmittedFeedback(user: UserModel, event: EventModel): Promise<boolean> {
const count = await this.repository.count({
where: { user, event },
});
return count > 0;
}

private getBaseFindQuery(options: FeedbackSearchOptions): SelectQueryBuilder<FeedbackModel> {
let query = this.repository.createQueryBuilder('feedback')
.leftJoinAndSelect('feedback.user', 'user')
.leftJoinAndSelect('feedback.event', 'event')
.orderBy('timestamp', 'DESC');

if (options.event) {
query = query.andWhere('event = :event').setParameter('event', options.event);
}
if (options.user) {
query = query.andWhere('"user" = :user').setParameter('user', options.user);
}
if (options.type) {
query = query.andWhere('type = :type').setParameter('type', options.type);
}
if (options.status) {
query = query.andWhere('status = :status').setParameter('status', options.status);
}

return query;
}
}
Loading

0 comments on commit 699d758

Please sign in to comment.