From 0278beb9eb10e3566b3c38bf1dc1f8d4e97049c8 Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:16:00 +0000 Subject: [PATCH 1/8] Add similar course table --- module/Education/src/Model/Course.php | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/module/Education/src/Model/Course.php b/module/Education/src/Model/Course.php index b178c81c03..3305c6a1b4 100644 --- a/module/Education/src/Model/Course.php +++ b/module/Education/src/Model/Course.php @@ -9,6 +9,10 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\InverseJoinColumn; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\JoinTable; +use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OrderBy; use Laminas\Permissions\Acl\Resource\ResourceInterface; @@ -44,9 +48,42 @@ class Course implements ResourceInterface #[OrderBy(value: ['date' => 'ASC'])] protected Collection $documents; + /** + * Courses that say they are similar to this course + * + * @var Collection + */ + #[ManyToMany( + targetEntity: self::class, + mappedBy: 'similarCoursesTo', + )] + protected Collection $similarCoursesFrom; + + /** + * Courses similar to this course + * + * @var Collection + */ + #[JoinTable(name: 'SimilarCourse')] + #[JoinColumn( + name: 'course_code', + referencedColumnName: 'code', + )] + #[InverseJoinColumn( + name: 'similar_course_code', + referencedColumnName: 'code', + )] + #[ManyToMany( + targetEntity: self::class, + inversedBy: 'similarCoursesFrom', + )] + private Collection $similarCoursesTo; + public function __construct() { $this->documents = new ArrayCollection(); + $this->similarCoursesFrom = new ArrayCollection(); + $this->similarCoursesTo = new ArrayCollection(); } /** From 7a63046eb4cee7fdd923e9ea437f92577a37ae66 Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:28:19 +0000 Subject: [PATCH 2/8] Rename exam to course service to clearify usage --- .../src/Controller/AdminController.php | 50 +++++++++---------- .../src/Controller/EducationController.php | 10 ++-- .../Factory/AdminControllerFactory.php | 2 +- .../Factory/EducationControllerFactory.php | 2 +- module/Education/src/Module.php | 8 +-- .../src/Service/{Exam.php => Course.php} | 4 +- module/Education/src/View/Helper/ExamUrl.php | 14 +++--- 7 files changed, 45 insertions(+), 45 deletions(-) rename module/Education/src/Service/{Exam.php => Course.php} (99%) diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index 3eb47380d4..6fe72e816c 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -8,7 +8,7 @@ use Education\Model\Exam as ExamModel; use Education\Model\Summary as SummaryModel; use Education\Service\AclService; -use Education\Service\Exam as ExamService; +use Education\Service\Course as CourseService; use Laminas\Http\Request; use Laminas\Http\Response; use Laminas\Mvc\Controller\AbstractActionController; @@ -29,7 +29,7 @@ class AdminController extends AbstractActionController public function __construct( private readonly AclService $aclService, private readonly Translator $translator, - private readonly ExamService $examService, + private readonly CourseService $courseService, private readonly array $educationTempConfig, ) { } @@ -51,7 +51,7 @@ public function courseAction(): ViewModel throw new NotAllowedException($this->translator->translate('You are not allowed to administer courses')); } - return new ViewModel(['courses' => $this->examService->getAllCourses()]); + return new ViewModel(['courses' => $this->courseService->getAllCourses()]); } public function addCourseAction(): Response|ViewModel @@ -62,7 +62,7 @@ public function addCourseAction(): Response|ViewModel /** @var Request $request */ $request = $this->getRequest(); - $form = $this->examService->getCourseForm(); + $form = $this->courseService->getCourseForm(); if ($request->isPost()) { $form->setData($request->getPost()->toArray()); @@ -71,7 +71,7 @@ public function addCourseAction(): Response|ViewModel /** @var CourseModel $course */ $course = $form->getObject(); - if ($this->examService->saveCourse($course)) { + if ($this->courseService->saveCourse($course)) { $this->flashMessenger()->addSuccessMessage( $this->translator->translate('Successfully added course!'), ); @@ -99,7 +99,7 @@ public function editCourseAction(): ViewModel } $courseId = $this->params()->fromRoute('course'); - $course = $this->examService->getCourse($courseId); + $course = $this->courseService->getCourse($courseId); if (null === $course) { return $this->notFoundAction(); @@ -107,7 +107,7 @@ public function editCourseAction(): ViewModel /** @var Request $request */ $request = $this->getRequest(); - $form = $this->examService->getCourseForm($course); + $form = $this->courseService->getCourseForm($course); if ($request->isPost()) { $form->setData($request->getPost()->toArray()); @@ -116,7 +116,7 @@ public function editCourseAction(): ViewModel /** @var CourseModel $course */ $course = $form->getObject(); - if ($this->examService->saveCourse($course)) { + if ($this->courseService->saveCourse($course)) { $this->flashMessenger()->addSuccessMessage( $this->translator->translate('Successfully updated course information!'), ); @@ -146,8 +146,8 @@ public function deleteCourseAction(): Response|ViewModel if ($request->getPost()) { $courseId = $this->params()->fromRoute('course'); - if (null !== ($course = $this->examService->getCourse($courseId))) { - $this->examService->deleteCourse($course); + if (null !== ($course = $this->courseService->getCourse($courseId))) { + $this->courseService->deleteCourse($course); return $this->redirect()->toRoute('admin_education/course'); } @@ -163,7 +163,7 @@ public function courseDocumentsAction(): ViewModel } $courseId = $this->params()->fromRoute('course'); - $course = $this->examService->getCourse($courseId); + $course = $this->courseService->getCourse($courseId); if (null === $course) { return $this->notFoundAction(); @@ -171,8 +171,8 @@ public function courseDocumentsAction(): ViewModel return new ViewModel([ 'course' => $course, - 'exams' => $this->examService->getDocumentsForCourse($course, ExamModel::class), - 'summaries' => $this->examService->getDocumentsForCourse($course, SummaryModel::class), + 'exams' => $this->courseService->getDocumentsForCourse($course, ExamModel::class), + 'summaries' => $this->courseService->getDocumentsForCourse($course, SummaryModel::class), ]); } @@ -185,7 +185,7 @@ public function deleteCourseDocumentAction(): Response|ViewModel } $courseId = $this->params()->fromRoute('course'); - $course = $this->examService->getCourse($courseId); + $course = $this->courseService->getCourse($courseId); if (null === $course) { return $this->notFoundAction(); @@ -197,8 +197,8 @@ public function deleteCourseDocumentAction(): Response|ViewModel if ($request->getPost()) { $documentId = (int) $this->params()->fromRoute('document'); - if (null !== ($document = $this->examService->getDocument($documentId))) { - $this->examService->deleteDocument($document); + if (null !== ($document = $this->courseService->getDocument($documentId))) { + $this->courseService->deleteDocument($document); return $this->redirect()->toRoute('admin_education/course/documents', ['course' => $course->getCode()]); } @@ -214,7 +214,7 @@ public function bulkExamAction(): ViewModel if ($request->isPost()) { // try uploading - if ($this->examService->tempExamUpload($request->getPost(), $request->getFiles())) { + if ($this->courseService->tempExamUpload($request->getPost(), $request->getFiles())) { return new ViewModel( [ 'success' => true, @@ -233,7 +233,7 @@ public function bulkExamAction(): ViewModel return new ViewModel( [ - 'form' => $this->examService->getTempUploadForm(), + 'form' => $this->courseService->getTempUploadForm(), ], ); } @@ -245,7 +245,7 @@ public function bulkSummaryAction(): ViewModel if ($request->isPost()) { // try uploading - if ($this->examService->tempSummaryUpload($request->getPost(), $request->getFiles())) { + if ($this->courseService->tempSummaryUpload($request->getPost(), $request->getFiles())) { return new ViewModel( [ 'success' => true, @@ -264,7 +264,7 @@ public function bulkSummaryAction(): ViewModel return new ViewModel( [ - 'form' => $this->examService->getTempUploadForm(), + 'form' => $this->courseService->getTempUploadForm(), ], ); } @@ -276,13 +276,13 @@ public function editExamAction(): ViewModel { /** @var Request $request */ $request = $this->getRequest(); - $form = $this->examService->getBulkExamForm(); + $form = $this->courseService->getBulkExamForm(); if ($request->isPost()) { $form->setData($request->getPost()->toArray()); if ($form->isValid()) { - if ($this->examService->bulkExamEdit($form->getData())) { + if ($this->courseService->bulkExamEdit($form->getData())) { return new ViewModel( [ 'success' => true, @@ -308,7 +308,7 @@ public function editSummaryAction(): ViewModel /** @var Request $request */ $request = $this->getRequest(); - if ($request->isPost() && $this->examService->bulkSummaryEdit($request->getPost()->toArray())) { + if ($request->isPost() && $this->courseService->bulkSummaryEdit($request->getPost()->toArray())) { return new ViewModel( [ 'success' => true, @@ -320,7 +320,7 @@ public function editSummaryAction(): ViewModel return new ViewModel( [ - 'form' => $this->examService->getBulkSummaryForm(), + 'form' => $this->courseService->getBulkSummaryForm(), 'config' => $config, ], ); @@ -332,7 +332,7 @@ public function deleteTempAction(): JsonModel|ViewModel $request = $this->getRequest(); if ($request->isPost()) { - $this->examService->deleteTempExam( + $this->courseService->deleteTempExam( $this->params()->fromRoute('filename'), $this->params()->fromRoute('type'), ); diff --git a/module/Education/src/Controller/EducationController.php b/module/Education/src/Controller/EducationController.php index f26b290a03..6386079afe 100644 --- a/module/Education/src/Controller/EducationController.php +++ b/module/Education/src/Controller/EducationController.php @@ -5,7 +5,7 @@ namespace Education\Controller; use Education\Form\SearchCourse as SearchCourseForm; -use Education\Service\Exam as ExamService; +use Education\Service\Course as CourseService; use Laminas\Http\Response; use Laminas\Http\Response\Stream; use Laminas\Mvc\Controller\AbstractActionController; @@ -14,7 +14,7 @@ class EducationController extends AbstractActionController { public function __construct( - private readonly ExamService $examService, + private readonly CourseService $courseService, private readonly SearchCourseForm $searchCourseForm, ) { } @@ -29,7 +29,7 @@ public function indexAction(): ViewModel $form->setData($query); if ($form->isValid()) { - $courses = $this->examService->searchCourse($form->getData()); + $courses = $this->courseService->searchCourse($form->getData()); return new ViewModel( [ @@ -50,7 +50,7 @@ public function indexAction(): ViewModel public function courseAction(): Response|ViewModel { $code = $this->params()->fromRoute('code'); - $course = $this->examService->getCourse($code); + $course = $this->courseService->getCourse($code); // If the course does not exist, trigger 404 if (null === $course) { @@ -71,7 +71,7 @@ public function downloadAction(): Stream|ViewModel { $id = (int) $this->params()->fromRoute('id'); - $download = $this->examService->getDocumentDownload($id); + $download = $this->courseService->getDocumentDownload($id); if (null === $download) { return $this->notFoundAction(); diff --git a/module/Education/src/Controller/Factory/AdminControllerFactory.php b/module/Education/src/Controller/Factory/AdminControllerFactory.php index 3d0e714a72..7addc642a2 100644 --- a/module/Education/src/Controller/Factory/AdminControllerFactory.php +++ b/module/Education/src/Controller/Factory/AdminControllerFactory.php @@ -22,7 +22,7 @@ public function __invoke( return new AdminController( $container->get('education_service_acl'), $container->get(MvcTranslator::class), - $container->get('education_service_exam'), + $container->get('education_service_course'), $container->get('config')['education_temp'], ); } diff --git a/module/Education/src/Controller/Factory/EducationControllerFactory.php b/module/Education/src/Controller/Factory/EducationControllerFactory.php index aeaa2810c4..64c1c72636 100644 --- a/module/Education/src/Controller/Factory/EducationControllerFactory.php +++ b/module/Education/src/Controller/Factory/EducationControllerFactory.php @@ -19,7 +19,7 @@ public function __invoke( ?array $options = null, ): EducationController { return new EducationController( - $container->get('education_service_exam'), + $container->get('education_service_course'), $container->get('education_form_searchcourse'), ); } diff --git a/module/Education/src/Module.php b/module/Education/src/Module.php index 542bf61bb9..6be14fa063 100644 --- a/module/Education/src/Module.php +++ b/module/Education/src/Module.php @@ -17,7 +17,7 @@ use Education\Mapper\CourseDocument as CourseDocumentMapper; use Education\Model\Exam as ExamModel; use Education\Model\Summary as SummaryModel; -use Education\Service\Exam as ExamService; +use Education\Service\Course as CourseService; use Education\View\Helper\ExamUrl; use Laminas\Mvc\I18n\Translator as MvcTranslator; use Psr\Container\ContainerInterface; @@ -44,7 +44,7 @@ public function getServiceConfig(): array { return [ 'factories' => [ - 'education_service_exam' => static function (ContainerInterface $container) { + 'education_service_course' => static function (ContainerInterface $container) { $aclService = $container->get('education_service_acl'); $translator = $container->get(MvcTranslator::class); $storageService = $container->get('application_service_storage'); @@ -56,7 +56,7 @@ public function getServiceConfig(): array $bulkExamForm = $container->get('education_form_bulk_exam'); $config = $container->get('config'); - return new ExamService( + return new CourseService( $aclService, $translator, $storageService, @@ -167,7 +167,7 @@ public function getViewHelperConfig(): array $config = $container->get('config'); $helper = new ExamUrl(); $helper->setDir($config['education']['public_dir']); - $helper->setExamService($container->get('education_service_exam')); + $helper->setExamService($container->get('education_service_course')); return $helper; }, diff --git a/module/Education/src/Service/Exam.php b/module/Education/src/Service/Course.php similarity index 99% rename from module/Education/src/Service/Exam.php rename to module/Education/src/Service/Course.php index 7ffcd823f3..7ebf8eb274 100644 --- a/module/Education/src/Service/Exam.php +++ b/module/Education/src/Service/Course.php @@ -43,9 +43,9 @@ use function unlink; /** - * Exam service. + * Course service. */ -class Exam +class Course { protected ?BulkForm $bulkForm = null; diff --git a/module/Education/src/View/Helper/ExamUrl.php b/module/Education/src/View/Helper/ExamUrl.php index 1c7e414dd0..57a2288c4e 100644 --- a/module/Education/src/View/Helper/ExamUrl.php +++ b/module/Education/src/View/Helper/ExamUrl.php @@ -5,15 +5,15 @@ namespace Education\View\Helper; use Education\Model\Exam; -use Education\Service\Exam as ExamService; +use Education\Service\Course as CourseService; use Laminas\View\Helper\AbstractHelper; class ExamUrl extends AbstractHelper { /** - * Exam service. + * Course service. */ - protected ExamService $examService; + protected CourseService $courseService; /** * Education data dir. @@ -25,7 +25,7 @@ class ExamUrl extends AbstractHelper */ public function __invoke(Exam $exam): string { - return $this->getView()->basePath($this->getDir() . '/' . $this->examService->courseDocumentToFilename($exam)); + return $this->getView()->basePath($this->getDir() . '/' . $this->courseService->courseDocumentToFilename($exam)); } /** @@ -49,14 +49,14 @@ public function setDir(string $dir): void */ public function getExamService(): ExamService { - return $this->examService; + return $this->courseService; } /** * Set the authentication service. */ - public function setExamService(ExamService $examService): void + public function setCourseService(CourseService $courseService): void { - $this->examService = $examService; + $this->courseService = $courseService; } } From 9d6a1f4fc89018bd19f4c9c06397a5e5896b90ed Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:07:45 +0000 Subject: [PATCH 3/8] Implement functionality for similar course (MVP) --- module/Application/language/en.po | 10 +++ module/Application/language/gewisweb.pot | 8 +++ module/Application/language/nl.po | 10 +++ .../src/Controller/AdminController.php | 13 ++-- module/Education/src/Form/Course.php | 61 +++++++++++++++++++ module/Education/src/Model/Course.php | 44 +++++++++++++ module/Education/src/Service/Course.php | 42 ++++++++++++- .../view/education/admin/add-course.phtml | 13 ++++ .../view/education/admin/edit-course.phtml | 13 ++++ .../view/education/education/course.phtml | 17 ++++++ 10 files changed, 222 insertions(+), 9 deletions(-) diff --git a/module/Application/language/en.po b/module/Application/language/en.po index a4c907e010..ee03409b87 100644 --- a/module/Application/language/en.po +++ b/module/Application/language/en.po @@ -2040,6 +2040,13 @@ msgstr "Older" msgid "Onderwijscommissaris" msgstr "Educational Officer" +msgid "" +"One of the courses is not valid (either it does not exist or is the same as " +"the current course)" +msgstr "" +"One of the courses is not valid (either it does not exist or is the same as " +"the current course)" + msgid "Only allow GEWIS members" msgstr "Only allow GEWIS members" @@ -2570,6 +2577,9 @@ msgstr "Sign-up List" msgid "Sign-up Lists" msgstr "Sign-up Lists" +msgid "Similar courses" +msgstr "Similar courses" + msgid "Slogan" msgstr "Slogan" diff --git a/module/Application/language/gewisweb.pot b/module/Application/language/gewisweb.pot index 4a2f2b383f..de6f7906eb 100644 --- a/module/Application/language/gewisweb.pot +++ b/module/Application/language/gewisweb.pot @@ -1941,6 +1941,11 @@ msgstr "" msgid "Onderwijscommissaris" msgstr "" +msgid "" +"One of the courses is not valid (either it does not exist or is the same as " +"the current course)" +msgstr "" + msgid "Only allow GEWIS members" msgstr "" @@ -2433,6 +2438,9 @@ msgstr "" msgid "Sign-up Lists" msgstr "" +msgid "Similar courses" +msgstr "" + msgid "Slogan" msgstr "" diff --git a/module/Application/language/nl.po b/module/Application/language/nl.po index 0a12e63e60..e41c0f7c9b 100644 --- a/module/Application/language/nl.po +++ b/module/Application/language/nl.po @@ -2063,6 +2063,13 @@ msgstr "Ouder" msgid "Onderwijscommissaris" msgstr "Onderwijscommissaris" +msgid "" +"One of the courses is not valid (either it does not exist or is the same as " +"the current course)" +msgstr "" +"Een van de vakken is ongeldig (hij bestaat niet of is hetzelfde als het " +"huidige vak)" + msgid "Only allow GEWIS members" msgstr "Sta alleen GEWIS leden toe" @@ -2596,6 +2603,9 @@ msgstr "Inschrijflijst" msgid "Sign-up Lists" msgstr "Inschrijflijsten" +msgid "Similar courses" +msgstr "Vergelijkbare vakken" + msgid "Slogan" msgstr "Slogan" diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index 6fe72e816c..a05033d338 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -9,6 +9,7 @@ use Education\Model\Summary as SummaryModel; use Education\Service\AclService; use Education\Service\Course as CourseService; +use Laminas\Form\FormInterface; use Laminas\Http\Request; use Laminas\Http\Response; use Laminas\Mvc\Controller\AbstractActionController; @@ -68,10 +69,10 @@ public function addCourseAction(): Response|ViewModel $form->setData($request->getPost()->toArray()); if ($form->isValid()) { - /** @var CourseModel $course */ - $course = $form->getObject(); + $data = $form->getData(FormInterface::VALUES_AS_ARRAY); + $course = $this->courseService->saveCourse($data); - if ($this->courseService->saveCourse($course)) { + if (null !== $course) { $this->flashMessenger()->addSuccessMessage( $this->translator->translate('Successfully added course!'), ); @@ -113,10 +114,10 @@ public function editCourseAction(): ViewModel $form->setData($request->getPost()->toArray()); if ($form->isValid()) { - /** @var CourseModel $course */ - $course = $form->getObject(); + $data = $form->getData(FormInterface::VALUES_AS_ARRAY); + $course = $this->courseService->updateCourse($course, $data); - if ($this->courseService->saveCourse($course)) { + if (null !== $course) { $this->flashMessenger()->addSuccessMessage( $this->translator->translate('Successfully updated course information!'), ); diff --git a/module/Education/src/Form/Course.php b/module/Education/src/Form/Course.php index 1efa45aaa5..982f208566 100644 --- a/module/Education/src/Form/Course.php +++ b/module/Education/src/Form/Course.php @@ -15,6 +15,8 @@ use Laminas\Validator\Callback; use Laminas\Validator\StringLength; +use function explode; + class Course extends Form implements InputFilterProviderInterface { private ?string $currentCode = null; @@ -45,6 +47,16 @@ public function __construct( ], ); + $this->add( + [ + 'name' => 'similar', + 'type' => Text::class, + 'options' => [ + 'label' => $translator->translate('Similar courses'), + ], + ], + ); + $this->add( [ 'name' => 'submit', @@ -96,6 +108,22 @@ public function getInputFilterSpecification(): array 'name' => [ 'required' => true, ], + 'similar' => [ + 'required' => false, + 'validators' => [ + [ + 'name' => Callback::class, + 'options' => [ + 'callback' => [$this, 'areSimilarValid'], + 'messages' => [ + Callback::INVALID_VALUE => $this->translator->translate( + 'One of the courses is not valid (either it does not exist or is the same as the current course)', + ), + ], + ], + ], + ], + ], ]; } @@ -107,6 +135,9 @@ public function setCurrentCode(string $currentCode): void $this->currentCode = $currentCode; } + /** + * Check if a course code is unique. + */ public function isCourseCodeUnique(string $code): bool { if ($this->currentCode === $code) { @@ -115,4 +146,34 @@ public function isCourseCodeUnique(string $code): bool return null === $this->courseMapper->find($code); } + + /** + * Check if the similar courses are valid. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification + */ + public function areSimilarValid( + string $similar, + array $context = [] + ): bool { + $code = $context['code']; + $courses = explode(",", $similar); + + foreach ($courses as $course) { + if (!$this->isSimilarValid($course, $code)) { + return false; + } + } + + return true; + } + + /** + * Check if a similar course is valid. + */ + public function isSimilarValid(string $similar, string $code): bool + { + return $similar !== $code + && null !== $this->courseMapper->find($similar); + } } diff --git a/module/Education/src/Model/Course.php b/module/Education/src/Model/Course.php index 3305c6a1b4..2d5359ee31 100644 --- a/module/Education/src/Model/Course.php +++ b/module/Education/src/Model/Course.php @@ -17,6 +17,8 @@ use Doctrine\ORM\Mapping\OrderBy; use Laminas\Permissions\Acl\Resource\ResourceInterface; +use function implode; + /** * Course. */ @@ -132,6 +134,7 @@ public function setName(string $name): void * @return array{ * code: string, * name: string, + * similar: string, * } */ public function toArray(): array @@ -139,9 +142,50 @@ public function toArray(): array return [ 'code' => $this->getCode(), 'name' => $this->getName(), + 'similar' => $this->getSimilarCoursesAsString(), ]; } + /** + * Get the similar courses to this course as a comma separated string. + */ + public function getSimilarCoursesAsString(): string + { + return implode(',', $this->similarCoursesTo->map( + fn (self $course) => $course->getCode() + )->toArray()); + } + + /** + * Get the similar courses to this course. + */ + public function getSimilarCoursesTo(): Collection + { + return $this->similarCoursesTo; + } + + /** + * Adds a course to the similar courses to list if it doesn't yet exist. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification + */ + public function addSimilarCourseTo(self $course): void + { + if ($this->similarCoursesTo->contains($course)) { + return; + } + + $this->similarCoursesTo->add($course); + } + + /** + * Removes all references to similar courses to this course. + */ + public function clearSimilarCoursesTo(): void + { + $this->similarCoursesTo->clear(); + } + /** * Get the resource ID. */ diff --git a/module/Education/src/Service/Course.php b/module/Education/src/Service/Course.php index 7ebf8eb274..e79117f75b 100644 --- a/module/Education/src/Service/Course.php +++ b/module/Education/src/Service/Course.php @@ -485,13 +485,49 @@ public function getCourseForm(?CourseModel $course = null): CourseForm } /** - * Add a new course. + * Save a course. + * + * @param array $data + * + * @throws ORMException + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ - public function saveCourse(CourseModel $course): bool + public function saveCourse(array $data): CourseModel { + $course = new CourseModel(); + $course->setCode($data['code']); + $course->setName($data['name']); + + $similarCoursesCodes = explode(",", $data['similar']); + + foreach ($similarCoursesCodes as $similarCourseCode) { + $similarCourse = $this->getCourse($similarCourseCode); + $course->addSimilarCourseTo($similarCourse); + } + $this->courseMapper->persist($course); - return true; + return $course; + } + + public function updateCourse(CourseModel $course, array $data): CourseModel + { + $course->setCode($data['code']); + $course->setName($data['name']); + + $similarCoursesCodes = explode(",", $data['similar']); + + $course->clearSimilarCoursesTo(); + + foreach ($similarCoursesCodes as $similarCourseCode) { + $similarCourse = $this->getCourse($similarCourseCode); + $course->addSimilarCourseTo($similarCourse); + } + + $this->courseMapper->persist($course); + + return $course; } /** diff --git a/module/Education/view/education/admin/add-course.phtml b/module/Education/view/education/admin/add-course.phtml index daa7c83689..f82e0c9326 100644 --- a/module/Education/view/education/admin/add-course.phtml +++ b/module/Education/view/education/admin/add-course.phtml @@ -53,6 +53,19 @@ $form->setAttribute('method', 'post'); formElementErrors($element) ?> +
+ get('similar'); + $element->setAttribute('class', 'form-control'); + ?> +
+ + formText($element) ?> + formElementErrors($element) ?> +
+
setAttribute('method', 'post'); formElementErrors($element) ?>
+
+ get('similar'); + $element->setAttribute('class', 'form-control'); + ?> +
+ + formText($element) ?> + formElementErrors($element) ?> +
+
+
+
+
+

