Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Merge of stabilized features from branch 'development' into 'main' #114

Merged
merged 15 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1cdc1e4
#104 [FIX]: Bug in image deletion and responses with itemValidations
IosBonaldi Jul 25, 2024
e6753e2
#104 [FIX]: ItemValidations in applicationController response
IosBonaldi Jul 30, 2024
c7110d6
Merge pull request #105 from VRI-UFPR/104-fix-bug-in-image-deletion-a…
IosBonaldi Aug 12, 2024
026e117
#106 [FEAT]: Add search functionality for classrooms by name and for …
IosBonaldi Aug 13, 2024
e70e469
#106 [FIX]: Missing properties in fieldsWViewers in applicationContro…
IosBonaldi Aug 13, 2024
1a71101
#106 [FIX]: Checking existence of files before unlinking
IosBonaldi Aug 22, 2024
4b4f3d5
Merge pull request #107 from VRI-UFPR/106-feat-endpoints-for-searchin…
IosBonaldi Sep 13, 2024
a5cba8f
#108 [FEAT]: Support for non-institutional users and classrooms
IosBonaldi Sep 13, 2024
b3a9481
#110 [FIX]: Validation of visualization hierarchy between protocols a…
IosBonaldi Oct 1, 2024
37e59e2
Merge pull request #109 from VRI-UFPR/108-feat-support-for-non-instit…
IosBonaldi Oct 1, 2024
94c536f
#110 [FIX]: Default values for viewers arrays in updateApplication
IosBonaldi Oct 5, 2024
c429496
#110 [FEAT]: Tablerow as ItemType and a missing line at applicationCo…
YuriTobias Oct 8, 2024
cd17c66
Merge pull request #111 from VRI-UFPR/110-fix-validation-of-visualiza…
IosBonaldi Oct 12, 2024
c0d1ecf
Merge branch 'main' of https://github.com/VRI-UFPR/PICCE-API into 112…
IosBonaldi Oct 12, 2024
849eafb
Merge pull request #113 from VRI-UFPR/112-fix-merge-branch-main-into-…
IosBonaldi Oct 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum ItemGroupType {
}

enum ItemType {
TABLEROW
TEXTBOX
CHECKBOX
RADIO
Expand Down Expand Up @@ -223,8 +224,8 @@ model Classroom {
visibleProtocolAnswers Protocol[] @relation(name: "ProtocolAnswersViewersClassroom") // protocols that the classroom can see the answers
visibleApplicationAnswers Application[] @relation(name: "ApplicationAnswersViewersClassroom") // applications that the classroom can see the answers

institutionId Int
institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade)
institutionId Int?
institution Institution? @relation(fields: [institutionId], references: [id], onDelete: Cascade)

@@unique([name, institutionId])
}
Expand Down
8 changes: 4 additions & 4 deletions src/controllers/applicationAnswerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ApplicationAnswer, User, UserRole, VisibilityMode } from '@prisma/clien
import * as yup from 'yup';
import prismaClient from '../services/prismaClient';
import errorFormatter from '../services/errorFormatter';
import { unlinkSync } from 'fs';
import { unlinkSync, existsSync } from 'fs';

