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

Feature several tutors in a tutorium #1060

Merged
merged 6 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 9 additions & 7 deletions client/src/components/forms/TutorialForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Theme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import { Role } from 'shared/model/Role';
import { IUser } from 'shared/model/User';
import * as Yup from 'yup';
import { Tutorial } from '../../model/Tutorial';
Expand Down Expand Up @@ -54,7 +53,7 @@ const useStyles = makeStyles((theme: Theme) =>

export interface TutorialFormState {
slot: string;
tutor: string;
tutors: string[];
startDate: string;
endDate: string;
startTime: string;
Expand Down Expand Up @@ -132,7 +131,7 @@ export function getInitialTutorialFormValues(tutorial?: Tutorial): TutorialFormS
if (!tutorial) {
return {
slot: '',
tutor: '',
tutors: [],
startDate,
endDate,
startTime: DateTime.local().toISO() ?? '',
Expand All @@ -146,7 +145,7 @@ export function getInitialTutorialFormValues(tutorial?: Tutorial): TutorialFormS

return {
slot: tutorial.slot,
tutor: tutorial.tutor ? tutorial.tutor.id : '',
tutors: tutorial.tutors.map((t) => t.id),
startDate: sortedDates[0] ? (sortedDates[0].toISODate() ?? '') : startDate,
endDate: sortedDates[sortedDates.length - 1]
? (sortedDates[sortedDates.length - 1].toISODate() ?? '')
Expand Down Expand Up @@ -188,11 +187,14 @@ function TutorialForm({
<FormikTextField name='slot' label='Slot' required />

<FormikSelect
name='tutor'
label='Tutor'
name='tutors'
label='Tutoren'
emptyPlaceholder='Keine Tutoren vorhanden.'
items={tutors.filter((tutor) => tutor.roles.indexOf(Role.TUTOR) > -1)}
items={tutors}
{...userConverterFunctions}
multiple
isItemSelected={(tutor) => values['tutors'].indexOf(tutor.id) > -1}
fullWidth
/>

<div className={classes.twoPickerContainer}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ function FormikTutorialSelect({
name={name}
itemToString={(tutorial) => ({
primary: tutorial.toDisplayStringWithTime(),
secondary: tutorial.tutor ? `Tutor/in: ${getNameOfEntity(tutorial.tutor)}` : undefined,
secondary:
tutorial.tutors.length > 0
? `Tutoren: ${tutorial.tutors.map((tutor) => getNameOfEntity(tutor)).join(', ')}`
: undefined,
})}
itemToValue={(tutorial) => tutorial.id}
{...props}
Expand Down
7 changes: 5 additions & 2 deletions client/src/hooks/dialog-service/DialogService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const useStyles = makeStyles((theme) =>
deleteButton: {
color: theme.palette.red.main,
},
noOverflow: {
overflowY: 'unset',
},
})
);

Expand Down Expand Up @@ -113,7 +116,7 @@ function DialogService({ children }: RequireChildrenProp): JSX.Element {
<Dialog open onClose={handleCloseDialog} fullWidth {...dialog.DialogProps}>
{dialog.title && <DialogTitle>{dialog.title}</DialogTitle>}

<DialogContent>
<DialogContent className={classes.noOverflow}>
{typeof dialog.content === 'string' ? (
<DialogContentText>{dialog.content}</DialogContentText>
) : typeof dialog.content === 'function' ? (
Expand Down Expand Up @@ -266,4 +269,4 @@ function getDialogOutsideContext(): Pick<DialogHelpers, 'show' | 'hide'> {
}

export default DialogService;
export { useDialog, getDialogOutsideContext };
export { getDialogOutsideContext, useDialog };
2 changes: 1 addition & 1 deletion client/src/model/Student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class StudentInTeam implements Modify<IStudentInTeam, Modified> {
getDatesOfAttendances(): DateTime[] {
const dates: DateTime[] = [];

for (const date in this.attendances.keys()) {
for (const date of this.attendances.keys()) {
dates.push(DateTime.fromISO(date));
}

Expand Down
2 changes: 1 addition & 1 deletion client/src/model/Tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface Modified {
export class Tutorial implements Modify<ITutorial, Modified> {
readonly id!: string;
readonly slot!: string;
readonly tutor?: UserInEntity;
readonly tutors!: UserInEntity[];
readonly students!: string[];
readonly teams!: string[];
readonly correctors!: UserInEntity[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
TableRow,
Typography,
} from '@mui/material';
import { DateTime } from 'luxon';
import React, { useMemo } from 'react';
import _ from 'lodash';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { AttendanceState, IAttendance } from 'shared/model/Attendance';
import AttendanceControls from '../../../components/attendance-controls/AttendanceControls';
import { useSettings } from '../../../hooks/useSettings';
Expand Down Expand Up @@ -55,7 +55,11 @@ function AttendanceInformation({
.getDatesOfAttendances()
.map((date) => new AttendanceDate(date, AttendanceDateSource.STUDENT));

return _.unionWith(tutorialDates, studentDates, (a, b) => a.date === b.date).sort(
const uniqueStudentDates = _.differenceWith(studentDates, tutorialDates, (a, b) =>
a.date.equals(b.date)
);

return [...tutorialDates, ...uniqueStudentDates].sort(
(a, b) => a.date.toMillis() - b.date.toMillis()
);
}, [student, tutorialOfStudent]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
import _ from 'lodash';
import { AccountSearch as SearchIcon } from 'mdi-material-ui';
import React, { useEffect, useReducer, useRef } from 'react';
import { useEffect, useReducer, useRef } from 'react';
import { NamedElement } from 'shared/model/Common';
import { getNameOfEntity } from 'shared/util/helpers';
import DateOrIntervalText from '../../../components/DateOrIntervalText';
Expand Down Expand Up @@ -31,7 +31,7 @@ function filterTutors(
tutors: NamedElement[] = []
): NamedElement[] {
return tutors.filter((tutor) => {
if (tutorial?.tutor?.id === tutor.id) {
if (tutorial?.tutors.some((t) => t.id === tutor.id)) {
return false;
}

Expand Down
7 changes: 4 additions & 3 deletions client/src/pages/tutorialmanagement/TutorialManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import { AutoFix as GenerateIcon } from 'mdi-material-ui';
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { HasId } from 'shared/model/Common';
import { ITutorialDTO } from 'shared/model/Tutorial';
Expand Down Expand Up @@ -42,7 +42,7 @@ const useStyles = makeStyles((theme: Theme) =>

function generateCreateTutorialDTO({
slot,
tutor,
tutors,
startTime: startTimeString,
endTime: endTimeString,
correctors,
Expand All @@ -57,10 +57,10 @@ function generateCreateTutorialDTO({

return {
slot,
tutorId: tutor,
dates,
startTime: startTime.toISOTime() ?? 'DATE_NOT_PARSABLE',
endTime: endTime.toISOTime() ?? 'DATE_NOT_PARSABLE',
tutorIds: tutors,
correctorIds: correctors,
};
}
Expand Down Expand Up @@ -190,6 +190,7 @@ function TutorialManagementContent(): JSX.Element {
<TutorialTableRow
tutorial={tutorial}
disableManageTutorialButton={!user.isAdmin()}
tutors={tutorial.tutors.map((t) => getNameOfEntity(t))}
correctors={tutorial.correctors.map((corr) => getNameOfEntity(corr))}
substitutes={[...tutorial.substitutes]
.map(([date, substituteId]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import EntityListItemMenu from '../../../components/list-item-menu/EntityListItemMenu';
import { renderLink } from '../../../components/navigation-rail/components/renderLink';
import PaperTableRow, { PaperTableRowProps } from '../../../components/PaperTableRow';
import { Tutorial } from '../../../model/Tutorial';
import { ROUTES } from '../../../routes/Routing.routes';
import { renderLink } from '../../../components/navigation-rail/components/renderLink';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
Expand All @@ -31,6 +31,7 @@ interface Substitute {

interface Props extends PaperTableRowProps {
tutorial: Tutorial;
tutors: string[];
substitutes: Substitute[];
correctors: string[];
onEditTutorialClicked: (tutorial: Tutorial) => void;
Expand All @@ -40,6 +41,7 @@ interface Props extends PaperTableRowProps {

function TutorialTableRow({
tutorial,
tutors,
substitutes,
correctors,
onEditTutorialClicked,
Expand Down Expand Up @@ -95,13 +97,17 @@ function TutorialTableRow({
>
<TableCell>
<div>
{tutorial.tutor && (
<Chip
key={tutorial.id}
label={`Tutor: ${tutorial.tutor.lastname}, ${tutorial.tutor.firstname}`}
className={classes.tutorChip}
color='primary'
/>
{tutors.length > 0 && (
<div>
{tutors.map((tut) => (
<Chip
key={tut}
label={`Tutor: ${tut}`}
className={classes.tutorChip}
color='primary'
/>
))}
</div>
)}

{correctors.length > 0 && (
Expand All @@ -111,7 +117,7 @@ function TutorialTableRow({
key={cor}
label={`Korrektor: ${cor}`}
className={classes.tutorChip}
size={tutorial.tutor ? 'small' : 'medium'}
size={tutorial.tutors ? 'small' : 'medium'}
/>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-test-docker/config/templates/attendance.pug
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ div(style='display: flex; width: 100%')
span(style='margin-left: auto; float: right') Datum: #{date.toFormat('dd.MM.yyyy')}

div(style='margin-bottom: 16px')
span Tutor: #{tutorName}
span Tutor(s): #{tutorNames}

table
thead
Expand Down
2 changes: 1 addition & 1 deletion server/config/templates/attendance.pug
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ div(style='display: flex; width: 100%')
span(style='margin-left: auto; float: right') Datum: #{date.toFormat('dd.MM.yyyy')}

div(style='margin-bottom: 16px')
span Tutor: #{tutorName}
span Tutor(s): #{tutorNames}

table
thead
Expand Down
16 changes: 4 additions & 12 deletions server/src/database/entities/tutorial.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
Collection,
Entity,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Collection, Entity, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { DateTime, Interval, ToISOTimeOptions } from 'luxon';
import { ITutorialInEntity } from 'shared/model/Common';
import { ITutorial } from 'shared/model/Tutorial';
Expand Down Expand Up @@ -35,8 +27,8 @@ export class Tutorial {
@Property({ type: LuxonTimeType })
endTime: DateTime;

@ManyToOne()
tutor?: User;
@ManyToMany({ entity: () => User, mappedBy: 'tutorials' })
tutors = new Collection<User>(this);

@OneToMany(() => Student, (student) => student.tutorial)
students = new Collection<Student>(this);
Expand Down Expand Up @@ -70,7 +62,7 @@ export class Tutorial {
return {
id: this.id,
slot: this.slot,
tutor: this.tutor?.toInEntity(),
tutors: this.tutors.getItems().map((tutor) => tutor.toInEntity()),
dates: this.dates.map((date) => date.toISODate() ?? 'DATE_NOT_PARSEABLE'),
startTime: this.startTime.toISOTime(dateOptions) ?? 'DATE_NOT_PARSEABLE',
endTime: this.endTime.toISOTime(dateOptions) ?? 'DATE_NOT_PARSEABLE',
Expand Down
4 changes: 3 additions & 1 deletion server/src/database/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export class User {
@Property({ type: EncryptedStringType })
temporaryPassword?: string;

@OneToMany(() => Tutorial, (tutorial: { tutor: any }) => tutorial.tutor)
@ManyToMany(() => Tutorial, (tutorial: { tutors: any }) => tutorial.tutors, {
owner: true,
})
tutorials = new Collection<Tutorial>(this);

@ManyToMany(() => Tutorial, (tutorial: { correctors: any }) => tutorial.correctors, {
Expand Down
2 changes: 1 addition & 1 deletion server/src/guards/created-in-own-tutorial.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ export class CreatedInOwnTutorialGuard extends UseUserFromRequest {
.getRequest<Request>().body;
const tutorial = await this.tutorialService.findById(body.tutorial);

return tutorial.tutor?.id === user.id;
return tutorial.tutors.getItems().some((tutor) => tutor.id === user.id);
}
}
2 changes: 1 addition & 1 deletion server/src/guards/tutorial.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TutorialGuard extends UseMetadata {
const allowSubstitutes = this.isAllowedForSubstitutes(context);
const allowCorrectors = this.isAllowedForCorrectors(context);

if (tutorial.tutor?.id === userId) {
if (tutorial.tutors.getItems().some((tutor) => tutor.id === userId)) {
return true;
}

Expand Down
11 changes: 7 additions & 4 deletions server/src/module/pdf/subservices/PDFGenerator.attendance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,23 @@ export class AttendancePDFGenerator extends PDFGenerator<GeneratorOptions> {
public async generatePDF({ tutorialId, date }: GeneratorOptions): Promise<Buffer> {
const tutorial = await this.tutorialService.findById(tutorialId);

if (!tutorial.tutor) {
if (tutorial.tutors.getItems().length === 0) {
throw new BadRequestException(
'Tutorial which attendance list should be generated does NOT have a tutor assigned.'
);
}

const { tutor, slot: tutorialSlot } = tutorial;
const tutorName = getNameOfEntity(tutor);
const { tutors, slot: tutorialSlot } = tutorial;
const tutorNames = tutors
.getItems()
.map((tutor) => getNameOfEntity(tutor))
.join('; ');
const template = this.templateService.getAttendanceTemplate();
const students = tutorial.getStudents();
const content = template({
date,
students: students.sort(sortByName).map((s) => ({ name: getNameOfEntity(s) })),
tutorName,
tutorNames,
tutorialSlot,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class AttendanceCriteria extends PossiblePercentageCriteria {
}

checkCriteriaStatus({ student }: CriteriaPayload): StatusCheckResponse {
const total = student.getAllAttendances().length;
const total = student.getAllAttendances().filter(({ state }) => state !== undefined).length;
let visitedOrExcused = 0;

student.getAllAttendances().forEach(({ state }) => {
Expand Down
2 changes: 1 addition & 1 deletion server/src/module/template/template.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface ScheinexamStatus {

export interface AttendanceAttributes {
tutorialSlot: string;
tutorName: string;
tutorNames: string;
date: DateTime;
students: { name: string }[];
}
Expand Down
6 changes: 3 additions & 3 deletions server/src/module/tutorial/tutorial.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export class TutorialDTO implements ITutorialDTO {
@IsString()
slot!: string;

@IsOptional()
@IsString()
tutorId?: string;
@IsArray()
@IsString({ each: true })
tutorIds!: string[];

@IsLuxonDateTime()
startTime!: string;
Expand Down
Loading
Loading