translate('Similar courses') ?>

+ +
+
From fadc5e9948568c0a7e8315febdd8a5bab13c6f9e Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:27:09 +0000 Subject: [PATCH 4/8] Fix styling and small bugs --- .../src/Controller/AdminController.php | 31 +++++++------------ module/Education/src/Module.php | 2 +- module/Education/src/Service/Course.php | 19 ++++++------ module/Education/src/View/Helper/ExamUrl.php | 10 +++--- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index a05033d338..372e0ca03d 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -4,7 +4,6 @@ namespace Education\Controller; -use Education\Model\Course as CourseModel; use Education\Model\Exam as ExamModel; use Education\Model\Summary as SummaryModel; use Education\Service\AclService; @@ -72,18 +71,16 @@ public function addCourseAction(): Response|ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->saveCourse($data); - if (null !== $course) { - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully added course!'), - ); - - return $this->redirect()->toRoute('admin_education/course/edit', ['course' => $course->getCode()]); - } - - $this->flashMessenger()->addErrorMessage( - $this->translator->translate('An error occurred while saving the course!'), + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully added course!'), ); + + return $this->redirect()->toRoute('admin_education/course/edit', ['course' => $course->getCode()]); } + + $this->flashMessenger()->addErrorMessage( + $this->translator->translate('The course form is invalid!'), + ); } return new ViewModel( @@ -117,15 +114,9 @@ public function editCourseAction(): ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->updateCourse($course, $data); - if (null !== $course) { - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully updated course information!'), - ); - } else { - $this->flashMessenger()->addErrorMessage( - $this->translator->translate('An error occurred while saving the course!'), - ); - } + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully updated course information!'), + ); } } diff --git a/module/Education/src/Module.php b/module/Education/src/Module.php index 6be14fa063..3ec3700d5d 100644 --- a/module/Education/src/Module.php +++ b/module/Education/src/Module.php @@ -167,7 +167,7 @@ public function getViewHelperConfig(): array $config = $container->get('config'); $helper = new ExamUrl(); $helper->setDir($config['education']['public_dir']); - $helper->setExamService($container->get('education_service_course')); + $helper->setCourseService($container->get('education_service_course')); return $helper; }, diff --git a/module/Education/src/Service/Course.php b/module/Education/src/Service/Course.php index e79117f75b..71f428021c 100644 --- a/module/Education/src/Service/Course.php +++ b/module/Education/src/Service/Course.php @@ -486,12 +486,6 @@ public function getCourseForm(?CourseModel $course = null): CourseForm /** * Save a course. - * - * @param array $data - * - * @throws ORMException - * - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ public function saveCourse(array $data): CourseModel { @@ -499,7 +493,7 @@ public function saveCourse(array $data): CourseModel $course->setCode($data['code']); $course->setName($data['name']); - $similarCoursesCodes = explode(",", $data['similar']); + $similarCoursesCodes = explode(',', $data['similar']); foreach ($similarCoursesCodes as $similarCourseCode) { $similarCourse = $this->getCourse($similarCourseCode); @@ -511,12 +505,17 @@ public function saveCourse(array $data): CourseModel return $course; } - public function updateCourse(CourseModel $course, array $data): CourseModel - { + /** + * Update a course. + */ + public function updateCourse( + CourseModel $course, + array $data, + ): CourseModel { $course->setCode($data['code']); $course->setName($data['name']); - $similarCoursesCodes = explode(",", $data['similar']); + $similarCoursesCodes = explode(',', $data['similar']); $course->clearSimilarCoursesTo(); diff --git a/module/Education/src/View/Helper/ExamUrl.php b/module/Education/src/View/Helper/ExamUrl.php index 57a2288c4e..25e1c2ff9e 100644 --- a/module/Education/src/View/Helper/ExamUrl.php +++ b/module/Education/src/View/Helper/ExamUrl.php @@ -25,7 +25,9 @@ class ExamUrl extends AbstractHelper */ public function __invoke(Exam $exam): string { - return $this->getView()->basePath($this->getDir() . '/' . $this->courseService->courseDocumentToFilename($exam)); + return $this->getView()->basePath( + $this->getDir() . '/' . $this->courseService->courseDocumentToFilename($exam), + ); } /** @@ -45,15 +47,15 @@ public function setDir(string $dir): void } /** - * Get the authentication service. + * Get the course service. */ - public function getExamService(): ExamService + public function getCourseService(): CourseService { return $this->courseService; } /** - * Set the authentication service. + * Set the course service. */ public function setCourseService(CourseService $courseService): void { From fb163e893bf5e3ee493b0f247fe8e042688bcd99 Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:07:45 +0000 Subject: [PATCH 5/8] Implement functionality for similar course (MVP) --- .../src/Controller/AdminController.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index 372e0ca03d..ff4e28fcec 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -71,9 +71,10 @@ public function addCourseAction(): Response|ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->saveCourse($data); - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully added course!'), - ); + if ($this->courseService->saveCourse($course)) { + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully added course!'), + ); return $this->redirect()->toRoute('admin_education/course/edit', ['course' => $course->getCode()]); } @@ -114,9 +115,15 @@ public function editCourseAction(): ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->updateCourse($course, $data); - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully updated course information!'), - ); + if ($this->courseService->saveCourse($course)) { + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully updated course information!'), + ); + } else { + $this->flashMessenger()->addErrorMessage( + $this->translator->translate('An error occurred while saving the course!'), + ); + } } } From 1b13555b370b98e43b32d273bd2f4d18f93774a7 Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:52:57 +0000 Subject: [PATCH 6/8] Hide similar courses when there are none --- .../src/Controller/AdminController.php | 7 ++-- .../view/education/education/course.phtml | 34 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index ff4e28fcec..ae32b4e90b 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -71,10 +71,9 @@ public function addCourseAction(): Response|ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->saveCourse($data); - if ($this->courseService->saveCourse($course)) { - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully added course!'), - ); + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully added course!'), + ); return $this->redirect()->toRoute('admin_education/course/edit', ['course' => $course->getCode()]); } diff --git a/module/Education/view/education/education/course.phtml b/module/Education/view/education/education/course.phtml index 79a3eef550..c78b27046e 100644 --- a/module/Education/view/education/education/course.phtml +++ b/module/Education/view/education/education/course.phtml @@ -124,22 +124,24 @@ foreach ($documents as $document) { -
-
-
-

translate('Similar courses') ?>

- + getSimilarCoursesTo()) > 0): ?> +
+
+
+

