Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/acmucsd/membership-portal
Browse files Browse the repository at this point in the history
…into feature/merch-store-refactor

merging from master
  • Loading branch information
nik-dange committed May 11, 2024
2 parents 7bc659e + 9893841 commit 5e092c9
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 25 deletions.
8 changes: 4 additions & 4 deletions api/controllers/AdminController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
CreateMilestoneResponse,
CreateBonusResponse,
UploadBannerResponse,
GetAllEmailsResponse,
GetAllNamesAndEmailsResponse,
SubmitAttendanceForUsersResponse,
ModifyUserAccessLevelResponse,
GetAllUserAccessLevelsResponse,
Expand Down Expand Up @@ -41,10 +41,10 @@ export class AdminController {
}

@Get('/email')
async getAllEmails(@AuthenticatedUser() user: UserModel): Promise<GetAllEmailsResponse> {
async getAllNamesAndEmails(@AuthenticatedUser() user: UserModel): Promise<GetAllNamesAndEmailsResponse> {
if (!PermissionsService.canSeeAllUserEmails(user)) throw new ForbiddenError();
const emails = await this.userAccountService.getAllEmails();
return { error: null, emails };
const namesAndEmails = await this.userAccountService.getAllNamesAndEmails();
return { error: null, namesAndEmails };
}

@Post('/milestone')
Expand Down
7 changes: 7 additions & 0 deletions api/validators/EventControllerRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PatchEventRequest as IPatchEventRequest,
SubmitEventFeedbackRequest as ISubmitEventFeedbackRequest,
Event as IEvent,
Uuid,
} from '../../types';
import { IsValidEventFeedback } from '../decorators/Validators';

Expand All @@ -28,6 +29,12 @@ export class OptionalEventProperties implements IOptionalEventProperties {

@Allow()
staffPointBonus?: number;

@Allow()
discordEvent?: Uuid;

@Allow()
googleCalendarEvent?: Uuid;
}

export class Event extends OptionalEventProperties implements IEvent {
Expand Down
33 changes: 33 additions & 0 deletions migrations/0044-add-discord-and-google-calendar-event-columns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

const TABLE_NAME = 'Events';

export class AddDiscordAndGoogleCalendarEventColumns1712185658430 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns(TABLE_NAME, [
new TableColumn({
name: 'discordEvent',
type: 'uuid',
isNullable: true,
}),
new TableColumn({
name: 'googleCalendarEvent',
type: 'uuid',
isNullable: true,
}),
]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumns(TABLE_NAME, [
new TableColumn({
name: 'discordEvent',
type: 'uuid',
}),
new TableColumn({
name: 'googleCalendarEvent',
type: 'uuid',
}),
]);
}
}
12 changes: 12 additions & 0 deletions migrations/0045-fix-discord-gcal-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class FixDiscordGcalField1714770061929 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "Events" ALTER COLUMN "discordEvent" TYPE varchar');
await queryRunner.query('ALTER TABLE "Events" ALTER COLUMN "googleCalendarEvent" TYPE varchar');
}

