diff --git a/src/app/events/services/events-state.service.spec.ts b/src/app/events/services/events-state.service.spec.ts index 6e7c68e08..fad4e826b 100644 --- a/src/app/events/services/events-state.service.spec.ts +++ b/src/app/events/services/events-state.service.spec.ts @@ -4,6 +4,7 @@ import * as t from "io-ts/lib/index"; import { Course } from "src/app/shared/models/course.model"; import { Event } from "src/app/shared/models/event.model"; import { StudyClass } from "src/app/shared/models/study-class.model"; +import { StorageService } from "src/app/shared/services/storage.service"; import { buildCourse, buildFinalGrading, @@ -30,7 +31,28 @@ describe("EventsStateService", () => { let assessmentEntries: EventEntry[]; beforeEach(() => { - TestBed.configureTestingModule(buildTestModuleMetadata()); + TestBed.configureTestingModule( + buildTestModuleMetadata({ + providers: [ + { + provide: StorageService, + useValue: { + getPayload() { + return { + culture_info: "de_CH", + fullname: "Jane Doe", + id_person: "123", + holder_id: "456", + instance_id: "678", + roles: "", + substitution_id: undefined, + }; + }, + }, + }, + ], + }), + ); httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(EventsStateService); @@ -170,11 +192,24 @@ describe("EventsStateService", () => { }, ]; - studyCourses = [{ Id: 10, Designation: "Zoologie", StudentCount: 42 }]; + studyCourses = [ + { + Id: 10, + Designation: "Zentraler Gymnasialer Bildungsgang", + Leadership: "Jane Doe", + StudentCount: 42, + }, + { + Id: 20, + Designation: "Berufsmaturität", + Leadership: "John Doe", // Other leader (study course is ignored) + StudentCount: 10, + }, + ]; studyCoursesEntries = [ { id: 10, - designation: "Zoologie", + designation: "Zentraler Gymnasialer Bildungsgang", studentCount: 42, detailLink: "link-to-event-detail-module/10", state: null, diff --git a/src/app/events/services/events-state.service.ts b/src/app/events/services/events-state.service.ts index 9d83489b1..a29f1b65b 100644 --- a/src/app/events/services/events-state.service.ts +++ b/src/app/events/services/events-state.service.ts @@ -17,6 +17,7 @@ import { StudyClass } from "src/app/shared/models/study-class.model"; import { CoursesRestService } from "src/app/shared/services/courses-rest.service"; import { EventsRestService } from "src/app/shared/services/events-rest.service"; import { LoadingService } from "src/app/shared/services/loading-service"; +import { StorageService } from "src/app/shared/services/storage.service"; import { StudyClassesRestService } from "src/app/shared/services/study-classes-rest.service"; import { spread } from "src/app/shared/utils/function"; import { hasRole } from "src/app/shared/utils/roles"; @@ -26,6 +27,7 @@ import { getCourseDesignation, getEventState, isRated, + isStudyCourseLeader, } from "../utils/events"; export enum EventState { @@ -96,6 +98,7 @@ export class EventsStateService { private eventsRestService: EventsRestService, private studyClassRestService: StudyClassesRestService, private loadingService: LoadingService, + private storageService: StorageService, private translate: TranslateService, @Inject(SETTINGS) private settings: Settings, ) {} @@ -209,13 +212,16 @@ export class EventsStateService { private createFromStudyCourses( studyCourses: ReadonlyArray, ): ReadonlyArray { - return studyCourses.map((studyCourse) => ({ - id: studyCourse.Id, - designation: studyCourse.Designation, - detailLink: this.buildLink(studyCourse.Id, "eventdetail"), - studentCount: studyCourse.StudentCount, - state: null, - })); + const tokenPayload = this.storageService.getPayload(); + return studyCourses + .filter((studyCourse) => isStudyCourseLeader(tokenPayload, studyCourse)) // The user sees only the study courses he/she is leader of + .map((studyCourse) => ({ + id: studyCourse.Id, + designation: studyCourse.Designation, + detailLink: this.buildLink(studyCourse.Id, "eventdetail"), + studentCount: studyCourse.StudentCount, + state: null, + })); } private createFromAssessments( diff --git a/src/app/events/utils/events.spec.ts b/src/app/events/utils/events.spec.ts index 2c6a256d5..8efb871e2 100644 --- a/src/app/events/utils/events.spec.ts +++ b/src/app/events/utils/events.spec.ts @@ -1,10 +1,21 @@ import { EvaluationStatusRef } from "src/app/shared/models/course.model"; -import { buildCourse, buildFinalGrading } from "../../../spec-builders"; +import { Event } from "src/app/shared/models/event.model"; +import { TokenPayload } from "src/app/shared/models/token-payload.model"; +import { + buildCourse, + buildEvent, + buildFinalGrading, +} from "../../../spec-builders"; import { EventState } from "../services/events-state.service"; -import { canSetFinalGrade, getEventState, isRated } from "./events"; - -describe("Course utils", () => { - describe("Get course state", () => { +import { + canSetFinalGrade, + getEventState, + isRated, + isStudyCourseLeader, +} from "./events"; + +describe("Event/course utility functions", () => { + describe("getEventState", () => { beforeEach(() => { jasmine.clock().install(); jasmine.clock().mockDate(new Date(2022, 1, 3)); @@ -14,8 +25,7 @@ describe("Course utils", () => { jasmine.clock().uninstall(); }); - it("should get no state", () => { - // given + it("returns null for course without state", () => { const evaluationStatusRef = { HasEvaluationStarted: false, EvaluationUntil: null, @@ -31,12 +41,10 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(getEventState(course)).toEqual(null); }); - it("should get state add-tests", () => { - // given + it("returns 'add-tests' for course in this state", () => { const evaluationStatusRef = { HasEvaluationStarted: false, EvaluationUntil: null, @@ -52,14 +60,12 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(getEventState(course)).toEqual({ value: EventState.Tests, }); }); - it("should get state rating-until", () => { - // given + it("returns 'rating-until' for course in this state", () => { const evaluationStatusRef = { HasEvaluationStarted: true, EvaluationUntil: new Date(2022, 2, 3), @@ -74,14 +80,12 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(getEventState(course)).toEqual({ value: EventState.RatingUntil, }); }); - it("should get state intermediate-rating", () => { - // given + it("returns 'intermediate-rating' for course in this state", () => { const evaluationStatusRef = { HasEvaluationStarted: true, EvaluationUntil: null, @@ -98,16 +102,14 @@ describe("Course utils", () => { 10300, ); - // then expect(getEventState(course)).toEqual({ value: EventState.IntermediateRating, }); }); }); - describe("Course has final grading enabled", () => { - it("should return false when HasEvaluationStarted is false and EvaluationUntil is null", () => { - // given + describe("canSetFinalGrade", () => { + it("returns false when HasEvaluationStarted is false and EvaluationUntil is null", () => { const evaluationStatusRef = { HasEvaluationStarted: false, EvaluationUntil: null, @@ -123,12 +125,10 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(false); }); - it("should return false when HasEvaluationStarted is false and EvaluationUntil is undefined", () => { - // given + it("returns false when HasEvaluationStarted is false and EvaluationUntil is undefined", () => { const evaluationStatusRef = { HasEvaluationStarted: false, EvaluationUntil: undefined, @@ -144,12 +144,10 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(false); }); - it("should return true when evaluation has started and EvaluationUntil is undefined", () => { - // given + it("returns true when evaluation has started and EvaluationUntil is undefined", () => { const evaluationStatusRef = { HasEvaluationStarted: true, EvaluationUntil: undefined, @@ -165,12 +163,10 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(true); }); - it("should return true when evaluation has started and EvaluationUntil is null", () => { - // given + it("returns true when evaluation has started and EvaluationUntil is null", () => { const evaluationStatusRef = { HasEvaluationStarted: true, EvaluationUntil: null, @@ -186,12 +182,10 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(true); }); - it("should return false when evaluation has started and EvaluationUntil is in the past", () => { - // given + it("returns false when evaluation has started and EvaluationUntil is in the past", () => { jasmine.clock().install(); jasmine.clock().mockDate(new Date(2022, 1, 8)); @@ -212,13 +206,11 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(false); jasmine.clock().uninstall(); }); - it("should return true when evaluation has started and EvaluationUntil is in the future", () => { - // given + it("returns true when evaluation has started and EvaluationUntil is in the future", () => { jasmine.clock().install(); jasmine.clock().mockDate(new Date(2022, 1, 1)); const futureDate = new Date(2022, 6, 1); @@ -238,16 +230,14 @@ describe("Course utils", () => { evaluationStatusRef, ); - // then expect(canSetFinalGrade(course)).toEqual(true); jasmine.clock().uninstall(); }); }); - describe("is course rated", () => { - it("should return true if review of evaluation has started and final grades are set", () => { - // given + describe("isRated", () => { + it("returns true if review of evaluation has started and final grades are set", () => { const evaluationStatusRef = { HasReviewOfEvaluationStarted: true, } as unknown as EvaluationStatusRef; @@ -259,12 +249,10 @@ describe("Course utils", () => { ); course.FinalGrades = [buildFinalGrading(3)]; - // then expect(isRated(course)).toBeTrue(); }); - it("should return false if final grades are null", () => { - // given + it("returns false if final grades are null", () => { const evaluationStatusRef = { HasReviewOfEvaluationStarted: true, } as unknown as EvaluationStatusRef; @@ -276,12 +264,10 @@ describe("Course utils", () => { ); course.FinalGrades = null; - // then expect(isRated(course)).toBeFalse(); }); - it("should return false if final grades are emtpy", () => { - // given + it("returns false if final grades are emtpy", () => { const evaluationStatusRef = { HasReviewOfEvaluationStarted: true, } as unknown as EvaluationStatusRef; @@ -293,12 +279,10 @@ describe("Course utils", () => { ); course.FinalGrades = []; - // then expect(isRated(course)).toBeFalse(); }); - it("should return false if review of evaluation has not started", () => { - // given + it("returns false if review of evaluation has not started", () => { const evaluationStatusRef = { HasReviewOfEvaluationStarted: false, } as unknown as EvaluationStatusRef; @@ -310,8 +294,59 @@ describe("Course utils", () => { ); course.FinalGrades = null; - // then expect(isRated(course)).toBeFalse(); }); }); + + describe("isStudyCourseLeader", () => { + let event: Event; + let tokenPayload: TokenPayload; + + beforeEach(() => { + event = buildEvent(1234, "Gymnasialer Bildungsgang"); + tokenPayload = { + culture_info: "de_CH", + fullname: "Jane Doe", + id_person: "123", + holder_id: "456", + instance_id: "678", + roles: "", + substitution_id: undefined, + }; + }); + + describe("study course with single leadership", () => { + beforeEach(() => { + event.Leadership = "Jane Doe"; + }); + + it("returns false if token payload is unavailable", () => { + expect(isStudyCourseLeader(null, event)).toBeFalse(); + }); + + it("returns true if user is leader", () => { + expect(isStudyCourseLeader(tokenPayload, event)).toBeTrue(); + }); + + it("returns false if user is not leader", () => { + tokenPayload.fullname = "Jeanne Doe"; + expect(isStudyCourseLeader(tokenPayload, event)).toBeFalse(); + }); + }); + + describe("study course with multiple leaderships", () => { + beforeEach(() => { + event.Leadership = "John Doe, Jane Doe"; + }); + + it("returns true if user is leader", () => { + expect(isStudyCourseLeader(tokenPayload, event)).toBeTrue(); + }); + + it("returns false if user is not leader", () => { + tokenPayload.fullname = "Jeanne Doe"; + expect(isStudyCourseLeader(tokenPayload, event)).toBeFalse(); + }); + }); + }); }); diff --git a/src/app/events/utils/events.ts b/src/app/events/utils/events.ts index 4ce34a99f..07a8f5765 100644 --- a/src/app/events/utils/events.ts +++ b/src/app/events/utils/events.ts @@ -1,3 +1,5 @@ +import { Event } from "src/app/shared/models/event.model"; +import { TokenPayload } from "src/app/shared/models/token-payload.model"; import { Course } from "../../shared/models/course.model"; import { EventState } from "../services/events-state.service"; @@ -94,3 +96,22 @@ export function getCourseDesignation(course: Course): string { return classes ? course.Designation + ", " + classes : course.Designation; } + +/** + * Returns whether the user with the given token payload is leader of the given + * study course. + */ +export function isStudyCourseLeader( + tokenPayload: Option, + studyCourse: Event, +): boolean { + if (!tokenPayload) return false; + + // As a workaround (since the API does not provide the ID of the leadership + // and the `/EventLeaderships` are not accessible with the Tutoring token), we + // compare the user's fullname with the event's `Leadership` field containing + // the names of all leaders for now. + return (studyCourse.Leadership ?? "") + .split(",") + .some((leader) => leader.trim() === tokenPayload.fullname); +} diff --git a/src/app/shared/models/event.model.ts b/src/app/shared/models/event.model.ts index 82bb84855..633cf7cd4 100644 --- a/src/app/shared/models/event.model.ts +++ b/src/app/shared/models/event.model.ts @@ -1,4 +1,5 @@ import * as t from "io-ts"; +import { Maybe } from "./common-types"; const Event = t.type({ Id: t.number, @@ -24,7 +25,7 @@ const Event = t.type({ // HasQueue: t.boolean, // HighPrice: t.number, // LanguageOfInstruction: null, - // Leadership: t.string, + Leadership: Maybe(t.string), // Location: null, // MaxParticipants: t.number, // MinParticipants: t.number, diff --git a/src/spec-builders.ts b/src/spec-builders.ts index 311427028..e922c7d5f 100644 --- a/src/spec-builders.ts +++ b/src/spec-builders.ts @@ -13,6 +13,7 @@ import { FinalGrading, Grading, } from "./app/shared/models/course.model"; +import { Event } from "./app/shared/models/event.model"; import { Grade } from "./app/shared/models/grading-scale.model"; import { JobTrainer } from "./app/shared/models/job-trainer.model"; import { LegalRepresentative } from "./app/shared/models/legal-representative.model"; @@ -532,6 +533,56 @@ export function buildStudyClass(id: number, designation?: string): StudyClass { }; } +export function buildEvent(id: number, designation?: string): Event { + return { + Id: id, + // AreaOfEducation: t.string, + // AreaOfEducationId: t.number, + // EventCategory: t.string, + // EventCategoryId: t.number, + // EventLevel: t.string, + // EventLevelId: t.number, + // EventType: t.string, + // EventTypeId: t.number, + // Host: t.string, + // HostId: t.string, + // Status: t.string, + // StatusId: t.number, + // AllowSubscriptionByStatus: t.boolean, + // AllowSubscriptionInternetByStatus: t.boolean, + // DateFrom: null, + // DateTo: null, + Designation: designation || "Französisch-S2", + // Duration: null, + // FreeSeats: null, + // HasQueue: t.boolean, + // HighPrice: t.number, + // LanguageOfInstruction: null, + Leadership: null, + // Location: null, + // MaxParticipants: t.number, + // MinParticipants: t.number, + // Number: t.string, + // Price: t.number, + // StatusDate: null, + // SubscriptionDateFrom: null, + // SubscriptionDateTo: null, + // SubscriptionTimeFrom: null, + // SubscriptionTimeTo: null, + // TimeFrom: null, + // TimeTo: null, + // TypeOfSubscription: t.number, + // Weekday: null, + // IdObject: t.number, + // StatusText: null, + // Management: null, + // GradingScaleId: null, + // DateString: t.string, + StudentCount: 10, + // HRef: t.string, + }; +} + export function buildCourse( id: number, designation?: string,