From 60ef9c70f2a6f169a8725ad6b0cf55062d0485b6 Mon Sep 17 00:00:00 2001 From: guru-aot Date: Thu, 4 May 2023 15:05:06 -0600 Subject: [PATCH] #1792-Enable application details for public institution user (#1914) Enable application details for public institution user --- .../apps/api/src/app.institutions.module.ts | 4 + ...ntroller.getApplicationDetails.e2e-spec.ts | 136 ++++++++++++++++++ .../application.aest.controller.ts | 2 +- .../application.controller.service.ts | 2 +- .../application.institutions.controller.ts | 58 ++++++++ .../apps/api/src/route-controllers/index.ts | 1 + .../application/application.service.ts | 10 +- .../sidebar/InstitutionApplicationSideBar.vue | 51 +++++++ .../src/constants/routes/RouteConstants.ts | 1 + sources/packages/web/src/router/AESTRoutes.ts | 3 +- .../web/src/router/InstitutionRoutes.ts | 28 ++++ .../web/src/services/ApplicationService.ts | 7 +- .../web/src/services/http/ApplicationApi.ts | 13 +- sources/packages/web/src/types/AppRoutes.ts | 1 + .../src/views/aest/StudentApplicationView.vue | 9 +- .../views/institution/ApplicationDetails.vue | 3 + .../InstitutionStudentApplicationView.vue | 74 ++++++++++ .../InstitutionStudentApplications.vue | 16 +++ 18 files changed, 403 insertions(+), 16 deletions(-) create mode 100644 sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.institutions.controller.getApplicationDetails.e2e-spec.ts create mode 100644 sources/packages/backend/apps/api/src/route-controllers/application/application.institutions.controller.ts create mode 100644 sources/packages/web/src/components/layouts/Institution/sidebar/InstitutionApplicationSideBar.vue create mode 100644 sources/packages/web/src/views/institution/ApplicationDetails.vue create mode 100644 sources/packages/web/src/views/institution/student/InstitutionStudentApplicationView.vue diff --git a/sources/packages/backend/apps/api/src/app.institutions.module.ts b/sources/packages/backend/apps/api/src/app.institutions.module.ts index 35675e4a35..feaf876f29 100644 --- a/sources/packages/backend/apps/api/src/app.institutions.module.ts +++ b/sources/packages/backend/apps/api/src/app.institutions.module.ts @@ -25,6 +25,7 @@ import { EducationProgramOfferingValidationService, } from "./services"; import { + ApplicationControllerService, DesignationAgreementInstitutionsController, DesignationAgreementControllerService, InstitutionInstitutionsController, @@ -64,6 +65,7 @@ import { } from "@sims/services/sfas"; import { UserInstitutionsController } from "./route-controllers/user/user.institutions.controller"; import { UserControllerService } from "./route-controllers/user/user.controller.service"; +import { ApplicationInstitutionsController } from "./route-controllers/application/application.institutions.controller"; @Module({ imports: [AuthModule], @@ -72,6 +74,7 @@ import { UserControllerService } from "./route-controllers/user/user.controller. InstitutionInstitutionsController, InstitutionUserInstitutionsController, InstitutionLocationInstitutionsController, + ApplicationInstitutionsController, ScholasticStandingInstitutionsController, ConfirmationOfEnrollmentInstitutionsController, EducationProgramInstitutionsController, @@ -82,6 +85,7 @@ import { UserControllerService } from "./route-controllers/user/user.controller. OverawardInstitutionsController, ], providers: [ + ApplicationControllerService, WorkflowClientService, FormService, DesignationAgreementService, diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.institutions.controller.getApplicationDetails.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.institutions.controller.getApplicationDetails.e2e-spec.ts new file mode 100644 index 0000000000..7c7635cc03 --- /dev/null +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.institutions.controller.getApplicationDetails.e2e-spec.ts @@ -0,0 +1,136 @@ +import { HttpStatus, INestApplication } from "@nestjs/common"; +import * as request from "supertest"; +import { DataSource } from "typeorm"; +import { + authorizeUserTokenForLocation, + BEARER_AUTH_TYPE, + createTestingAppModule, + getAuthRelatedEntities, + getInstitutionToken, + INSTITUTION_BC_PUBLIC_ERROR_MESSAGE, + INSTITUTION_STUDENT_DATA_ACCESS_ERROR_MESSAGE, + InstitutionTokenTypes, +} from "../../../testHelpers"; +import { + createFakeInstitutionLocation, + saveFakeApplication, +} from "@sims/test-utils"; +import { Institution, InstitutionLocation } from "@sims/sims-db"; + +describe("ApplicationInstitutionsController(e2e)-getApplicationDetails", () => { + let app: INestApplication; + let appDataSource: DataSource; + let collegeF: Institution; + let collegeFLocation: InstitutionLocation; + let collegeCLocation: InstitutionLocation; + + beforeAll(async () => { + const { nestApplication, dataSource } = await createTestingAppModule(); + app = nestApplication; + appDataSource = dataSource; + // College F. + const { institution: collegeF } = await getAuthRelatedEntities( + appDataSource, + InstitutionTokenTypes.CollegeFUser, + ); + collegeFLocation = createFakeInstitutionLocation(collegeF); + await authorizeUserTokenForLocation( + appDataSource, + InstitutionTokenTypes.CollegeFUser, + collegeFLocation, + ); + // College C. + const { institution: collegeC } = await getAuthRelatedEntities( + appDataSource, + InstitutionTokenTypes.CollegeCUser, + ); + collegeCLocation = createFakeInstitutionLocation(collegeC); + await authorizeUserTokenForLocation( + appDataSource, + InstitutionTokenTypes.CollegeCUser, + collegeCLocation, + ); + }); + + it("Should get the student application details when student has a submitted application for the institution.", async () => { + // Arrange + // Create new application. + const savedApplication = await saveFakeApplication(appDataSource, { + institutionLocation: collegeFLocation, + }); + + const student = savedApplication.student; + const endpoint = `/institutions/application/student/${student.id}/application/${savedApplication.id}`; + const institutionUserToken = await getInstitutionToken( + InstitutionTokenTypes.CollegeFUser, + ); + + // Act/Assert + await request(app.getHttpServer()) + .get(endpoint) + .auth(institutionUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.OK) + .expect({ + data: {}, + id: savedApplication.id, + applicationStatus: savedApplication.applicationStatus, + applicationNumber: savedApplication.applicationNumber, + applicationFormName: "SFAA2022-23", + applicationProgramYearID: savedApplication.programYearId, + }); + }); + + it("Should not have access to get the student application details when the student submitted an application to non-public institution.", async () => { + // Arrange + // Create new application. + const savedApplication = await saveFakeApplication(appDataSource, { + institutionLocation: collegeCLocation, + }); + + const student = savedApplication.student; + const endpoint = `/institutions/application/student/${student.id}/application/${savedApplication.id}`; + const institutionUserTokenCUser = await getInstitutionToken( + InstitutionTokenTypes.CollegeCUser, + ); + + // Act/Assert + await request(app.getHttpServer()) + .get(endpoint) + .auth(institutionUserTokenCUser, BEARER_AUTH_TYPE) + .expect(HttpStatus.FORBIDDEN) + .expect({ + statusCode: 403, + message: INSTITUTION_BC_PUBLIC_ERROR_MESSAGE, + error: "Forbidden", + }); + }); + + it("Should not get the student application details when application is submitted for different institution.", async () => { + // Arrange + // Create new application. + const savedApplication = await saveFakeApplication(appDataSource, { + institutionLocation: collegeCLocation, + }); + + const student = savedApplication.student; + const endpoint = `/institutions/application/student/${student.id}/application/${savedApplication.id}`; + const institutionUserToken = await getInstitutionToken( + InstitutionTokenTypes.CollegeFUser, + ); + + // Act/Assert + await request(app.getHttpServer()) + .get(endpoint) + .auth(institutionUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.FORBIDDEN) + .expect({ + statusCode: 403, + message: INSTITUTION_STUDENT_DATA_ACCESS_ERROR_MESSAGE, + error: "Forbidden", + }); + }); + + afterAll(async () => { + await app?.close(); + }); +}); diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts index 67f2acb319..e26396ac3b 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts @@ -52,7 +52,7 @@ export class ApplicationAESTController extends BaseController { await this.applicationControllerService.generateApplicationFormData( application.data, ); - return this.applicationControllerService.transformToApplicationForAESTDTO( + return this.applicationControllerService.transformToApplicationDTO( application, ); } diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts index c2346a13f0..e81de1c009 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts @@ -127,7 +127,7 @@ export class ApplicationControllerService { * @param application * @returns Application DTO */ - async transformToApplicationForAESTDTO( + async transformToApplicationDTO( application: Application, ): Promise { return { diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.institutions.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.institutions.controller.ts new file mode 100644 index 0000000000..f2b17201c5 --- /dev/null +++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.institutions.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common"; +import { ApplicationService } from "../../services"; +import BaseController from "../BaseController"; +import { ApplicationBaseAPIOutDTO } from "./models/application.dto"; +import { + AllowAuthorizedParty, + HasStudentDataAccess, + IsBCPublicInstitution, + UserToken, +} from "../../auth/decorators"; +import { ApiTags } from "@nestjs/swagger"; +import { ClientTypeBaseRoute } from "../../types"; +import { ApplicationControllerService } from "./application.controller.service"; +import { AuthorizedParties, IInstitutionUserToken } from "../../auth"; + +@AllowAuthorizedParty(AuthorizedParties.institution) +@IsBCPublicInstitution() +@Controller("application") +@ApiTags(`${ClientTypeBaseRoute.Institution}-application`) +export class ApplicationInstitutionsController extends BaseController { + constructor( + private readonly applicationService: ApplicationService, + private readonly applicationControllerService: ApplicationControllerService, + ) { + super(); + } + + /** + * API to fetch application details by applicationId. + * This API will be used by institution users. + * @param applicationId for the application. + * @param studentId for the student. + * @returns Application details. + */ + @HasStudentDataAccess("studentId") + @Get("student/:studentId/application/:applicationId") + async getApplication( + @UserToken() userToken: IInstitutionUserToken, + @Param("applicationId", ParseIntPipe) applicationId: number, + @Param("studentId", ParseIntPipe) studentId: number, + ): Promise { + const application = await this.applicationService.getApplicationById( + applicationId, + { + loadDynamicData: true, + studentId: studentId, + institutionId: userToken.authorizations.institutionId, + }, + ); + application.data = + await this.applicationControllerService.generateApplicationFormData( + application.data, + ); + return this.applicationControllerService.transformToApplicationDTO( + application, + ); + } +} diff --git a/sources/packages/backend/apps/api/src/route-controllers/index.ts b/sources/packages/backend/apps/api/src/route-controllers/index.ts index 62a073e02f..59df8ec5f8 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/index.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/index.ts @@ -17,6 +17,7 @@ export * from "./designation-agreement/designation-agreement.controller.service" export * from "./designation-agreement/designation-agreement.aest.controller"; export * from "./application/application.aest.controller"; export * from "./application/application.students.controller"; +export * from "./application/application.institutions.controller"; export * from "./assessment/assessment.controller.service"; export * from "./institution/institution.aest.controller"; export * from "./institution/institution.institutions.controller"; diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts index 9c57b0ebf6..c036069d94 100644 --- a/sources/packages/backend/apps/api/src/services/application/application.service.ts +++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts @@ -513,16 +513,21 @@ export class ApplicationService extends RecordDataModelService { /** * Gets a student application by applicationId. - * Student id can be provided for authorization purposes. + * Student id/ institution id can be provided for authorization purposes. * @param applicationId application id. * @param options object that should contain: * - `loadDynamicData` indicates if the dynamic data(JSONB) should be loaded. * - `studentId` student id. + * - `institutionId` institution id. * @returns student application. */ async getApplicationById( applicationId: number, - options?: { loadDynamicData?: boolean; studentId?: number }, + options?: { + loadDynamicData?: boolean; + studentId?: number; + institutionId?: number; + }, ): Promise { return this.repo.findOne({ select: { @@ -578,6 +583,7 @@ export class ApplicationService extends RecordDataModelService { student: { id: options?.studentId, }, + location: { institution: { id: options?.institutionId } }, }, }); } diff --git a/sources/packages/web/src/components/layouts/Institution/sidebar/InstitutionApplicationSideBar.vue b/sources/packages/web/src/components/layouts/Institution/sidebar/InstitutionApplicationSideBar.vue new file mode 100644 index 0000000000..0dbb5b4dd2 --- /dev/null +++ b/sources/packages/web/src/components/layouts/Institution/sidebar/InstitutionApplicationSideBar.vue @@ -0,0 +1,51 @@ + + + diff --git a/sources/packages/web/src/constants/routes/RouteConstants.ts b/sources/packages/web/src/constants/routes/RouteConstants.ts index e12f92edb7..014fa52e32 100644 --- a/sources/packages/web/src/constants/routes/RouteConstants.ts +++ b/sources/packages/web/src/constants/routes/RouteConstants.ts @@ -72,6 +72,7 @@ export const InstitutionRoutesConst = { STUDENT_PROFILE: Symbol(), STUDENT_DETAILS: Symbol(), STUDENT_APPLICATIONS: Symbol(), + STUDENT_APPLICATION_DETAILS: Symbol(), STUDENT_RESTRICTIONS: Symbol(), STUDENT_FILE_UPLOADS: Symbol(), STUDENT_OVERAWARDS: Symbol(), diff --git a/sources/packages/web/src/router/AESTRoutes.ts b/sources/packages/web/src/router/AESTRoutes.ts index 4a3f88a5e2..62ef92c905 100644 --- a/sources/packages/web/src/router/AESTRoutes.ts +++ b/sources/packages/web/src/router/AESTRoutes.ts @@ -173,6 +173,7 @@ export const aestRoutes: Array = [ }, { path: AppRoutes.ApplicationDetail, + redirect: { name: AESTRoutesConst.APPLICATION_DETAILS }, props: true, components: { default: ApplicationDetails, @@ -183,7 +184,7 @@ export const aestRoutes: Array = [ }, children: [ { - path: "", + path: AppRoutes.ApplicationView, name: AESTRoutesConst.APPLICATION_DETAILS, props: true, component: StudentApplicationView, diff --git a/sources/packages/web/src/router/InstitutionRoutes.ts b/sources/packages/web/src/router/InstitutionRoutes.ts index a635d82867..f940488666 100644 --- a/sources/packages/web/src/router/InstitutionRoutes.ts +++ b/sources/packages/web/src/router/InstitutionRoutes.ts @@ -5,6 +5,7 @@ import InstitutionCreate from "@/views/institution/InstitutionCreate.vue"; import InstitutionUserProfile from "@/views/institution/InstitutionUserProfile.vue"; import AppInstitution from "@/views/institution/AppInstitution.vue"; import ManageLocation from "@/views/institution/ManageLocations.vue"; +import ApplicationDetails from "@/views/institution/ApplicationDetails.vue"; import LocationPrograms from "@/views/institution/locations/programs/LocationPrograms.vue"; import LocationProgramInfoRequestSummary from "@/views/institution/locations/program-info-request/LocationProgramInfoRequestSummary.vue"; import ActiveApplicationsSummary from "@/views/institution/locations/active-applications/LocationActiveApplicationSummary.vue"; @@ -25,6 +26,7 @@ import { ClientIdType } from "@/types/contracts/ConfigContract"; import { AuthStatus, AppRoutes, InstitutionUserTypes } from "@/types"; import ManageInstitutionSideBar from "@/components/layouts/Institution/sidebar/ManageInstitutionSideBar.vue"; import InstitutionHomeSideBar from "@/components/layouts/Institution/sidebar/HomeSideBar.vue"; +import InstitutionApplicationSideBar from "@/components/layouts/Institution/sidebar/InstitutionApplicationSideBar.vue"; import LocationProgramAddEdit from "@/views/institution/locations/programs/LocationProgramAddEdit.vue"; import LocationCOERequest from "@/views/institution/locations/confirmation-of-enrollment/ApplicationDetailsForCOE.vue"; import LocationProgramView from "@/views/institution/locations/programs/LocationProgramView.vue"; @@ -40,6 +42,7 @@ import InstitutionSearchStudents from "@/views/institution/student/InstitutionSe import InstitutionStudentDetails from "@/views/institution/student/InstitutionStudentDetails.vue"; import InstitutionStudentProfile from "@/views/institution/student/InstitutionStudentProfile.vue"; import InstitutionStudentApplications from "@/views/institution/student/InstitutionStudentApplications.vue"; +import InstitutionApplicationView from "@/views/institution/student/InstitutionStudentApplicationView.vue"; import InstitutionStudentRestrictions from "@/views/institution/student/InstitutionStudentRestrictions.vue"; import InstitutionStudentFileUploads from "@/views/institution/student/InstitutionStudentFileUploads.vue"; import InstitutionStudentOverawards from "@/views/institution/student/InstitutionStudentOverawards.vue"; @@ -560,6 +563,31 @@ export const institutionRoutes: Array = [ }, ], }, + { + path: AppRoutes.ApplicationDetail, + redirect: { name: InstitutionRoutesConst.STUDENT_APPLICATION_DETAILS }, + props: true, + components: { + default: ApplicationDetails, + sidebar: InstitutionApplicationSideBar, + }, + meta: { + clientType: ClientIdType.Institution, + institutionUserTypes: [ + InstitutionUserTypes.admin, + InstitutionUserTypes.user, + ], + allowOnlyBCPublic: true, + }, + children: [ + { + path: AppRoutes.ApplicationView, + name: InstitutionRoutesConst.STUDENT_APPLICATION_DETAILS, + props: true, + component: InstitutionApplicationView, + }, + ], + }, ], beforeEnter: (to, _from, next) => { AuthService.shared diff --git a/sources/packages/web/src/services/ApplicationService.ts b/sources/packages/web/src/services/ApplicationService.ts index 1204650fb6..b4e0cab24f 100644 --- a/sources/packages/web/src/services/ApplicationService.ts +++ b/sources/packages/web/src/services/ApplicationService.ts @@ -71,12 +71,17 @@ export class ApplicationService { /** * Get application detail of given application. * @param applicationId for the application. + * @param studentId for the student. * @returns application details. */ async getApplicationDetail( applicationId: number, + studentId?: number, ): Promise { - return ApiClient.Application.getApplicationDetails(applicationId); + return ApiClient.Application.getApplicationDetails( + applicationId, + studentId, + ); } /** diff --git a/sources/packages/web/src/services/http/ApplicationApi.ts b/sources/packages/web/src/services/http/ApplicationApi.ts index 14f896ef76..35e3c226c9 100644 --- a/sources/packages/web/src/services/http/ApplicationApi.ts +++ b/sources/packages/web/src/services/http/ApplicationApi.ts @@ -79,15 +79,18 @@ export class ApplicationApi extends HttpBaseClient { /** * API Client for application detail. - * @param applicationId - * @returns + * @param applicationId for the application. + * @param studentId for the student. + * @returns application details. */ async getApplicationDetails( applicationId: number, + studentId?: number, ): Promise { - return this.getCall( - this.addClientRoot(`application/${applicationId}`), - ); + const url = studentId + ? `application/student/${studentId}/application/${applicationId}` + : `application/${applicationId}`; + return this.getCall(this.addClientRoot(url)); } /** diff --git a/sources/packages/web/src/types/AppRoutes.ts b/sources/packages/web/src/types/AppRoutes.ts index 488d7f942b..2fddc9a30a 100644 --- a/sources/packages/web/src/types/AppRoutes.ts +++ b/sources/packages/web/src/types/AppRoutes.ts @@ -106,4 +106,5 @@ export enum AppRoutes { StudentNotes = "student-notes/:studentId", StudentRestrictions = "student-restrictions/:studentId", Overawards = "overawards", + ApplicationView = "view", } diff --git a/sources/packages/web/src/views/aest/StudentApplicationView.vue b/sources/packages/web/src/views/aest/StudentApplicationView.vue index 785b0e92f5..bb8bca41dd 100644 --- a/sources/packages/web/src/views/aest/StudentApplicationView.vue +++ b/sources/packages/web/src/views/aest/StudentApplicationView.vue @@ -13,11 +13,7 @@

Student Application Details - {{ - applicationDetail.applicationNumber - ? " - " + applicationDetail.applicationNumber - : "" - }} + {{ emptyStringFiller(applicationDetail.applicationNumber) }}

+ + diff --git a/sources/packages/web/src/views/institution/student/InstitutionStudentApplicationView.vue b/sources/packages/web/src/views/institution/student/InstitutionStudentApplicationView.vue new file mode 100644 index 0000000000..89a951e667 --- /dev/null +++ b/sources/packages/web/src/views/institution/student/InstitutionStudentApplicationView.vue @@ -0,0 +1,74 @@ + + diff --git a/sources/packages/web/src/views/institution/student/InstitutionStudentApplications.vue b/sources/packages/web/src/views/institution/student/InstitutionStudentApplications.vue index 5a390580aa..0767727daa 100644 --- a/sources/packages/web/src/views/institution/student/InstitutionStudentApplications.vue +++ b/sources/packages/web/src/views/institution/student/InstitutionStudentApplications.vue @@ -3,6 +3,7 @@ @@ -10,6 +11,8 @@