public async down(queryRunner: QueryRunner): Promise<void> {
// nothing here because it's fixing an earlier migration (# 0044)
}
}
8 changes: 8 additions & 0 deletions models/EventModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export class EventModel extends BaseEntity {
@OneToMany((type) => ExpressCheckinModel, (expressCheckin) => expressCheckin.event, { cascade: true })
expressCheckins: ExpressCheckinModel[];

@Column('varchar', { nullable: true })
discordEvent: Uuid;

@Column('varchar', { nullable: true })
googleCalendarEvent: Uuid;

public getPublicEvent(canSeeAttendanceCode = false): PublicEvent {
const publicEvent: PublicEvent = {
uuid: this.uuid,
Expand All @@ -83,6 +89,8 @@ export class EventModel extends BaseEntity {
pointValue: this.pointValue,
requiresStaff: this.requiresStaff,
staffPointBonus: this.staffPointBonus,
discordEvent: this.discordEvent,
googleCalendarEvent: this.googleCalendarEvent,
};
if (canSeeAttendanceCode) publicEvent.attendanceCode = this.attendanceCode;
return publicEvent;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@acmucsd/membership-portal",
"version": "3.5.1",
"version": "3.6.1",
"description": "REST API for ACM UCSD's membership portal.",
"main": "index.d.ts",
"files": [
Expand Down
14 changes: 9 additions & 5 deletions repositories/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EntityRepository, In } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Activity } from '../types/internal';
import { UserModel } from '../models/UserModel';
import { Uuid } from '../types';
import { Uuid, NameAndEmail } from '../types';
import { BaseRepository } from './BaseRepository';

@EntityRepository(UserModel)
Expand Down Expand Up @@ -50,12 +50,16 @@ export class UserRepository extends BaseRepository<UserModel> {
return this.repository.findOne({ accessCode });
}

public async getAllEmails(): Promise<string[]> {
const emailsRaw = await this.repository
public async getAllNamesAndEmails(): Promise<NameAndEmail[]> {
const namesAndEmailsRaw = await this.repository
.createQueryBuilder()
.select('email')
.select(['email', 'UserModel.firstName', 'UserModel.lastName'])
.getRawMany();
return emailsRaw.map((emailRaw) => emailRaw.email);
const namesAndEmailsFormatted: NameAndEmail[] = namesAndEmailsRaw.map((nameAndEmailRaw) => ({ firstName:
nameAndEmailRaw.UserModel_firstName,
lastName: nameAndEmailRaw.UserModel_lastName,
email: nameAndEmailRaw.email }));
return namesAndEmailsFormatted;
}

public static async generateHash(pass: string): Promise<string> {
Expand Down
10 changes: 5 additions & 5 deletions services/EmailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class EmailService {
order,
orderItems: ejs.render(EmailService.itemDisplayTemplate, { items: order.items, totalCost: order.totalCost }),
pickupEvent: order.pickupEvent,
link: `${Config.client}/store/order/${order.uuid}`,
link: `${Config.client}/store/orders`,
}),
};
await this.sendEmail(data);
Expand Down Expand Up @@ -141,7 +141,7 @@ export default class EmailService {
firstName,
order,
orderItems: ejs.render(EmailService.itemDisplayTemplate, { items: order.items, totalCost: order.totalCost }),
link: `${Config.client}/store/order/${order.uuid}`,
link: `${Config.client}/store/orders`,
}),
};
await this.sendEmail(data);
Expand All @@ -160,7 +160,7 @@ export default class EmailService {
firstName,
order,
orderItems: ejs.render(EmailService.itemDisplayTemplate, { items: order.items, totalCost: order.totalCost }),
link: `${Config.client}/store/order/${order.uuid}`,
link: `${Config.client}/store/orders`,
}),
};
await this.sendEmail(data);
Expand All @@ -179,7 +179,7 @@ export default class EmailService {
firstName,
order,
orderItems: ejs.render(EmailService.itemDisplayTemplate, { items: order.items, totalCost: order.totalCost }),
link: `${Config.client}/store/order/${order.uuid}`,
link: `${Config.client}/store/orders`,
}),
};
await this.sendEmail(data);
Expand Down Expand Up @@ -219,7 +219,7 @@ export default class EmailService {
unfulfilledItems: ejs.render(EmailService.itemDisplayTemplate, { items: unfulfilledItems }),
fulfilledItems: ejs.render(EmailService.itemDisplayTemplate, { items: fulfilledItems }),
pickupEvent,
link: `${Config.client}/store/order/${orderUuid}`,
link: `${Config.client}/store/orders`,
}),
};
await this.sendEmail(data);
Expand Down
5 changes: 3 additions & 2 deletions services/UserAccountService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
UserPatches,
UserState,
PrivateProfile,
NameAndEmail,
} from '../types';
import { UserRepository } from '../repositories/UserRepository';
import { UserModel } from '../models/UserModel';
Expand Down Expand Up @@ -181,10 +182,10 @@ export default class UserAccountService {
});
}

