Skip to content

Commit

Permalink
feat: init function for retrieving interviewers
Browse files Browse the repository at this point in the history
  • Loading branch information
whiitex committed Jun 1, 2024
1 parent 238f415 commit 4201b9e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 34 deletions.
2 changes: 2 additions & 0 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UsersModule } from './users/users.module';
import { ApplicationsModule } from './application/applications.module';
import { RecruitmentSessionModule } from './recruitment-session/recruitment-session.module';
import { TimeSlotsModule } from './timeslots/timeslots.module';
import { InterviewModule } from './interview/interview.module';
import { AuthenticationModule } from './authentication/authentication.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtGuard } from './authentication/jwt-guard.guard';
Expand Down Expand Up @@ -39,6 +40,7 @@ import { AvailabilityModule } from './availability/availability.module';
RecruitmentSessionModule,
TimeSlotsModule,
UsersModule,
InterviewModule,
],
providers: [
{
Expand Down
14 changes: 14 additions & 0 deletions api/src/interview/interview.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { AuthenticatedRequest } from 'src/authorization/authenticated-request.types';
import * as Joi from 'joi';
import { CheckPolicies } from 'src/authorization/check-policies.decorator';
import { Ability } from 'src/authorization/ability.decorator';
import { Application } from 'src/application/application.entity';
import { TimeSlot } from 'src/timeslots/timeslot.entity';

@ApiBearerAuth()
@ApiTags('interview')
Expand Down Expand Up @@ -137,4 +140,15 @@ export class InterviewController {
throw new ConflictException();
}
}

// @ApiUnauthorizedResponse()
// @CheckPolicies((ability) => ability.can(Action.Read, 'Interview'))
@Post('createInterview')
async CreateInterview(
@Body() interview: CreateInterviewDto,
@Body() application: Application,
@Body() timeslot: TimeSlot,
): Promise<Interview> {
return await this.interviewService.create(interview, application, timeslot);
}
}
33 changes: 22 additions & 11 deletions api/src/interview/interview.entity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
Relation,
} from 'typeorm';
import { Interview as InterviewSlot } from '@hkrecruitment/shared';
import { User } from '../users/user.entity';
import { TimeSlot } from '../timeslots/timeslot.entity';
import { Application } from '../application/application.entity';
import { on } from 'events';

