Passed
Push — master ( d57a01...9aa081 )
by Marcel
08:19
created

BookXhrController::possiblyAbsentStudents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 4
dl 0
loc 4
rs 10
c 3
b 0
f 0
1
<?php
2
3
namespace App\Controller;
4
5
use App\Book\AbsenceSuggestion\SuggestionResolver;
6
use App\Book\Lesson\LessonCancelHelper;
7
use App\Book\Student\AbsenceExcuseResolver;
8
use App\Entity\LessonAttendance;
9
use App\Entity\LessonEntry;
10
use App\Entity\Student as StudentEntity;
11
use App\Entity\StudentAbsence;
12
use App\Entity\TimetableLesson;
13
use App\Entity\Tuition;
14
use App\Markdown\Markdown;
15
use App\Repository\LessonAttendanceRepositoryInterface;
16
use App\Repository\StudentAbsenceRepositoryInterface;
17
use App\Repository\StudentRepositoryInterface;
18
use App\Repository\TeacherRepositoryInterface;
19
use App\Repository\TimetableLessonRepositoryInterface;
20
use App\Request\Book\CancelLessonRequest;
21
use App\Request\Book\UpdateAttendanceRequest;
22
use App\Response\Api\V1\Student;
23
use App\Response\Api\V1\Subject;
24
use App\Response\Api\V1\Teacher;
25
use App\Response\Api\V1\Tuition as TuitionResponse;
26
use App\Response\ViolationList;
27
use App\Section\SectionResolverInterface;
28
use App\Security\Voter\LessonEntryVoter;
29
use App\Settings\BookSettings;
30
use App\Utils\ArrayUtils;
31
use DateTime;
32
use JMS\Serializer\SerializerInterface;
33
use Nelmio\ApiDocBundle\Annotation\Model;
34
use OpenApi\Annotations as OA;
35
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
36
use Symfony\Component\HttpFoundation\Request;
37
use Symfony\Component\HttpFoundation\Response;
38
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
39
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
40
use Symfony\Component\Routing\Annotation\Route;
41
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
42
43
#[Route(path: '/book/xhr')]
44
class BookXhrController extends AbstractController {
45
46
    use DateRequestTrait;
0 ignored issues
show
introduced by
The trait App\Controller\DateRequestTrait requires some properties which are not provided by App\Controller\BookXhrController: $query, $request
Loading history...
47
48
    private function returnJson($data, SerializerInterface $serializer): Response {
49
        $json = $serializer->serialize($data, 'json');
50
        return new Response($json, Response::HTTP_OK, [
51
            'Content-Type' => 'application/json'
52
        ]);
53
    }
54
55
    #[Route(path: '/teachers', name: 'xhr_teachers')]
56
    public function teachers(TeacherRepositoryInterface $teacherRepository, SerializerInterface $serializer): Response {
57
        $teachers = [];
58
59
        foreach($teacherRepository->findAll() as $teacher) {
60
            $teachers[] = Teacher::fromEntity($teacher);
61
        }
62
63
        return $this->returnJson($teachers, $serializer);
64
    }
65
66
    #[Route(path: '/tuition/{uuid}', name: 'xhr_tuition')]
67
    public function tuition(Tuition $tuition, SerializerInterface $serializer): Response {
68
        return $this->returnJson(TuitionResponse::fromEntity($tuition), $serializer);
69
    }
70
71
    private function possiblyAbsentStudents(Tuition $tuition, DateTime $date, int $lesson, SuggestionResolver $suggestionResolver) {
72
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
73
74
        return $suggestionResolver->resolve($tuition, $date, $lesson);
75
    }
76
77
    #[Route(path: '/attendances/{uuid}', name: 'xhr_entry_attendances')]
78
    public function attendances(Request $request, LessonEntry $entry, SerializerInterface $serializer, SectionResolverInterface $sectionResolver): Response {
79
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
80
81
        $filter = $request->query->get('filter', null);
82
        $data = [ ];
83
84
        /** @var LessonAttendance $attendance */
85
        foreach($entry->getAttendances() as $attendance) {
86
            if($filter === null || intval($filter) === $attendance->getType()) {
87
                $data[] = [
88
                    'student' => Student::fromEntity($attendance->getStudent(), $sectionResolver->getCurrentSection()),
89
                    'type' => $attendance->getType()
90
                ];
91
            }
92
        }
93
94
        return $this->returnJson($data, $serializer);
95
    }
96
97
    #[Route(path: '/students', name: 'xhr_students')]
98
    public function students(StudentRepositoryInterface $studentRepository, SectionResolverInterface $sectionResolver, SerializerInterface $serializer): Response {
99
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
100
101
        $students = [ ];
102
        $section = $sectionResolver->getCurrentSection();
103
104
        foreach($studentRepository->findAllBySection($section) as $studentEntity) {
0 ignored issues
show
Bug introduced by
It seems like $section can also be of type null; however, parameter $section of App\Repository\StudentRe...ace::findAllBySection() does only seem to accept App\Entity\Section, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

104
        foreach($studentRepository->findAllBySection(/** @scrutinizer ignore-type */ $section) as $studentEntity) {
Loading history...
105
            $students[] = Student::fromEntity($studentEntity, $section);
106
        }
107
108
        return $this->returnJson($students, $serializer);
109
    }
110
111
    #[Route('/absence_note/{student}/{lesson}', name: 'xhr_student_absences')]
112
    #[ParamConverter('student', options: [ 'mapping' => ['student' => 'uuid']])]
113
    #[ParamConverter('lesson', options: [ 'mapping' => ['lesson' => 'uuid']])]
114
    public function absenceNote(StudentEntity $student, TimetableLesson $lesson, StudentAbsenceRepositoryInterface $absenceRepository, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator, Markdown $markdown): Response {
115
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
116
117
        $absences = [ ];
118
        for($lessonNumber = $lesson->getLessonStart(); $lessonNumber <= $lesson->getLessonEnd(); $lessonNumber++) {
119
            $absences = array_merge(
120
                $absences,
121
                $absenceRepository->findByStudents([$student], null, $lesson->getDate(), $lessonNumber)
122
            );
123
        }
124
125
        $absences = ArrayUtils::unique($absences);
126
        $json = array_map(fn(StudentAbsence $absence) => [
127
            'uuid' => $absence->getUuid()->toString(),
128
            'type' => $absence->getType()->getName(),
129
            'from' => [
130
                'date' => $absence->getFrom()->getDate()->format('Y-m-d'),
131
                'lesson' => $absence->getFrom()->getLesson()
132
            ],
133
            'until' => [
134
                'date' => $absence->getUntil()->getDate()->format('Y-m-d'),
135
                'lesson' => $absence->getUntil()->getLesson()
136
            ],
137
            'message' => $absence->getMessage(),
138
            'html' => $markdown->convertToHtml($absence->getMessage()),
0 ignored issues
show
Bug introduced by
It seems like $absence->getMessage() can also be of type null; however, parameter $markdown of App\Markdown\Markdown::convertToHtml() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

138
            'html' => $markdown->convertToHtml(/** @scrutinizer ignore-type */ $absence->getMessage()),
Loading history...
139
            'url' => $urlGenerator->generate('show_student_absence', [ 'uuid' => $absence->getUuid()->toString() ])
140
        ], $absences);
141
142
        return $this->returnJson($json, $serializer);
143
    }
144
145
    /**
146
     * @OA\Get()
147
     * @OA\Parameter(
148
     *     name="lesson",
149
     *     in="query",
150
     *     description="UUID of the lesson"
151
     * )
152
     * @OA\Parameter(
153
     *     name="start",
154
     *     in="query",
155
     *     description="Start lesson number"
156
     * )
157
     * @OA\Parameter(
158
     *     name="end",
159
     *     in="path",
160
     *     description="End lesson number"
161
     * )
162
     */
163
    #[Route(path: '/entry', name: 'xhr_lesson_entry', methods: ['GET'])]
164
    public function entry(Request $request, TimetableLessonRepositoryInterface $lessonRepository, SuggestionResolver $suggestionResolver,
165
                          SerializerInterface $serializer, AbsenceExcuseResolver $excuseResolver, BookSettings $settings): Response {
166
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
167
168
        $lesson = $lessonRepository->findOneByUuid($request->query->get('lesson'));
169
        if($lesson === null) {
170
            throw new NotFoundHttpException('Lesson not found.');
171
        }
172
173
        $start = $request->query->getInt('start');
174
        if(!is_numeric($start)) {
0 ignored issues
show
introduced by
The condition is_numeric($start) is always true.
Loading history...
175
            throw new BadRequestHttpException('Start and end must be numeric values.');
176
        }
177
178
        if($start < $lesson->getLessonStart() || $start > $lesson->getLessonEnd()) {
179
            throw new BadRequestHttpException('Start must be inside lesson boundaries.');
180
        }
181
182
        $entry = null;
183
184
        /** @var LessonEntry $lessonEntry */
185
        foreach($lesson->getEntries() as $lessonEntry) {
186
            if($lessonEntry->getLessonStart() <= (int)$start && (int)$start <= $lessonEntry->getLessonEnd()) {
187
                $entry = $lessonEntry;
188
                break;
189
            }
190
        }
191
192
        $entryJson = null;
193
194
        if($entry !== null) {
0 ignored issues
show
introduced by
The condition $entry !== null is always false.
Loading history...
195
            $attendances = [ ];
196
            /** @var LessonAttendance $attendance */
197
            foreach($entry->getAttendances() as $attendance) {
198
                $excuseInfo = $excuseResolver->resolve($attendance->getStudent(), [ $entry->getTuition() ]);
199
                $excuses = $excuseInfo->getExcuseCollectionForLesson($attendance);
200
201
                $attendances[] = [
202
                    'student' => Student::fromEntity($attendance->getStudent()),
203
                    'minutes' => $attendance->getLateMinutes(),
204
                    'lessons' => $attendance->getAbsentLessons(),
205
                    'comment' => $attendance->getComment(),
206
                    'excuse_status' => $attendance->getExcuseStatus(),
207
                    'has_excuses' => $excuses->count() > 0,
208
                    'type' => $attendance->getType()
209
                ];
210
            }
211
212
            $entryJson = [
213
                'uuid' => $entry->getUuid()->toString(),
214
                'start' => $entry->getLessonStart(),
215
                'end' => $entry->getLessonEnd(),
216
                'subject' => Subject::fromEntity($entry->getSubject()),
217
                'replacement_subject' => $entry->getReplacementSubject(),
218
                'teacher' => Teacher::fromEntity($entry->getTeacher()),
219
                'replacement_teacher' => Teacher::fromEntity($entry->getReplacementTeacher()),
220
                'topic' => $entry->getTopic(),
221
                'exercises' => $entry->getExercises(),
222
                'comment' => $entry->getComment(),
223
                'is_cancelled' => $entry->isCancelled(),
224
                'cancel_reason' => $entry->getCancelReason(),
225
                'attendances' => $attendances
226
            ];
227
        }
228
229
        $students = [ ];
230
        foreach($lesson->getTuition()->getStudyGroup()->getMemberships() as $membership) {
231
            if(in_array($membership->getStudent()->getStatus(), $settings->getExcludeStudentsStatus())) {
232
                // skip student
233
                continue;
234
            }
235
236
            $students[] = Student::fromEntity($membership->getStudent());
237
        }
238
239
        $response = [
240
            'lesson' => [
241
                'uuid' => $lesson->getUuid()->toString(),
242
                'date' => $lesson->getDate()->format('Y-m-d'),
243
                'lesson_start' => $lesson->getLessonStart(),
244
                'lesson_end' => $lesson->getLessonEnd(),
245
                'tuition' => TuitionResponse::fromEntity($lesson->getTuition())
246
            ],
247
            'absences' => $this->possiblyAbsentStudents($lesson->getTuition(), $lesson->getDate(), $start, $suggestionResolver),
0 ignored issues
show
Bug introduced by
It seems like $lesson->getTuition() can also be of type null; however, parameter $tuition of App\Controller\BookXhrCo...ossiblyAbsentStudents() does only seem to accept App\Entity\Tuition, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
            'absences' => $this->possiblyAbsentStudents(/** @scrutinizer ignore-type */ $lesson->getTuition(), $lesson->getDate(), $start, $suggestionResolver),
Loading history...
Bug introduced by
It seems like $lesson->getDate() can also be of type null; however, parameter $date of App\Controller\BookXhrCo...ossiblyAbsentStudents() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
            'absences' => $this->possiblyAbsentStudents($lesson->getTuition(), /** @scrutinizer ignore-type */ $lesson->getDate(), $start, $suggestionResolver),
Loading history...
248
            'entry' => $entryJson,
249
            'students' => $students,
250
            'has_other_entries' => count($lesson->getEntries()) > 0
251
        ];
252
253
        return $this->returnJson($response, $serializer);
254
    }
255
256
    /**
257
     * @OA\Post()
258
     * @OA\Parameter(
259
     *     name="payload",
260
     *     in="body",
261
     *     @Model(type=CancelLessonRequest::class)
262
     * )
263
     * @OA\Response(
264
     *     response="201",
265
     *     description="Lessons are cancelled successfully. Empty content."
266
     * )
267
     * @OA\Response(
268
     *     response="403",
269
     *     description="Bad request.",
270
     *     @Model(type=ViolationList::class)
271
     * )
272
     */
273
    #[Route(path: '/cancel/{uuid}', name: 'xhr_cancel_lesson', methods: ['POST'])]
274
    public function cancelLesson(TimetableLesson $lesson, CancelLessonRequest $request, LessonCancelHelper $lessonCancelHelper): Response {
275
        $this->denyAccessUnlessGranted(LessonEntryVoter::New);
276
        $reason = $request->getReason();
277
        $lessonCancelHelper->cancelLesson($lesson, $reason);
0 ignored issues
show
Bug introduced by
It seems like $reason can also be of type null; however, parameter $reason of App\Book\Lesson\LessonCancelHelper::cancelLesson() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
        $lessonCancelHelper->cancelLesson($lesson, /** @scrutinizer ignore-type */ $reason);
Loading history...
278
279
        return new Response('', Response::HTTP_CREATED, [
280
            'Content-Type' => 'application/json'
281
        ]);
282
    }
283
284
    /**
285
     * @OA\Put()
286
     * @OA\Parameter(
287
     *     name="payload",
288
     *     in="body",
289
     *     @Model(type=UpdateAttendanceRequest::class)
290
     * )
291
     * @OA\Response(
292
     *     response="200",
293
     *     description="Attendance successfully updated"
294
     * )
295
     * @OA\Response(
296
     *     response="403",
297
     *     description="Bad request.",
298
     *     @Model(type=ViolationList::class)
299
     * )
300
     */
301
    #[Route(path: '/attendance/{uuid}', name: 'xhr_update_attendance', methods: ['PUT'])]
302
    public function updateAttendance(LessonAttendance $attendance, UpdateAttendanceRequest $request, LessonAttendanceRepositoryInterface $repository): Response {
303
        $this->denyAccessUnlessGranted(LessonEntryVoter::Edit, $attendance->getEntry());
304
305
        $attendance->setAbsentLessons($request->getAbsentLessons());
306
        $attendance->setLateMinutes($request->getLateMinutes());
307
        $attendance->setExcuseStatus($request->getExcuseStatus());
308
        $attendance->setType($request->getType());
309
        $attendance->setComment($request->getComment());
310
311
        $repository->persist($attendance);
312
313
        return new Response('', Response::HTTP_OK, [
314
            'Content-Type' => 'application/json'
315
        ]);
316
    }
317
318
    #[Route(path: '/font/regular', name: 'xhr_font_regular')]
319
    public function regularFont(BookSettings $bookSettings) {
320
        $font = $bookSettings->getRegularFont();
321
322
        if(empty($font)) {
323
            throw new NotFoundHttpException();
324
        }
325
326
        return new Response($font);
327
    }
328
329
    #[Route(path: '/font/bold', name: 'xhr_font_bold')]
330
    public function boldFont(BookSettings $bookSettings) {
331
        $font = $bookSettings->getBoldFont();
332
333
        if(empty($font)) {
334
            throw new NotFoundHttpException();
335
        }
336
337
        return new Response($font);
338
    }
339
}