const checkAuthorization = async (
user: User,
Expand Down Expand Up @@ -340,7 +340,7 @@ export const createApplicationAnswer = async (req: Request, res: Response) => {
res.status(201).json({ message: 'Application answer created.', data: createdApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down Expand Up @@ -454,7 +454,7 @@ export const updateApplicationAnswer = async (req: Request, res: Response): Prom
},
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
for (const file of filesToDelete) if (existsSync(file.path)) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
// Create new files (udpating files is not supported)
const itemAnswerFiles = files
Expand Down Expand Up @@ -534,7 +534,7 @@ export const updateApplicationAnswer = async (req: Request, res: Response): Prom
res.status(200).json({ message: 'Application answer updated.', data: upsertedApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down
50 changes: 36 additions & 14 deletions src/controllers/applicationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,28 @@ const validateVisibility = async (
const protocolViewers = await prismaClient.protocol.findUnique({
where: {
id: protocolId,
visibility: visibility,
answersVisibility: answersVisibility,
answersViewersUser: { every: { id: { in: answersViewersUsers } } },
answersViewersClassroom: { every: { id: { in: answersViewersClassrooms } } },
viewersUser: { every: { id: { in: viewersUsers } } },
viewersClassroom: { every: { id: { in: viewersClassrooms } } },
AND: [
{
OR: [
{ visibility: 'PUBLIC' },
{
visibility: visibility,
viewersUser: { every: { id: { in: viewersUsers } } },
viewersClassroom: { every: { id: { in: viewersClassrooms } } },
},
],
},
{
OR: [
{ answersVisibility: 'PUBLIC' },
{
answersVisibility: answersVisibility,
answersViewersUser: { every: { id: { in: answersViewersUsers } } },
answersViewersClassroom: { every: { id: { in: answersViewersClassrooms } } },
},
],
},
],
},
});

Expand All @@ -130,10 +146,14 @@ const fields = {

const fieldsWViewers = {
...fields,
viewersUser: { select: { id: true, username: true } },
viewersClassroom: { select: { id: true, institution: { select: { name: true } } } },
answersViewersUser: { select: { id: true, username: true } },
answersViewersClassroom: { select: { id: true, institution: { select: { name: true } } } },
viewersUser: { select: { id: true, username: true, classrooms: { select: { id: true, name: true } } } },
viewersClassroom: {
select: { id: true, name: true, institution: { select: { name: true } }, users: { select: { id: true, username: true } } },
},
answersViewersUser: { select: { id: true, username: true, classrooms: { select: { id: true, name: true } } } },
answersViewersClassroom: {
select: { id: true, name: true, institution: { select: { name: true } }, users: { select: { id: true, username: true } } },
},
};

const fieldsWProtocol = {
Expand All @@ -157,6 +177,7 @@ const fieldsWProtocol = {
type: true,
placement: true,
isRepeatable: true,
tableColumns: { select: { id: true, text: true, placement: true } },
items: {
orderBy: { placement: 'asc' as any },
select: {
Expand All @@ -175,6 +196,7 @@ const fieldsWProtocol = {
},
},
files: { select: { id: true, path: true, description: true } },
itemValidations: { select: { type: true, argument: true, customMessage: true } },
},
},
dependencies: { select: { type: true, argument: true, itemId: true, customMessage: true } },
Expand Down Expand Up @@ -254,10 +276,10 @@ export const updateApplication = async (req: Request, res: Response): Promise<vo
.shape({
visibility: yup.mixed<VisibilityMode>().oneOf(Object.values(VisibilityMode)),
answersVisibility: yup.mixed<VisibilityMode>().oneOf(Object.values(VisibilityMode)),
viewersUser: yup.array().of(yup.number()).required(),
viewersClassroom: yup.array().of(yup.number()).required(),
answersViewersUser: yup.array().of(yup.number()).required(),
answersViewersClassroom: yup.array().of(yup.number()).required(),
viewersUser: yup.array().of(yup.number()).default([]),
viewersClassroom: yup.array().of(yup.number()).default([]),
answersViewersUser: yup.array().of(yup.number()).default([]),
answersViewersClassroom: yup.array().of(yup.number()).default([]),
})
.noUnknown();
// Yup parsing/validation
Expand Down
69 changes: 59 additions & 10 deletions src/controllers/classroomController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ const fields = {
updatedAt: true,
};

const publicFields = {
id: true,
name: true,
users: { select: { id: true, name: true, username: true, role: true } },
};

// Only admins or the coordinator of the institution can perform C-UD operations on classrooms
const checkAuthorization = async (user: User, classroomId: number | undefined, institutionId: number | undefined, action: string) => {
switch (action) {
case 'create':
// Only ADMINs or members of an institution can perform create operations on its classrooms
if (user.role !== UserRole.ADMIN && (user.role === UserRole.USER || user.institutionId !== institutionId)) {
if (user.role !== UserRole.ADMIN && (user.role === UserRole.USER || (institutionId && user.institutionId !== institutionId))) {
throw new Error('This user is not authorized to perform this action');
}
break;
Expand All @@ -40,7 +46,12 @@ const checkAuthorization = async (user: User, classroomId: number | undefined, i
const classroom = await prismaClient.classroom.findUnique({
where: user.institutionId ? { id: classroomId, institutionId: user.institutionId } : { id: classroomId },
});
if (user.role === UserRole.USER || user.role === UserRole.APPLIER || !user.institutionId || !classroom) {
if (
user.role === UserRole.USER ||
user.role === UserRole.APPLIER ||
(institutionId && institutionId !== user.institutionId) ||
!classroom
) {
throw new Error('This user is not authorized to perform this action');
}
}
Expand All @@ -61,6 +72,11 @@ const checkAuthorization = async (user: User, classroomId: number | undefined, i
}
}
break;
case 'search':
if (user.role === UserRole.USER) {
throw new Error('This user is not authorized to perform this action');
}
break;
case 'getMy':
// All users can perform get my classrooms operation (the result will be filtered based on the user)
break;
Expand All @@ -73,9 +89,8 @@ export const createClassroom = async (req: Request, res: Response) => {
const createClassroomSchema = yup
.object()
.shape({
id: yup.number().min(1),
name: yup.string().min(1).max(255).required(),
institutionId: yup.number().required(),
name: yup.string().min(3).max(255).required(),
institutionId: yup.number(),
users: yup.array().of(yup.number()).min(2).required(),
})
.noUnknown();
Expand All @@ -88,9 +103,8 @@ export const createClassroom = async (req: Request, res: Response) => {
// Prisma operation
const createdClassroom: Classroom = await prismaClient.classroom.create({
data: {
id: classroom.id,
name: classroom.name,
institutionId: classroom.institutionId,
institution: { connect: classroom.institutionId ? { id: classroom.institutionId } : undefined },
users: { connect: classroom.users.map((id) => ({ id: id })) },
},
});
Expand All @@ -108,18 +122,26 @@ export const updateClassroom = async (req: Request, res: Response): Promise<void
// Yup schemas
const updateClassroomSchema = yup
.object()
.shape({ name: yup.string().min(1).max(255), users: yup.array().of(yup.number()).min(2) })
.shape({
name: yup.string().min(3).max(255),
institutionId: yup.number(),
users: yup.array().of(yup.number()).min(2),
})
.noUnknown();
// Yup parsing/validation
const classroom = await updateClassroomSchema.validate(req.body);
// User from Passport-JWT
const user = req.user as User;
// Check if user is authorized to update this classroom
await checkAuthorization(user, classroomId, undefined, 'update');
await checkAuthorization(user, classroomId, classroom.institutionId, 'update');
// Prisma operation
const updatedClassroom = await prismaClient.classroom.update({
where: { id: classroomId },
data: { name: classroom.name, users: { set: [], connect: classroom.users?.map((id) => ({ id: id })) } },
data: {
name: classroom.name,
institution: { disconnect: true, connect: classroom.institutionId ? { id: classroom.institutionId } : undefined },
users: { set: [], connect: classroom.users?.map((id) => ({ id: id })) },
},
select: fields,
});

Expand Down Expand Up @@ -179,6 +201,33 @@ export const getMyClassrooms = async (req: Request, res: Response): Promise<void
}
};

export const searchClassroomByName = async (req: Request, res: Response): Promise<void> => {
try {
// User from passport-jwt
const curUser = req.user as User;
// Check if user is authorized to search users
await checkAuthorization(curUser, undefined, undefined, 'search');
// Yup schemas
const searchUserSchema = yup
.object()
.shape({
term: yup.string().min(3).max(20).required(),
})
.noUnknown();
// Yup parsing/validation
const { term } = await searchUserSchema.validate(req.body);
// Prisma operation
const classrooms = await prismaClient.classroom.findMany({
where: { name: { startsWith: term } },
select: publicFields,
});

res.status(200).json({ message: 'Searched classrooms found.', data: classrooms });
} catch (error: any) {
res.status(400).json(errorFormatter(error));
}
};

export const deleteClassroom = async (req: Request, res: Response): Promise<void> => {
try {
// ID from params
Expand Down
18 changes: 9 additions & 9 deletions src/controllers/protocolController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ItemType, ItemGroupType, PageType, ItemValidationType, User, UserRole,
import * as yup from 'yup';
import prismaClient from '../services/prismaClient';
import errorFormatter from '../services/errorFormatter';
import { unlinkSync } from 'fs';
import { unlinkSync, existsSync } from 'fs';

const checkAuthorization = async (user: User, protocolId: number | undefined, action: string) => {
switch (action) {
Expand Down Expand Up @@ -325,10 +325,10 @@ const fields = {
const fieldsWViewers = {
...fields,
managers: { select: { id: true, username: true } },
viewersUser: { select: { id: true, username: true } },
viewersClassroom: { select: { id: true } },
answersViewersUser: { select: { id: true, username: true } },
answersViewersClassroom: { select: { id: true } },
viewersUser: { select: { id: true, username: true, classrooms: { select: { id: true, name: true } } } },
viewersClassroom: { select: { id: true, name: true, users: { select: { id: true, username: true } } } },
answersViewersUser: { select: { id: true, username: true, classrooms: { select: { id: true, name: true } } } },
answersViewersClassroom: { select: { id: true, name: true, users: { select: { id: true, username: true } } } },
appliers: { select: { id: true, username: true } },
};

Expand Down Expand Up @@ -595,7 +595,7 @@ export const createProtocol = async (req: Request, res: Response) => {
res.status(201).json({ message: 'Protocol created.', data: createdProtocol });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down Expand Up @@ -876,7 +876,7 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
where: { id: { notIn: item.files.map((file) => file.id as number) }, itemId: upsertedItem.id },
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
for (const file of filesToDelete) if (existsSync(file.path)) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
const itemFiles = item.files.map((file, fileIndex) => {
const storedFile = files.find(
Expand Down Expand Up @@ -923,7 +923,7 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
},
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
for (const file of filesToDelete) if (existsSync(file.path)) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
const itemOptionFiles = itemOption.files.map((file, fileIndex) => {
const storedFile = files.find(
Expand Down Expand Up @@ -1045,7 +1045,7 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
res.status(200).json({ message: 'Protocol updated.', data: upsertedProtocol });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
for (const file of files) if (existsSync(file.path)) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down
Loading
Loading