translate('Similar courses') ?>

+ +
-
+
From b796f3f0f02b14a0fd17b9cf9aa70e51df2108cd Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:08:26 +0000 Subject: [PATCH 7/8] Small styling fixes and bug fix --- .../Education/src/Controller/AdminController.php | 12 +++--------- module/Education/src/Form/Course.php | 14 +++++++++----- module/Education/src/Model/Course.php | 12 +++++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/module/Education/src/Controller/AdminController.php b/module/Education/src/Controller/AdminController.php index ae32b4e90b..372e0ca03d 100644 --- a/module/Education/src/Controller/AdminController.php +++ b/module/Education/src/Controller/AdminController.php @@ -114,15 +114,9 @@ public function editCourseAction(): ViewModel $data = $form->getData(FormInterface::VALUES_AS_ARRAY); $course = $this->courseService->updateCourse($course, $data); - if ($this->courseService->saveCourse($course)) { - $this->flashMessenger()->addSuccessMessage( - $this->translator->translate('Successfully updated course information!'), - ); - } else { - $this->flashMessenger()->addErrorMessage( - $this->translator->translate('An error occurred while saving the course!'), - ); - } + $this->flashMessenger()->addSuccessMessage( + $this->translator->translate('Successfully updated course information!'), + ); } } diff --git a/module/Education/src/Form/Course.php b/module/Education/src/Form/Course.php index 982f208566..2276e96439 100644 --- a/module/Education/src/Form/Course.php +++ b/module/Education/src/Form/Course.php @@ -117,7 +117,8 @@ public function getInputFilterSpecification(): array 'callback' => [$this, 'areSimilarValid'], 'messages' => [ Callback::INVALID_VALUE => $this->translator->translate( - 'One of the courses is not valid (either it does not exist or is the same as the current course)', + 'One of the courses is not valid (either it does not exist or is the same as the ' + . 'current course)', ), ], ], @@ -153,11 +154,11 @@ public function isCourseCodeUnique(string $code): bool * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ public function areSimilarValid( - string $similar, - array $context = [] + string $similar, + array $context = [], ): bool { $code = $context['code']; - $courses = explode(",", $similar); + $courses = explode(',', $similar); foreach ($courses as $course) { if (!$this->isSimilarValid($course, $code)) { @@ -171,7 +172,10 @@ public function areSimilarValid( /** * Check if a similar course is valid. */ - public function isSimilarValid(string $similar, string $code): bool + public function isSimilarValid( + string $similar, + string $code + ): bool { return $similar !== $code && null !== $this->courseMapper->find($similar); diff --git a/module/Education/src/Model/Course.php b/module/Education/src/Model/Course.php index 2d5359ee31..95e23a7c68 100644 --- a/module/Education/src/Model/Course.php +++ b/module/Education/src/Model/Course.php @@ -63,20 +63,20 @@ class Course implements ResourceInterface /** * Courses similar to this course - * + * * @var Collection */ #[JoinTable(name: 'SimilarCourse')] #[JoinColumn( - name: 'course_code', + name: 'course_code', referencedColumnName: 'code', )] #[InverseJoinColumn( - name: 'similar_course_code', + name: 'similar_course_code', referencedColumnName: 'code', )] #[ManyToMany( - targetEntity: self::class, + targetEntity: self::class, inversedBy: 'similarCoursesFrom', )] private Collection $similarCoursesTo; @@ -152,12 +152,14 @@ public function toArray(): array public function getSimilarCoursesAsString(): string { return implode(',', $this->similarCoursesTo->map( - fn (self $course) => $course->getCode() + static fn (self $course) => $course->getCode() )->toArray()); } /** * Get the similar courses to this course. + * + * @return Collection */ public function getSimilarCoursesTo(): Collection { From 74036ddb9b2f81973fa3d0fbce22587b5f8d5b16 Mon Sep 17 00:00:00 2001 From: Jort van Driel <10116429+JortvD@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:20:31 +0000 Subject: [PATCH 8/8] More styling fixes --- module/Education/src/Form/Course.php | 7 +++---- module/Education/src/Model/Course.php | 4 +--- module/Education/src/Service/Course.php | 4 ++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/module/Education/src/Form/Course.php b/module/Education/src/Form/Course.php index 2276e96439..66a7cc1caf 100644 --- a/module/Education/src/Form/Course.php +++ b/module/Education/src/Form/Course.php @@ -173,10 +173,9 @@ public function areSimilarValid( * Check if a similar course is valid. */ public function isSimilarValid( - string $similar, - string $code - ): bool - { + string $similar, + string $code, + ): bool { return $similar !== $code && null !== $this->courseMapper->find($similar); } diff --git a/module/Education/src/Model/Course.php b/module/Education/src/Model/Course.php index 95e23a7c68..a7f8a75b26 100644 --- a/module/Education/src/Model/Course.php +++ b/module/Education/src/Model/Course.php @@ -158,7 +158,7 @@ public function getSimilarCoursesAsString(): string /** * Get the similar courses to this course. - * + * * @return Collection */ public function getSimilarCoursesTo(): Collection @@ -168,8 +168,6 @@ public function getSimilarCoursesTo(): Collection /** * Adds a course to the similar courses to list if it doesn't yet exist. - * - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ public function addSimilarCourseTo(self $course): void { diff --git a/module/Education/src/Service/Course.php b/module/Education/src/Service/Course.php index 71f428021c..c09e0b05ca 100644 --- a/module/Education/src/Service/Course.php +++ b/module/Education/src/Service/Course.php @@ -486,6 +486,8 @@ public function getCourseForm(?CourseModel $course = null): CourseForm /** * Save a course. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ public function saveCourse(array $data): CourseModel { @@ -507,6 +509,8 @@ public function saveCourse(array $data): CourseModel /** * Update a course. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification */ public function updateCourse( CourseModel $course,