public async getAllEmails(): Promise<string[]> {
public async getAllNamesAndEmails(): Promise<NameAndEmail[]> {
return this.transactions.readOnly(async (txn) => Repositories
.user(txn)
.getAllEmails());
.getAllNamesAndEmails());
}

/**
Expand Down
15 changes: 10 additions & 5 deletions tests/admin.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestError, ForbiddenError } from 'routing-controllers';
import { In } from 'typeorm';
import { ActivityScope, ActivityType, SubmitAttendanceForUsersRequest, UserAccessType } from '../types';
import { ActivityScope, ActivityType, SubmitAttendanceForUsersRequest, UserAccessType, NameAndEmail } from '../types';
import { ControllerFactory } from './controllers';
import { DatabaseConnection, EventFactory, PortalState, UserFactory } from './data';
import { UserModel } from '../models/UserModel';
Expand Down Expand Up @@ -131,19 +131,24 @@ describe('retroactive attendance submission', () => {
});
});

describe('email retrieval', () => {
describe('names and emails retrieval', () => {
test('gets all the emails of stored users', async () => {
const conn = await DatabaseConnection.get();
const users = UserFactory.create(5);
const emails = users.map((user) => user.email.toLowerCase());
const namesAndEmails: NameAndEmail[] = users.map((user) => ({ firstName: user.firstName,
lastName: user.lastName,
email:
user.email.toLowerCase() }));
const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN });

await new PortalState()
.createUsers(...users, admin)
.write();

const response = await ControllerFactory.admin(conn).getAllEmails(admin);
expect(expect.arrayContaining(response.emails)).toEqual([...emails, admin.email]);
const response = await ControllerFactory.admin(conn).getAllNamesAndEmails(admin);
const expected: NameAndEmail = { firstName: admin.firstName, lastName: admin.lastName, email: admin.email };
expect(expect.arrayContaining(response.namesAndEmails)).toEqual([...namesAndEmails,
expected]);
});
});

Expand Down
2 changes: 2 additions & 0 deletions tests/data/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class EventFactory {
deleted: false,
eventLink: faker.internet.url(),
thumbnail: FactoryUtils.getRandomImageUrl(),
discordEvent: faker.datatype.hexaDecimal(10),
googleCalendarEvent: faker.datatype.hexaDecimal(10),
});
return EventModel.merge(fake, substitute);
}
Expand Down
2 changes: 2 additions & 0 deletions types/ApiRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export interface OptionalEventProperties {
eventLink?: string;
requiresStaff?: boolean;
staffPointBonus?: number;
discordEvent?: Uuid;
googleCalendarEvent?: Uuid;
}

export interface Event extends OptionalEventProperties {
Expand Down
12 changes: 9 additions & 3 deletions types/ApiResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
import { MerchItemOptionMetadata, Uuid } from '.';

// RESPONSE TYPES

export interface CustomErrorBody {
name: string;
message: string;
Expand All @@ -31,8 +30,8 @@ export interface UploadBannerResponse extends ApiResponse {
banner: string;
}

export interface GetAllEmailsResponse extends ApiResponse {
emails: string[];
export interface GetAllNamesAndEmailsResponse extends ApiResponse {
namesAndEmails: NameAndEmail[];
}

export interface SubmitAttendanceForUsersResponse extends ApiResponse {
Expand Down Expand Up @@ -117,6 +116,8 @@ export interface PublicEvent {
pointValue: number;
requiresStaff: boolean;
staffPointBonus: number;
discordEvent: Uuid;
googleCalendarEvent: Uuid;
}

export interface GetPastEventsResponse extends ApiResponse {
Expand Down Expand Up @@ -323,6 +324,11 @@ export interface FulfillMerchOrderResponse extends ApiResponse {
}

// USER
export interface NameAndEmail {
firstName: string;
lastName: string;
email: string;
}

export interface PublicActivity {
type: ActivityType,
Expand Down

0 comments on commit 5e092c9

Please sign in to comment.