diff --git a/backend/apis/nodejs/src/models/job_application_model.js b/backend/apis/nodejs/src/models/job_application_model.js index 509b629..c07d37a 100644 --- a/backend/apis/nodejs/src/models/job_application_model.js +++ b/backend/apis/nodejs/src/models/job_application_model.js @@ -11,6 +11,7 @@ IN THE SOFTWARE. */ const knex = require('../utils/knex_config'); +const OrganizationAdmin = require('../models/organization_admin_model'); /** * Inserts a new JobApplication. @@ -99,7 +100,43 @@ async function getApplicansByOrganization(organizationId) { description: applicant.description, }, })); +} +async function find(jobApplicationId) { + return await knex('JobApplication') + .where('id', jobApplicationId) + .first(); +} + +async function remove(jobApplicationId) { + return await knex('JobApplication') + .where('id', jobApplicationId) + .del(); +} + +async function setStatus(jobApplicationId, status) { + return await knex('JobApplication') + .where('id', jobApplicationId) + .update('application_status', status); +} + +/** + * Only Organization Administrators can change the status of a JobApplication + * @param {*} jobApplicationId + * @param {*} personId + */ +async function canPersonSetStatus(jobApplicationId, personId) { + const organization = await knex('JobApplication') + .where('JobApplication.id', jobApplicationId) + .join('JobOffer', 'JobOffer.id', 'JobApplication.job_offer_id') + .join('Organization', 'Organization.id', 'JobOffer.organization_id') + .first() + .select('Organization.id'); + if (organization == null) { + return false; + } + const isAdmin = await OrganizationAdmin.isAdmin(personId, organization.id); + return isAdmin; } module.exports = { @@ -107,5 +144,9 @@ module.exports = { userAlreadyApplicated, getMyApplications, getApplicantsByJobOffer, - getApplicansByOrganization + getApplicansByOrganization, + find, + remove, + setStatus, + canPersonSetStatus } \ No newline at end of file diff --git a/backend/apis/nodejs/src/routes/job_application_routes.js b/backend/apis/nodejs/src/routes/job_application_routes.js index e51ae96..6fc14d2 100644 --- a/backend/apis/nodejs/src/routes/job_application_routes.js +++ b/backend/apis/nodejs/src/routes/job_application_routes.js @@ -1,3 +1,5 @@ +// TODO: I don't like the routes. It should be /api/organizations/idOrganization/joboffers/idJobOffer/ +// TODO: Create a validator /* This code is part of Blink licensed under GPLv3 @@ -18,12 +20,9 @@ const express = require('express'); const jwtUtils = require('../utils/jwt_utils'); /** - * POST Request + * **POST** Request * * Inserts a new job application - * @param {*} req - * @param {*} res - * @returns */ async function insert(req, res) { try { @@ -53,12 +52,9 @@ async function insert(req, res) { } /** - * GET Request + * **GET** Request * * Retrieves all the job applications of the logged in user - * @param {*} req - * @param {*} res - * @returns */ async function myApplications(req, res) { try { @@ -73,15 +69,15 @@ async function myApplications(req, res) { } /** - * GET Request. Retrieve all the applicants who applicated to a job offer. + * **GET** Request + * + * Retrieve all the applicants who applicated to a job offer. * Only an organization administrator is allowed to perform this action. - * @param {*} req - * @param {*} res */ async function getApplicantsByJobOffer(req, res) { try { - const isAdmin = await OrganizationAdmin.isAdmin(req.jwt.person_id, jobOffer.organization_id); - if(!isAdmin){ + const isAdmin = await OrganizationAdmin.isAdmin(req.jwt.person_id, req.body.organizationId); + if (!isAdmin) { return res.status(401).json({ error: 'Forbidden' }); @@ -97,16 +93,16 @@ async function getApplicantsByJobOffer(req, res) { } /** - * GET Request. Retrieve all the applicants who applicated to a job offer created + * **GET** Request + * + * Retrieve all the applicants who applicated to a job offer created * by the specific organization. * Only an organization administrator is allowed to perform this action. - * @param {*} req - * @param {*} res */ -async function getApplicantsByOrganization(req, res){ +async function getApplicantsByOrganization(req, res) { try { const isAdmin = await OrganizationAdmin.isAdmin(req.jwt.person_id, req.body.organizationId); - if(!isAdmin){ + if (!isAdmin) { return res.status(401).json({ error: 'Forbidden' }); @@ -121,11 +117,63 @@ async function getApplicantsByOrganization(req, res){ } } +/** + * **DELETE** Request + * + * Removes a job application. Only the applicant is allowed to do that. + */ +async function remove(req, res) { + try { + const jobApplication = await Application.find(req.body.jobApplicationId); + if (jobApplication == null) { + return res.status(404).send(); + } + if (jobApplication.person_id !== req.jwt.person_id) { + return res.status(401).json({ + error: 'Forbidden' + }); + } + await Application.remove(req.body.jobApplicationId); + return res.status(200).send(); + } catch (error) { + console.error(`Error in function ${remove.name}: ${error}`); + res.status(500).json({ + error: 'Internal server error' + }); + } +} + +/** + * **PATCH** Request + * + * Sets a new status to a JobApplication. Only an Organization Administrator + * can perform this action + */ +async function setStatus(req, res) { + try { + const canPersonSetStatus = Application.canPersonSetStatus(req.body.jobApplication, req.jwt.person_id); + if (!canPersonSetStatus) { + return res.status(401).json({ + error: 'Forbidden' + }); + } + await Application.setStatus(req.body.jobApplication, req.body.status); + return res.status(204).send(); + } catch (error) { + console.error(`Error in function ${remove.name}: ${error}`); + res.status(500).json({ + error: 'Internal server error' + }); + } +} + const routes = express.Router(); routes.post('/', jwtUtils.extractToken, insert); routes.get('/myapplications', jwtUtils.extractToken, myApplications); routes.get('/applicantsbyjoboffer', jwtUtils.extractToken, getApplicantsByJobOffer); routes.get('/applicantsbyorganization', jwtUtils.extractToken, getApplicantsByOrganization); +routes.delete('/', jwtUtils.extractToken, remove); +routes.patch('/', jwtUtils.extractToken, setStatus); module.exports = { routes diff --git a/backend/sql/1-create_tables.sql b/backend/sql/1-create_tables.sql index 3002976..8254ca2 100644 --- a/backend/sql/1-create_tables.sql +++ b/backend/sql/1-create_tables.sql @@ -5,6 +5,7 @@ CREATE TYPE ContractType as ENUM ('FULL-TIME','PART-TIME','INTERNSHIP','CONTRACT CREATE TYPE ExperienceType as ENUM ('EDUCATION', 'WORK', 'VOLOUNTEER'); CREATE TYPE SalaryFrequency as ENUM ('HOURLY', 'WEEKLY', 'YEARLY'); CREATE TYPE InfoType AS ENUM ('EMAIL', 'PHONE', 'WEBSITE', 'GIT', 'MASTODON', 'YOUTUBE', 'FACEBOOK', 'INSTAGRAM', 'OTHER'); +CREATE TYPE ApplicationStatus AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); -- Table: Person CREATE TABLE IF NOT EXISTS "Person" ( @@ -80,7 +81,8 @@ CREATE TABLE IF NOT EXISTS "JobOfferTag" ( CREATE TABLE IF NOT EXISTS "JobApplication" ( id SERIAL PRIMARY KEY, person_id INTEGER NOT NULL, - job_offer_id INTEGER NOT NULL, + job_offer_id INTEGER NOT NULL, + application_status ApplicationStatus NOT NULL DEFAULT 'PENDING', CONSTRAINT "PersonFk" FOREIGN KEY (person_id) REFERENCES "Person" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT "JobOfferFk" FOREIGN KEY (job_offer_id) REFERENCES "JobOffer" (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE ); diff --git a/backend/sql/BlinkClassDiagram.png b/backend/sql/BlinkClassDiagram.png index 1bbc911..4231373 100644 Binary files a/backend/sql/BlinkClassDiagram.png and b/backend/sql/BlinkClassDiagram.png differ