@Entity()
export class Interview implements InterviewSlot {
Expand All @@ -15,18 +23,21 @@ export class Interview implements InterviewSlot {
@Column({ name: 'created_at' })
createdAt: Date;

@Column()
timeslot: TimeSlot;
@OneToOne(() => TimeSlot)
timeslot: Relation<TimeSlot>;

@Column()
application: Application;
@OneToOne(() => Application)
application: Relation<Application>;

@Column({ name: 'interviewer_1' })
interviewer1: User;
@OneToOne(() => User)
@JoinColumn({ name: 'interviewer_1' })
interviewer1: Relation<User>;

@Column({ name: 'interviewer_2' })
interviewer2: User;
@OneToOne(() => User)
@JoinColumn({ name: 'interviewer_2' })
interviewer2: Relation<User>;

@Column({ name: 'optional_interviewer', nullable: true })
optionalInterviewer?: User;
@OneToOne(() => User)
@JoinColumn({ name: 'optional_interviewer' })
optionalInterviewer?: Relation<User>;
}
7 changes: 6 additions & 1 deletion api/src/interview/interview.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { InterviewController } from './interview.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Interview } from './interview.entity';
import { RecruitmentSessionModule } from 'src/recruitment-session/recruitment-session.module';
import { TimeSlotsModule } from 'src/timeslots/timeslots.module';

@Module({
imports: [TypeOrmModule.forFeature([Interview]), RecruitmentSessionModule],
imports: [
TypeOrmModule.forFeature([Interview]),
RecruitmentSessionModule,
TimeSlotsModule,
],
providers: [InterviewService],
controllers: [InterviewController],
exports: [InterviewService],
Expand Down
104 changes: 92 additions & 12 deletions api/src/interview/interview.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Interview } from './interview.entity';
import { Application } from '../application/application.entity';
import { TimeSlot } from '../timeslots/timeslot.entity';
import { User } from '../users/user.entity';
import { InjectRepository, InjectDataSource } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { Repository, DataSource } from 'typeorm';
import { CreateInterviewDto } from './create-interview.dto';
import { transaction } from 'src/utils/database';
import {
AvailabilityState,
Role,
RecruitmentSessionState,
} from '@hkrecruitment/shared';
import { RecruitmentSessionService } from 'src/recruitment-session/recruitment-session.service';

@Injectable()
export class InterviewService {
constructor(
@InjectRepository(Interview)
private readonly interviewRepository: Repository<Interview>,
@InjectRepository(TimeSlot)
private readonly timeSlotRepository: Repository<TimeSlot>,
@InjectDataSource()
private dataSource: DataSource,
private readonly recruitmentSessionService: RecruitmentSessionService,
Expand All @@ -26,24 +34,96 @@ export class InterviewService {
return await this.interviewRepository.remove(interview);
}

async countInterviewsPerGivenUser(user: User): Promise<number> {
const queryBuilder =
this.interviewRepository.createQueryBuilder('Interview');
queryBuilder
.innerJoin('Interview.interviewer1', 'interviewer1')
.innerJoin('Interview.interviewer2', 'interviewer2')
.innerJoin('Interview.optionalInterviewer', 'optionalInterviewer')
.where('interviewer1.oauthId = :userId', { userId: user.oauthId })
.orWhere('interviewer2.oauthId = :userId', { userId: user.oauthId })
.orWhere('optionalInterviewer.oauthId = :userId', {
userId: user.oauthId,
});
return await queryBuilder.getCount();
}

async create(
interview: CreateInterviewDto,
application: Application,
timeslot: TimeSlot,
): Promise<Interview> {
return transaction(this.dataSource, async (queryRunner) => {
const recruitmentSession =
await this.recruitmentSessionService.findActiveRecruitmentSession();

// filtrare applicant no None e No applicant
// join per disponibilità usando timeslot, caso non disponibili mando eccezione,
// 1 board 1 expert necessaria MODIFICA FUTURA

const scheduledInterview = await queryRunner.manager
.getRepository(Interview)
.save({ ...interview, application, timeslot, recruitmentSession });
return recruitmentSession;
const queryBuilder = this.timeSlotRepository.createQueryBuilder('TimeSlot');
queryBuilder
.innerJoinAndSelect('TimeSlot.availabilities', 'availability')
.innerJoinAndSelect('TimeSlot.recruitmentSession', 'recruitmentSession')
.innerJoinAndSelect('availability.user', 'user')
.where('recruitmentSession.state = :recruitmentSessionState', {
recruitmentSessionState: RecruitmentSessionState.Active,
})

// only the given time slot
.andWhere('TimeSlot.id = :timeSlotId', { timeSlotId: timeslot.id })
.andWhere('user.role NOT IN (:...roles)', {
roles: [Role.None, Role.Applicant],
})

.andWhere(
'availability.state = :availabilityState AND (user.is_board = true OR user.is_expert = true)',
{
availabilityState: AvailabilityState.Free,
},
)
.andWhere(
'(SELECT COUNT(availability.id) FROM Availability availability WHERE availability.timeSlotId = TimeSlot.id) > 1',
);

const allMatchesGivenTimeslot: TimeSlot[] = await queryBuilder.getMany();

// all available experts only members sorted by assigned interviews
const availableExpert = allMatchesGivenTimeslot[0].availabilities
.filter((av) => {
return av.user.is_expert && !av.user.is_board;
})
.map((av) => av.user);
let availableExpertSorted = [];
availableExpert.forEach((expert) => {
const interviewCount = this.countInterviewsPerGivenUser(expert);
availableExpertSorted.push([expert, interviewCount]);
});

// all available board members sorted by assigned interviews
const availableBoard = allMatchesGivenTimeslot[0].availabilities
.filter((av) => {
return av.user.is_board;
})
.map((av) => av.user);
let availableBoardSorted = [];
availableBoard.forEach((board) => {
const interviewCount = this.countInterviewsPerGivenUser(board);
availableBoardSorted.push([board, interviewCount]);
});

// all available optional member
const availableOptional = allMatchesGivenTimeslot[0].availabilities
.filter((av) => {
return !av.user.is_board && !av.user.is_expert;
})
.map((av) => av.user);
let availableOptionalSorted = [];
availableOptional.forEach((optional) => {
const interviewCount = this.countInterviewsPerGivenUser(optional);
availableOptionalSorted.push([optional, interviewCount]);
});

availableOptionalSorted = availableOptionalSorted.filter((optional) => {
return optional[1] == 0;
});
availableExpertSorted.sort((a, b) => a[1] - b[1]);
availableBoardSorted.sort((a, b) => a[1] - b[1]);


}

async update(interview: Interview): Promise<Interview> {
Expand Down
29 changes: 28 additions & 1 deletion api/src/mocks/requests.http
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
GET http://localhost:3000/v1/timeslots/
Authorization: Bearer <personal-token>
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imc4aFdBRTB0U00tX3JJVUR0WElNMyJ9.eyJlbWFpbCI6InBhc3F1YWxlLmJpYW5jb0Boa25wb2xpdG8ub3JnIiwiaXNzIjoiaHR0cHM6Ly9kZXYtYzhyb29jZGw3NjNsbDVxZi5ldS5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMDYyMjgyMTU0MzYxNTk5ODcwNTEiLCJhdWQiOlsiaHR0cDovL2hrcmVjcnVpdG1lbnQub3JnIiwiaHR0cHM6Ly9kZXYtYzhyb29jZGw3NjNsbDVxZi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNzE3MjYwMTI1LCJleHAiOjE3MTczNDY1MjUsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJhenAiOiJaekNWd2R2eUJOUWZKc3R1ZUJPcVh3TW1jazZCa0d4NiJ9.TFqppFFgYh7ujlPGaeLF1cg1WACxiof6SX0G3IocfSPMGidJdZtiaG1lRLwphATZ4QDdAGWK_gouMj56zOhFmOaKO4aaXl1nj93RY8EvyEsEC2kiKTUH_qNo9tgV_28zJ9lZn4rSohxTmUTXKE5o4uMKFDF24FALGprmRb_5CI2IZhO6buc3nFHpBXTf-rAZzXnPTeFGG7kb-l9v3MRo2HBYDiqM1Dj3B5fRhpVbHzO4V6oT93lsPAa0i2C0HhM9faesHR_X7p-MfzuH-Xgi4RqXQtpb7H1vv7_cfFM01anf-M7beew5btj4CEfLEAmi9qq5Lfik3TCFt-nPhg1i-A

###

POST http://localhost:3000/v1/interview/localhost:3000/v1/interview/createInterview/
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imc4aFdBRTB0U00tX3JJVUR0WElNMyJ9.eyJlbWFpbCI6InBhc3F1YWxlLmJpYW5jb0Boa25wb2xpdG8ub3JnIiwiaXNzIjoiaHR0cHM6Ly9kZXYtYzhyb29jZGw3NjNsbDVxZi5ldS5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMDYyMjgyMTU0MzYxNTk5ODcwNTEiLCJhdWQiOlsiaHR0cDovL2hrcmVjcnVpdG1lbnQub3JnIiwiaHR0cHM6Ly9kZXYtYzhyb29jZGw3NjNsbDVxZi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNzE3MjYwMTI1LCJleHAiOjE3MTczNDY1MjUsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJhenAiOiJaekNWd2R2eUJOUWZKc3R1ZUJPcVh3TW1jazZCa0d4NiJ9.TFqppFFgYh7ujlPGaeLF1cg1WACxiof6SX0G3IocfSPMGidJdZtiaG1lRLwphATZ4QDdAGWK_gouMj56zOhFmOaKO4aaXl1nj93RY8EvyEsEC2kiKTUH_qNo9tgV_28zJ9lZn4rSohxTmUTXKE5o4uMKFDF24FALGprmRb_5CI2IZhO6buc3nFHpBXTf-rAZzXnPTeFGG7kb-l9v3MRo2HBYDiqM1Dj3B5fRhpVbHzO4V6oT93lsPAa0i2C0HhM9faesHR_X7p-MfzuH-Xgi4RqXQtpb7H1vv7_cfFM01anf-M7beew5btj4CEfLEAmi9qq5Lfik3TCFt-nPhg1i-A
Content-Type: application/json

{
"interview": {
"createdAt": "2021-04-01T00:00:00.000Z",
"timeslot_id": 1
},
"application": {
"id": 1,
"type": "internship",
"applicantId": "ciao-123",
"submission": "2021-04-01T00:00:00.000Z",
"lastModified": "2021-04-01T00:00:00.000Z",
"cv": "https://www.google.com",
"itaLevel": "B1"
},
"timeslot": {
"id": 1,
"start": "2021-04-01T00:00:00.000Z",
"end": "2021-04-01T00:00:00.000Z"
}
}
4 changes: 2 additions & 2 deletions api/src/timeslots/timeslots.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { Action } from '@hkrecruitment/shared';
export class TimeSlotsController {
constructor(private readonly timeSlotsService: TimeSlotsService) {}

@ApiUnauthorizedResponse()
@CheckPolicies((ability) => ability.can(Action.Read, 'TimeSlot'))
// @ApiUnauthorizedResponse()
// @CheckPolicies((ability) => ability.can(Action.Read, 'TimeSlot'))
@Get()
async findAvailableTimeSlots(): Promise<TimeSlot[]> {
return await this.timeSlotsService.findAvailableTimeSlots();
Expand Down
14 changes: 7 additions & 7 deletions shared/src/interview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ export interface Interview {
export const createInterviewSchema = Joi.object<Interview>({
notes: Joi.string().required(),
createdAt: Joi.date().required(),
interviewer1: createUserSchema.required(),
interviewer2: createUserSchema.required(),
optionalInterviewer: createUserSchema.optional(),
interviewer1: Joi.object<Person>().required(),
interviewer2: Joi.object<Person>().required(),
optionalInterviewer: Joi.object<Person>().optional(),
});

export const updateInterviewSchema = Joi.object<Interview>({
notes: Joi.string().optional(),
createdAt: Joi.date().optional(),
//timeslot: updateTimeSlotSchema.optional(),
application: updateApplicationSchema.optional(),
interviewer1: updateUserSchema.optional(),
interviewer2: updateUserSchema.optional(),
optionalInterviewer: updateUserSchema.optional(),
application: Joi.object<Person>().optional(),
interviewer1: Joi.object<Person>().optional(),
interviewer2: Joi.object<Person>().optional(),
optionalInterviewer: Joi.object<Person>().optional(),
});

export const applyAbilitiesOnInterview: ApplyAbilities = (
Expand Down

0 comments on commit 4201b9e

Please sign in to comment.