diff --git a/CHANGELOG.md b/CHANGELOG.md index 1609a963..a38d7298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ en las distintas versiones de la misma. 5.x.x (xxxx-xx-xx) ------------------ * fix: Solucionado el error de generación de documentos PDF muy complejos +* feat: Ahora se pueden añadir comentarios a las concreciones de un convenio concreto y desactivar elementos 5.2.2 (2022-11-28) ------------------ diff --git a/config/packages/simple_things_entity_audit.yaml b/config/packages/simple_things_entity_audit.yaml index e2d260c0..6dcad99d 100644 --- a/config/packages/simple_things_entity_audit.yaml +++ b/config/packages/simple_things_entity_audit.yaml @@ -8,6 +8,7 @@ simple_things_entity_audit: - App\Entity\Edu\ContactMethod - App\Entity\WLT\ActivityRealizationGrade - App\Entity\WLT\AgreementActivityRealization + - App\Entity\WLT\AgreementActivityRealizationComment - App\Entity\WLT\Contact - App\Entity\WLT\EducationalTutorAnsweredSurvey - App\Entity\WLT\LearningProgram diff --git a/src/Controller/WLT/EvaluationController.php b/src/Controller/WLT/EvaluationController.php index c10541f4..e5eb1ae4 100644 --- a/src/Controller/WLT/EvaluationController.php +++ b/src/Controller/WLT/EvaluationController.php @@ -21,11 +21,15 @@ use App\Entity\Edu\AcademicYear; use App\Entity\Person; use App\Entity\WLT\Agreement; +use App\Entity\WLT\AgreementActivityRealization; +use App\Entity\WLT\AgreementActivityRealizationComment; +use App\Form\Type\WLT\AgreementActivityRealizationNewCommentType; use App\Form\Type\WLT\AgreementEvaluationType; use App\Repository\Edu\AcademicYearRepository; use App\Repository\WLT\ProjectRepository; use App\Repository\WLT\WLTGroupRepository; use App\Security\OrganizationVoter; +use App\Security\WLT\AgreementActivityRealizationCommentVoter; use App\Security\WLT\AgreementVoter; use App\Security\WLT\WLTOrganizationVoter; use App\Service\UserExtensionService; @@ -237,4 +241,140 @@ public function listAction( 'academic_years' => $academicYearRepository->findAllByOrganization($organization) ]); } + + /** + * @Route("/comentarios/{id}", name="work_linked_training_evaluation_comment_form", + * requirements={"id" = "\d+"}, methods={"GET", "POST"}) + */ + public function commentAction( + Request $request, + TranslatorInterface $translator, + AgreementActivityRealization $agreementActivityRealization + ) { + $agreement = $agreementActivityRealization->getAgreement(); + $this->denyAccessUnlessGranted(AgreementVoter::VIEW_GRADE, $agreement); + + $em = $this->getDoctrine()->getManager(); + + $readOnly = !$this->isGranted(AgreementVoter::GRADE, $agreement); + + $form = $this->createForm(AgreementActivityRealizationNewCommentType::class, $agreementActivityRealization, [ + 'disabled' => $readOnly + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $newComment = null; + if (trim($form->get('newComment')->getData()) !== '') { + $newComment = new AgreementActivityRealizationComment(); + $em->persist($newComment); + $newComment + ->setAgreementActivityRealization($agreementActivityRealization) + ->setComment(trim($form->get('newComment')->getData())) + ->setTimestamp(new \DateTime()) + ->setPerson($this->getUser()); + } + $em->flush(); + $this->addFlash('success', $translator->trans('message.saved', [], + 'wlt_agreement_activity_realization')); + + if (!$newComment) { + return $this->redirectToRoute('work_linked_training_evaluation_form', [ + 'id' => $agreement->getId() + ]); + } else { + return $this->redirectToRoute('work_linked_training_evaluation_comment_form', [ + 'id' => $agreementActivityRealization->getId() + ]); + } + } catch (\Exception $e) { + $this->addFlash('error', $translator->trans('message.error', [], + 'wlt_agreement_activity_realization')); + } + } + + $title = $translator->trans('title.comment', [], 'wlt_agreement_activity_realization'); + + $breadcrumb = [ + [ + 'fixed' => (string) $agreement, + 'routeName' => 'work_linked_training_evaluation_form', + 'routeParams' => ['id' => $agreement->getId()] + ], + ['fixed' => (string) $agreementActivityRealization->getActivityRealization()] + ]; + + return $this->render('wlt/evaluation/comment_form.html.twig', [ + 'menu_path' => 'work_linked_training_evaluation_list', + 'breadcrumb' => $breadcrumb, + 'title' => $title, + 'agreement' => $agreement, + 'agreement_activity_realization' => $agreementActivityRealization, + 'read_only' => $readOnly, + 'form' => $form->createView() + ]); + } + + /** + * @Route("/comentarios/eliminar/{id}", name="work_linked_training_evaluation_comment_delete", + * requirements={"id" = "\d+"}, methods={"GET", "POST"}) + */ + public function deleteCommentAction( + Request $request, + TranslatorInterface $translator, + AgreementActivityRealizationComment $agreementActivityRealizationComment + ) { + $this->denyAccessUnlessGranted(AgreementActivityRealizationCommentVoter::DELETE, + $agreementActivityRealizationComment); + + $em = $this->getDoctrine()->getManager(); + + $agreement = $agreementActivityRealizationComment->getAgreementActivityRealization()->getAgreement(); + + if ($request->get('confirm', '') === 'ok') { + try { + $em->remove($agreementActivityRealizationComment); + $em->flush(); + $this->addFlash('success', $translator->trans('message.comment_deleted', [], + 'wlt_agreement_activity_realization')); + } catch (\Exception $e) { + $this->addFlash('error', $translator->trans('message.comment_delete_error', [], + 'wlt_agreement_activity_realization')); + } + return $this->redirectToRoute('work_linked_training_evaluation_comment_form', [ + 'id' => $agreementActivityRealizationComment->getAgreementActivityRealization()->getId() + ]); + } + + $title = $translator->trans('title.delete_comment', [], 'wlt_agreement_activity_realization'); + + $breadcrumb = [ + [ + 'fixed' => (string) $agreement, + 'routeName' => 'work_linked_training_evaluation_form', + 'routeParams' => ['id' => $agreement->getId()] + ], + [ + 'fixed' => (string) $agreementActivityRealizationComment + ->getAgreementActivityRealization()->getActivityRealization(), + 'routeName' => 'work_linked_training_evaluation_comment_form', + 'routeParams' => ['id' => $agreementActivityRealizationComment + ->getAgreementActivityRealization()->getId()] + ], + [ + 'fixed' => $title + ] + ]; + + return $this->render('wlt/evaluation/comment_delete.html.twig', [ + 'menu_path' => 'work_linked_training_evaluation_list', + 'breadcrumb' => $breadcrumb, + 'title' => $title, + 'comment' => $agreementActivityRealizationComment, + 'agreement' => $agreement, + 'agreement_activity_realization' => $agreementActivityRealizationComment->getAgreementActivityRealization() + ]); + } } diff --git a/src/Controller/WLT/TrackingCalendarController.php b/src/Controller/WLT/TrackingCalendarController.php index 90088e3e..5fed1fce 100644 --- a/src/Controller/WLT/TrackingCalendarController.php +++ b/src/Controller/WLT/TrackingCalendarController.php @@ -68,7 +68,7 @@ public function indexAction( ? $workDayRepository->hoursStatsByAgreement($agreement) : []; - $activityRealizations = $agreementActivityRealizationRepository->findByAgreementSorted($agreement); + $activityRealizations = $agreementActivityRealizationRepository->findByAgreementSortedAndEnabled($agreement); $title = $translator->trans('title.calendar', [], 'wlt_tracking'); diff --git a/src/Entity/WLT/AgreementActivityRealization.php b/src/Entity/WLT/AgreementActivityRealization.php index 8082c1ee..9abeb26f 100644 --- a/src/Entity/WLT/AgreementActivityRealization.php +++ b/src/Entity/WLT/AgreementActivityRealization.php @@ -19,6 +19,8 @@ namespace App\Entity\WLT; use App\Entity\Person; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -43,7 +45,7 @@ class AgreementActivityRealization private $agreement; /** - * @ORM\ManyToOne(targetEntity="ActivityRealization") + * @ORM\ManyToOne(targetEntity="ActivityRealization", fetch="EAGER") * @ORM\JoinColumn(nullable=false) * @var ActivityRealization */ @@ -69,6 +71,24 @@ class AgreementActivityRealization */ private $gradedOn; + /** + * @ORM\Column(type="boolean") + * @var bool + */ + private $disabled; + + /** + * @ORM\OneToMany(targetEntity="AgreementActivityRealizationComment", mappedBy="agreementActivityRealization") + * @ORM\OrderBy({"timestamp":"ASC"}) + * @var AgreementActivityRealizationComment[]|Collection + */ + private $comments; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } + /** * @return int */ @@ -166,4 +186,40 @@ public function setGradedOn(\DateTimeInterface $gradedOn = null) $this->gradedOn = $gradedOn; return $this; } + + /** + * @return bool + */ + public function isDisabled() + { + return $this->disabled; + } + + /** + * @param bool $disabled + * @return AgreementActivityRealization + */ + public function setDisabled($disabled) + { + $this->disabled = $disabled; + return $this; + } + + /** + * @return AgreementActivityRealizationComment[]|Collection + */ + public function getComments() + { + return $this->comments; + } + + /** + * @param Collection $comments + * @return AgreementActivityRealization + */ + public function setComments($comments) + { + $this->comments = $comments; + return $this; + } } diff --git a/src/Entity/WLT/AgreementActivityRealizationComment.php b/src/Entity/WLT/AgreementActivityRealizationComment.php new file mode 100644 index 00000000..1b92ba59 --- /dev/null +++ b/src/Entity/WLT/AgreementActivityRealizationComment.php @@ -0,0 +1,143 @@ +id; + } + + /** + * @return AgreementActivityRealization + */ + public function getAgreementActivityRealization() + { + return $this->agreementActivityRealization; + } + + /** + * @param AgreementActivityRealization $agreementActivityRealization + * @return AgreementActivityRealizationComment + */ + public function setAgreementActivityRealization(AgreementActivityRealization $agreementActivityRealization) + { + $this->agreementActivityRealization = $agreementActivityRealization; + return $this; + } + + /** + * @return Person + */ + public function getPerson() + { + return $this->person; + } + + /** + * @param Person $person + * @return AgreementActivityRealizationComment + */ + public function setPerson(Person $person) + { + $this->person = $person; + return $this; + } + + /** + * @return string + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * @param string $comment + * @return AgreementActivityRealizationComment + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * @return \DateTime + */ + public function getTimestamp(): \DateTime + { + return $this->timestamp; + } + + /** + * @param \DateTime $timestamp + * @return AgreementActivityRealizationComment + */ + public function setTimestamp(\DateTime $timestamp) + { + $this->timestamp = $timestamp; + return $this; + } +} diff --git a/src/Form/Type/WLT/AgreementActivityRealizationNewCommentType.php b/src/Form/Type/WLT/AgreementActivityRealizationNewCommentType.php new file mode 100644 index 00000000..74beb11a --- /dev/null +++ b/src/Form/Type/WLT/AgreementActivityRealizationNewCommentType.php @@ -0,0 +1,65 @@ +add('disabled', ChoiceType::class, [ + 'label' => 'form.disabled', + 'required' => true, + 'expanded' => true, + 'choices' => [ + 'form.disabled.false' => false, + 'form.disabled.true' => true + ] + ]) + ->add('newComment', TextareaType::class, [ + 'label' => 'form.new_comment', + 'mapped' => false, + 'attr' => [ + 'rows' => 5 + ], + 'required' => false + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => AgreementActivityRealization::class, + 'translation_domain' => 'wlt_agreement_activity_realization' + ]); + } +} diff --git a/src/Form/Type/WLT/AgreementActivityRealizationType.php b/src/Form/Type/WLT/AgreementActivityRealizationType.php index 96133746..d50ca6bb 100644 --- a/src/Form/Type/WLT/AgreementActivityRealizationType.php +++ b/src/Form/Type/WLT/AgreementActivityRealizationType.php @@ -74,7 +74,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'choice_translation_domain' => false, 'choices' => $this->grades, 'class' => ActivityRealizationGrade::class, - 'disabled' => !in_array($data, $this->submittedAgreementActivityRealizations, true), + 'disabled' => $data->isDisabled() || + !in_array($data, $this->submittedAgreementActivityRealizations, true), 'expanded' => true, 'label_attr' => ['class' => 'radio-inline'], 'placeholder' => $this->translator->trans( diff --git a/src/Form/Type/WLT/WorkDayTrackingType.php b/src/Form/Type/WLT/WorkDayTrackingType.php index 46ffb137..7941deb9 100644 --- a/src/Form/Type/WLT/WorkDayTrackingType.php +++ b/src/Form/Type/WLT/WorkDayTrackingType.php @@ -75,9 +75,12 @@ public function addElements( ['disabled' => 'disabled'] : []; }, - 'choice_label' => function (ActivityRealization $ar) use ($lockedActivityRealizations) { + 'choice_label' => function (ActivityRealization $ar) use ($lockedActivityRealizations, $lockManager) { $label = $ar->__toString(); if (in_array($ar, $lockedActivityRealizations, true)) { + if ($lockManager) { + $label = '***' . $label; + } $label .= $this->translator->trans('form.caption.locked', [], 'wlt_tracking'); } return $label; diff --git a/src/Migrations/Version20221229112412.php b/src/Migrations/Version20221229112412.php new file mode 100644 index 00000000..5f158c5a --- /dev/null +++ b/src/Migrations/Version20221229112412.php @@ -0,0 +1,39 @@ +addSql('CREATE TABLE wlt_agreement_activity_realization_comment (id INT AUTO_INCREMENT NOT NULL, agreement_activity_realization_id INT NOT NULL, person_id INT DEFAULT NULL, comment LONGTEXT NOT NULL, timestamp DATETIME NOT NULL, INDEX IDX_DB49388DDEF5E775 (agreement_activity_realization_id), INDEX IDX_DB49388D217BBB47 (person_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE wlt_agreement_activity_realization_comment_audit (id INT NOT NULL, rev INT NOT NULL, agreement_activity_realization_id INT DEFAULT NULL, person_id INT DEFAULT NULL, comment LONGTEXT DEFAULT NULL, timestamp DATETIME DEFAULT NULL, revtype VARCHAR(4) NOT NULL, INDEX rev_935c6eb03972c07b2fc1ea23e44b8f95_idx (rev), PRIMARY KEY(id, rev)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization_comment ADD CONSTRAINT FK_DB49388DDEF5E775 FOREIGN KEY (agreement_activity_realization_id) REFERENCES wlt_agreement_activity_realization (id)'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization_comment ADD CONSTRAINT FK_DB49388D217BBB47 FOREIGN KEY (person_id) REFERENCES person (id)'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization ADD disabled TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization_audit ADD disabled TINYINT(1) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE wlt_agreement_activity_realization_comment'); + $this->addSql('DROP TABLE wlt_agreement_activity_realization_comment_audit'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization DROP disabled'); + $this->addSql('ALTER TABLE wlt_agreement_activity_realization_audit DROP disabled'); + } +} diff --git a/src/Repository/WLT/ActivityRealizationRepository.php b/src/Repository/WLT/ActivityRealizationRepository.php index e42050f1..4ed02b21 100644 --- a/src/Repository/WLT/ActivityRealizationRepository.php +++ b/src/Repository/WLT/ActivityRealizationRepository.php @@ -135,7 +135,8 @@ public function findLockedByAgreement(Agreement $agreement) 'aar.activityRealization = ar' ) ->where('aar.agreement = :agreement') - ->andWhere('aar.grade IS NOT NULL') + ->andWhere('aar.grade IS NOT NULL OR aar.disabled = :disabled') + ->setParameter('disabled', true) ->setParameter('agreement', $agreement) ->getQuery() ->getResult(); diff --git a/src/Repository/WLT/AgreementActivityRealizationCommentRepository.php b/src/Repository/WLT/AgreementActivityRealizationCommentRepository.php new file mode 100644 index 00000000..b51424a8 --- /dev/null +++ b/src/Repository/WLT/AgreementActivityRealizationCommentRepository.php @@ -0,0 +1,31 @@ +createQueryBuilder('aar') ->addSelect('ar') @@ -39,6 +39,8 @@ public function findByAgreementSorted(Agreement $agreement) ->join('aar.activityRealization', 'ar') ->leftJoin('aar.grade', 'gr') ->where('aar.agreement = :agreement') + ->andWhere('aar.disabled = :disabled') + ->setParameter('disabled', false) ->setParameter('agreement', $agreement) ->orderBy('ar.code') ->getQuery() diff --git a/src/Security/WLT/AgreementActivityRealizationCommentVoter.php b/src/Security/WLT/AgreementActivityRealizationCommentVoter.php new file mode 100644 index 00000000..8db6a56d --- /dev/null +++ b/src/Security/WLT/AgreementActivityRealizationCommentVoter.php @@ -0,0 +1,176 @@ +decisionManager = $decisionManager; + $this->userExtensionService = $userExtensionService; + } + + /** + * {@inheritdoc} + */ + protected function supports($attribute, $subject) + { + + if (!$subject instanceof AgreementActivityRealizationComment) { + return false; + } + return in_array($attribute, [ + self::DELETE, + self::ACCESS + ], true); + } + + /** + * {@inheritdoc} + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + if (!$subject instanceof AgreementActivityRealizationComment) { + return false; + } + + // los administradores globales siempre tienen permiso + if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) { + return true; + } + + /** @var Person $user */ + $user = $token->getUser(); + + if (!$user instanceof Person) { + // si el usuario no ha entrado, denegar + return false; + } + + $organization = $this->userExtensionService->getCurrentOrganization(); + + // si es de otra organización, denegar + if ($organization !== $this->userExtensionService->getCurrentOrganization()) { + return false; + } + + // Si es administrador de la organización, permitir siempre + if ($this->decisionManager->decide($token, [OrganizationVoter::MANAGE], $organization)) { + return true; + } + + $person = $user; + + $agreement = $subject->getAgreementActivityRealization()->getAgreement(); + + $isDepartmentHead = false; + $isWltManager = false; + $isGroupTutor = false; + $isTeacher = false; + $academicYearIsCurrent = false; + + // Coordinador de FP dual, autorizado salvo modificar si el acuerdo es de otro curso académico + if ($agreement->getProject()->getManager() === $person) { + $isWltManager = true; + } + + // Tutor laboral y de seguimiento + $isWorkTutor = ($user === $agreement->getWorkTutor() || $user === $agreement->getAdditionalWorkTutor()); + $isEducationalTutor = + ($agreement->getEducationalTutor() && $agreement->getEducationalTutor()->getPerson() === $person) || + ( + $agreement->getAdditionalEducationalTutor() && + $agreement->getAdditionalEducationalTutor()->getPerson() === $person + ); + + + // hay estudiante asociado (puede que no lo haya si es un convenio nuevo) + if ($agreement->getStudentEnrollment()) { + // Si es jefe de su departamento o coordinador de FP dual, permitir acceder siempre + // Jefe del departamento del estudiante, autorizado salvo modificar si el acuerdo es de otro curso académico + $training = $agreement->getStudentEnrollment()->getGroup()->getGrade()->getTraining(); + if ($training && $training->getDepartment() && $training->getDepartment()->getHead() && + $training->getDepartment()->getHead()->getPerson() === $person) { + $isDepartmentHead = true; + } + + // Otros casos: ver qué permisos tiene el usuario + + // Tutor del grupo del acuerdo + $tutors = $agreement->getStudentEnrollment()->getGroup()->getTutors(); + foreach ($tutors as $tutor) { + if ($tutor->getPerson() === $user) { + $isGroupTutor = true; + break; + } + } + + // Docente del grupo del acuerdo + $isTeacher = $this->decisionManager->decide( + $token, + [GroupVoter::TEACH], + $agreement->getStudentEnrollment()->getGroup() + ); + + $academicYearIsCurrent = $organization->getCurrentAcademicYear() === $training->getAcademicYear(); + } + + switch ($attribute) { + case self::DELETE: + // Pueden borrar todos los comentarios el jefe/a de departamento, el coordinador del proyecto, + // el responsable de seguimiento o el autor original del comentario + if ($isDepartmentHead || $isWltManager || $isEducationalTutor || $subject->getPerson() === $person) { + return $academicYearIsCurrent; + } + return false; + + // Si es permiso de acceso, comprobar si es docente del grupo, el tutor de grupo o + // el responsable de seguimiento o laboral + case self::ACCESS: + return $isDepartmentHead || $isWltManager || $isEducationalTutor + || $isTeacher || $isWorkTutor || $isGroupTutor; + } + + // denegamos en cualquier otro caso + return false; + } +} diff --git a/templates/wlt/evaluation/comment_delete.html.twig b/templates/wlt/evaluation/comment_delete.html.twig new file mode 100644 index 00000000..bf5aae44 --- /dev/null +++ b/templates/wlt/evaluation/comment_delete.html.twig @@ -0,0 +1,15 @@ +{% trans_default_domain 'wlt_agreement_activity_realization' %} +{% extends 'layout.html.twig' %} +{% import 'macros.html.twig' as m %} +{% block content %} +
+{% endblock %} diff --git a/templates/wlt/evaluation/comment_form.html.twig b/templates/wlt/evaluation/comment_form.html.twig new file mode 100644 index 00000000..819cb48c --- /dev/null +++ b/templates/wlt/evaluation/comment_form.html.twig @@ -0,0 +1,33 @@ +{% trans_default_domain 'wlt_agreement_activity_realization' %} +{% extends 'layout.html.twig' %} +{% import 'macros.html.twig' as m %} +{% block content %} +