diff --git a/assets/vue/services/linkService.js b/assets/vue/services/linkService.js index d85e1589c68..9f0356a355a 100644 --- a/assets/vue/services/linkService.js +++ b/assets/vue/services/linkService.js @@ -68,8 +68,8 @@ export default { */ toggleLinkVisibility: async (linkId, visible, cid, sid) => { const endpoint = `${ENTRYPOINT}links/${linkId}/toggle_visibility?cid=${cid}&sid=${sid}` - - return baseService.put(endpoint, { visible }) + const response = await axios.put(endpoint, { visible }) + return response.data }, /** diff --git a/assets/vue/views/links/LinksList.vue b/assets/vue/views/links/LinksList.vue index 9df453c33b3..6082c2e375b 100644 --- a/assets/vue/views/links/LinksList.vue +++ b/assets/vue/views/links/LinksList.vue @@ -217,6 +217,7 @@ const categoryToDelete = ref(null) const isLoading = ref(true) const linkValidationResults = ref({}) +const isToggling = ref({}) onMounted(async () => { isAllowedToEdit.value = await checkIsAllowedToEdit(true, true, true) @@ -266,17 +267,28 @@ async function checkLink(id, url) { } async function toggleVisibility(link) { + if (isToggling.value[link.iid]) return + isToggling.value = { ...isToggling.value, [link.iid]: true } + try { - const visibility = toggleVisibilityProperty(!link.linkVisible) - let newLink = await linkService.toggleLinkVisibility(link.iid, isVisible(visibility), cid, sid) - notifications.showSuccessNotification(t("Link visibility updated")) - Object.values(categories.value) - .map((c) => c.links) - .flat() + const newVisible = !isVisible(link.linkVisible) + const updatedLink = await linkService.toggleLinkVisibility(link.iid, newVisible, cid, sid) + const newFlagValue = visibilityFromBoolean(updatedLink.linkVisible) + + linksWithoutCategory.value .filter((l) => l.iid === link.iid) - .forEach((l) => (l.linkVisible = visibilityFromBoolean(newLink.linkVisible))) - } catch (error) { + .forEach((l) => (l.linkVisible = newFlagValue)) + + categories.value + .flatMap((c) => c.links || []) + .filter((l) => l.iid === link.iid) + .forEach((l) => (l.linkVisible = newFlagValue)) + + notifications.showSuccessNotification(t("Link visibility updated")) + } catch (err) { notifications.showErrorNotification(t("Could not change visibility of link")) + } finally { + isToggling.value = { ...isToggling.value, [link.iid]: false } } } diff --git a/public/main/exercise/exercise.class.php b/public/main/exercise/exercise.class.php index b35c32e044b..00d306244fc 100644 --- a/public/main/exercise/exercise.class.php +++ b/public/main/exercise/exercise.class.php @@ -10,6 +10,7 @@ use Chamilo\CoreBundle\Entity\TrackEHotspot; use Chamilo\CoreBundle\Framework\Container; use Chamilo\CoreBundle\Repository\ResourceLinkRepository; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Chamilo\CourseBundle\Entity\CQuizCategory; use Chamilo\CourseBundle\Entity\CQuiz; use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory; @@ -1851,6 +1852,21 @@ public function delete() GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']); } + // Register resource deletion manually because this is a soft delete (active = -1) + // and Doctrine does not trigger postRemove in this case. + /* @var TrackEDefaultRepository $trackRepo */ + $trackRepo = Container::$container->get(TrackEDefaultRepository::class); + $resourceNode = $exercise->getResourceNode(); + if ($resourceNode) { + $trackRepo->registerResourceEvent( + $resourceNode, + 'deletion', + api_get_user_id(), + api_get_course_int_id(), + api_get_session_id() + ); + } + return true; } diff --git a/public/main/forum/forumfunction.inc.php b/public/main/forum/forumfunction.inc.php index 16d25872411..ee748a08798 100644 --- a/public/main/forum/forumfunction.inc.php +++ b/public/main/forum/forumfunction.inc.php @@ -9,6 +9,7 @@ use Chamilo\CoreBundle\Entity\Session as SessionEntity; use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\Framework\Container; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Chamilo\CourseBundle\Entity\CForum; use Chamilo\CourseBundle\Entity\CForumAttachment; use Chamilo\CourseBundle\Entity\CForumCategory; @@ -131,7 +132,7 @@ function handleForum($url) if ('visible' === $action) { $repo->setVisibilityPublished($resource, $course, $session); } else { - $repo->setVisibilityPending($resource); + $repo->setVisibilityPending($resource, $course, $session); } if ('visible' === $action) { @@ -149,6 +150,13 @@ function handleForum($url) if ($resource) { $linksRepo->removeByResourceInContext($resource, $course, $session); + // Manually register thread deletion event because the resource is not removed via Doctrine + $trackRepo = Container::$container->get(TrackEDefaultRepository::class); + $node = $resource->getResourceNode(); + if ($node) { + $trackRepo->registerResourceEvent($node, 'deletion', api_get_user_id(), api_get_course_int_id(), api_get_session_id()); + } + Display::addFlash( Display::return_message(get_lang('Forum category deleted'), 'confirmation', false) ); @@ -160,6 +168,13 @@ function handleForum($url) if ($resource) { $linksRepo->removeByResourceInContext($resource, $course, $session); + // Register forum deletion manually as it's not deleted via Doctrine + $trackRepo = Container::$container->get(TrackEDefaultRepository::class); + $node = $resource->getResourceNode(); + if ($node) { + $trackRepo->registerResourceEvent($node, 'deletion', api_get_user_id(), api_get_course_int_id(), api_get_session_id()); + } + Display::addFlash(Display::return_message(get_lang('Forum deleted'), 'confirmation', false)); } @@ -183,6 +198,14 @@ function handleForum($url) $link_id = $link_info['id']; GradebookUtils::remove_resource_from_course_gradebook($link_id); } + + // Manually register thread deletion event because the resource is not removed via Doctrine + $trackRepo = Container::$container->get(TrackEDefaultRepository::class); + $node = $resource->getResourceNode(); + if ($node) { + $trackRepo->registerResourceEvent($node, 'deletion', api_get_user_id(), api_get_course_int_id(), api_get_session_id()); + } + Display::addFlash(Display::return_message(get_lang('Thread deleted'), 'confirmation', false)); } diff --git a/public/main/lp/learnpath.class.php b/public/main/lp/learnpath.class.php index f02dacd522e..41eea1be9fe 100644 --- a/public/main/lp/learnpath.class.php +++ b/public/main/lp/learnpath.class.php @@ -9,6 +9,7 @@ use Chamilo\CoreBundle\Entity\Session as SessionEntity; use Chamilo\CoreBundle\Event\Events; use Chamilo\CoreBundle\Event\LearningPathEndedEvent; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Chamilo\CoreBundle\ServiceHelper\ThemeHelper; use Chamilo\CourseBundle\Entity\CLpRelUser; use Chamilo\CoreBundle\Framework\Container; @@ -793,33 +794,6 @@ public function delete($courseInfo = null, $id = null, $delete = 'keep') $course = api_get_course_entity(); $session = api_get_session_entity(); - - //$lp_item = Database::get_course_table(TABLE_LP_ITEM); - //$lp_view = Database::get_course_table(TABLE_LP_VIEW); - //$lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW); - - // Delete lp item id. - //foreach ($this->items as $lpItemId => $dummy) { - // $sql = "DELETE FROM $lp_item_view - // WHERE lp_item_id = '".$lpItemId."'"; - // Database::query($sql); - //} - - // Proposed by Christophe (nickname: clefevre) - //$sql = "DELETE FROM $lp_item - // WHERE lp_id = ".$this->lp_id; - //Database::query($sql); - - //$sql = "DELETE FROM $lp_view - // WHERE lp_id = ".$this->lp_id; - //Database::query($sql); - - //$table = Database::get_course_table(TABLE_LP_REL_USERGROUP); - //$sql = "DELETE FROM $table - // WHERE - // lp_id = {$this->lp_id}"; - //Database::query($sql); - $lp = Container::getLpRepository()->find($this->lp_id); Database::getManager() @@ -837,9 +811,17 @@ public function delete($courseInfo = null, $id = null, $delete = 'keep') GradebookUtils::remove_resource_from_course_gradebook($link_info['id']); } - //if ('true' === api_get_setting('search_enabled')) { - // delete_all_values_for_item($this->cc, TOOL_LEARNPATH, $this->lp_id); - //} + $trackRepo = Container::$container->get(TrackEDefaultRepository::class); + $resourceNode = $lp->getResourceNode(); + if ($resourceNode) { + $trackRepo->registerResourceEvent( + $resourceNode, + 'deletion', + api_get_user_id(), + api_get_course_int_id(), + api_get_session_id() + ); + } } /** diff --git a/src/CoreBundle/Controller/Api/UpdateDocumentFileAction.php b/src/CoreBundle/Controller/Api/UpdateDocumentFileAction.php index 7f4584bcd2a..31c820a31d3 100644 --- a/src/CoreBundle/Controller/Api/UpdateDocumentFileAction.php +++ b/src/CoreBundle/Controller/Api/UpdateDocumentFileAction.php @@ -6,17 +6,33 @@ namespace Chamilo\CoreBundle\Controller\Api; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Chamilo\CourseBundle\Entity\CDocument; use Chamilo\CourseBundle\Repository\CDocumentRepository; use Doctrine\ORM\EntityManager; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; class UpdateDocumentFileAction extends BaseResourceFileAction { + public function __construct( + private TrackEDefaultRepository $trackRepo, + private Security $security + ) {} + public function __invoke(CDocument $document, Request $request, CDocumentRepository $repo, EntityManager $em): CDocument { $this->handleUpdateRequest($document, $repo, $request, $em); + $node = $document->getResourceNode(); + if ($node) { + $this->trackRepo->registerResourceEvent( + $node, + 'edition', + $this->security->getUser()?->getId() + ); + } + return $document; } } diff --git a/src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php b/src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php index f9c2d721e25..eb9e15b7419 100644 --- a/src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php +++ b/src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php @@ -6,26 +6,37 @@ namespace Chamilo\CoreBundle\Controller\Api; +use Chamilo\CoreBundle\Entity\Course; +use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\ServiceHelper\CidReqHelper; use Chamilo\CourseBundle\Entity\CDocument; use Chamilo\CourseBundle\Repository\CDocumentRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpKernel\Attribute\AsController; +use Doctrine\ORM\EntityManagerInterface; #[AsController] class UpdateVisibilityDocument extends AbstractController { public function __construct( private readonly CidReqHelper $cidReqHelper, + private readonly EntityManagerInterface $em, ) {} public function __invoke(CDocument $document, CDocumentRepository $repo): CDocument { - $repo->toggleVisibilityPublishedDraft( - $document, - $this->cidReqHelper->getCourseEntity(), - $this->cidReqHelper->getSessionEntity() - ); + $course = $this->cidReqHelper->getCourseEntity(); + $session = $this->cidReqHelper->getSessionEntity(); + + if ($course) { + $course = $this->em->getRepository(Course::class)->find($course->getId()); + } + + if ($session) { + $session = $this->em->getRepository(Session::class)->find($session->getId()); + } + + $repo->toggleVisibilityPublishedDraft($document, $course, $session); return $document; } diff --git a/src/CoreBundle/Controller/Api/UpdateVisibilityLink.php b/src/CoreBundle/Controller/Api/UpdateVisibilityLink.php index 9d670a43206..9b13c499482 100644 --- a/src/CoreBundle/Controller/Api/UpdateVisibilityLink.php +++ b/src/CoreBundle/Controller/Api/UpdateVisibilityLink.php @@ -23,8 +23,8 @@ public function __invoke(CLink $link, CLinkRepository $repo): CLink { $repo->toggleVisibilityPublishedDraft( $link, - $this->cidReqHelper->getCourseEntity(), - $this->cidReqHelper->getSessionEntity() + $this->cidReqHelper->getDoctrineCourseEntity(), + $this->cidReqHelper->getDoctrineSessionEntity() ); $link->toggleVisibility(); diff --git a/src/CoreBundle/Controller/Api/UpdateVisibilityLinkCategory.php b/src/CoreBundle/Controller/Api/UpdateVisibilityLinkCategory.php index 0d3c7181fa5..1c44a00f07a 100644 --- a/src/CoreBundle/Controller/Api/UpdateVisibilityLinkCategory.php +++ b/src/CoreBundle/Controller/Api/UpdateVisibilityLinkCategory.php @@ -23,8 +23,8 @@ public function __invoke(CLinkCategory $linkCategory, CLinkCategoryRepository $r { $repo->toggleVisibilityPublishedDraft( $linkCategory, - $this->cidReqHelper->getCourseEntity(), - $this->cidReqHelper->getSessionEntity() + $this->cidReqHelper->getDoctrineCourseEntity(), + $this->cidReqHelper->getDoctrineSessionEntity() ); $linkCategory->toggleVisibility(); diff --git a/src/CoreBundle/Entity/Listener/ResourceLinkListener.php b/src/CoreBundle/Entity/Listener/ResourceLinkListener.php index 2d61950fd43..813a3a59b18 100644 --- a/src/CoreBundle/Entity/Listener/ResourceLinkListener.php +++ b/src/CoreBundle/Entity/Listener/ResourceLinkListener.php @@ -7,12 +7,33 @@ namespace Chamilo\CoreBundle\Entity\Listener; use Chamilo\CoreBundle\Entity\ResourceLink; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Doctrine\ORM\Event\PostRemoveEventArgs; +use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Exception\ORMException; use Event; +use Symfony\Bundle\SecurityBundle\Security; class ResourceLinkListener { + public function __construct( + protected Security $security, + protected TrackEDefaultRepository $trackEDefaultRepository + ) {} + + public function postUpdate(ResourceLink $resourceLink, PostUpdateEventArgs $event): void + { + $changeSet = $event->getObjectManager()->getUnitOfWork()->getEntityChangeSet($resourceLink); + + if (isset($changeSet['visibility'])) { + $this->trackEDefaultRepository->registerResourceEvent( + $resourceLink->getResourceNode(), + 'visibility_change', + $this->security->getUser()?->getId() + ); + } + } + /** * @throws ORMException */ diff --git a/src/CoreBundle/Entity/Listener/ResourceListener.php b/src/CoreBundle/Entity/Listener/ResourceListener.php index 447e67b1afb..08d180dc81a 100644 --- a/src/CoreBundle/Entity/Listener/ResourceListener.php +++ b/src/CoreBundle/Entity/Listener/ResourceListener.php @@ -19,10 +19,13 @@ use Chamilo\CoreBundle\Entity\ResourceType; use Chamilo\CoreBundle\Entity\ResourceWithAccessUrlInterface; use Chamilo\CoreBundle\Entity\User; +use Chamilo\CoreBundle\Repository\TrackEDefaultRepository; use Chamilo\CoreBundle\Tool\ToolChain; use Chamilo\CoreBundle\Traits\AccessUrlListenerTrait; use Chamilo\CourseBundle\Entity\CCalendarEvent; use Cocur\Slugify\SlugifyInterface; +use Doctrine\ORM\Event\PostPersistEventArgs; +use Doctrine\ORM\Event\PostRemoveEventArgs; use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Event\PrePersistEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; @@ -44,7 +47,8 @@ public function __construct( protected SlugifyInterface $slugify, protected ToolChain $toolChain, protected RequestStack $request, - protected Security $security + protected Security $security, + protected TrackEDefaultRepository $trackEDefaultRepository ) {} /** @@ -106,6 +110,9 @@ public function prePersist(AbstractResource $resource, PrePersistEventArgs $even $entityClass = $eventArgs->getObject()::class; $name = $this->toolChain->getResourceTypeNameByEntity($entityClass); + if (empty($name)) { + return; + } $resourceType = $resourceTypeRepo->findOneBy([ 'title' => $name, @@ -265,6 +272,45 @@ public function prePersist(AbstractResource $resource, PrePersistEventArgs $even } } + public function postPersist(AbstractResource $resource, PostPersistEventArgs $event): void + { + $resourceNode = $resource->getResourceNode(); + + if ($resourceNode) { + $this->trackEDefaultRepository->registerResourceEvent( + $resourceNode, + 'creation', + $this->security->getUser()?->getId() + ); + } + } + + public function postUpdate(AbstractResource $resource, PostUpdateEventArgs $event): void + { + $resourceNode = $resource->getResourceNode(); + + if ($resourceNode) { + $this->trackEDefaultRepository->registerResourceEvent( + $resourceNode, + 'edition', + $this->security->getUser()?->getId() + ); + } + } + + public function postRemove(AbstractResource $resource, PostRemoveEventArgs $event): void + { + $resourceNode = $resource->getResourceNode(); + + if ($resourceNode) { + $this->trackEDefaultRepository->registerResourceEvent( + $resourceNode, + 'deletion', + $this->security->getUser()?->getId() + ); + } + } + /** * When updating a Resource. */ @@ -281,13 +327,6 @@ public function preUpdate(AbstractResource $resource, PreUpdateEventArgs $eventA // $this->setLinks($resource, $eventArgs->getEntityManager()); } - public function postUpdate(AbstractResource $resource, PostUpdateEventArgs $eventArgs): void - { - // error_log('resource listener postUpdate'); - // $em = $eventArgs->getEntityManager(); - // $this->updateResourceName($resource, $resource->getResourceName(), $em); - } - public function updateResourceName(AbstractResource $resource): void { $resourceName = $resource->getResourceName(); diff --git a/src/CoreBundle/Repository/TrackEDefaultRepository.php b/src/CoreBundle/Repository/TrackEDefaultRepository.php index 9ab7d588480..308b41ce976 100644 --- a/src/CoreBundle/Repository/TrackEDefaultRepository.php +++ b/src/CoreBundle/Repository/TrackEDefaultRepository.php @@ -6,6 +6,7 @@ namespace Chamilo\CoreBundle\Repository; +use Chamilo\CoreBundle\Entity\ResourceNode; use Chamilo\CoreBundle\Entity\TrackEDefault; use Chamilo\CoreBundle\Entity\ValidationToken; use DateTime; @@ -13,10 +14,11 @@ use Doctrine\Persistence\ManagerRegistry; use Exception; use RuntimeException; +use Symfony\Bundle\SecurityBundle\Security; class TrackEDefaultRepository extends ServiceEntityRepository { - public function __construct(ManagerRegistry $registry) + public function __construct(ManagerRegistry $registry, private readonly Security $security) { parent::__construct($registry, TrackEDefault::class); } @@ -102,4 +104,50 @@ public function registerTicketUnsubscribeEvent(int $ticketId, int $userId): void $this->_em->persist($event); $this->_em->flush(); } + + public function registerResourceEvent( + ResourceNode $resourceNode, + string $eventType, + ?int $userId = null, + ?int $courseId = null, + ?int $sessionId = null + ): void { + if (!$userId) { + $user = $this->security->getUser(); + if ($user && method_exists($user, 'getId')) { + $userId = $user->getId(); + } + } + + if (null === $courseId || null === $sessionId) { + $link = $resourceNode->getResourceLinks()->first(); + if (!$link) { + return; + } + + if (null === $courseId && $link->getCourse()) { + $courseId = $link->getCourse()->getId(); + } + if (null === $sessionId && $link->getSession()) { + $sessionId = $link->getSession()->getId(); + } + } + + $resourceTypeTitle = $resourceNode->getResourceType()?->getTitle(); + if (null === $resourceTypeTitle) { + $resourceTypeTitle = $resourceNode ? (new \ReflectionClass($resourceNode))->getShortName() : 'undefined'; + } + + $event = new TrackEDefault(); + $event->setDefaultUserId($userId ?? 0); + $event->setCId($courseId); + $event->setSessionId($sessionId); + $event->setDefaultDate(new \DateTime()); + $event->setDefaultEventType($eventType); + $event->setDefaultValueType('resource_type_' . $resourceTypeTitle); + $event->setDefaultValue((string) $resourceNode->getId()); + + $this->_em->persist($event); + $this->_em->flush(); + } } diff --git a/src/CoreBundle/Resources/config/listeners.yml b/src/CoreBundle/Resources/config/listeners.yml index 8e2f32ab75d..3cf182b4ec4 100644 --- a/src/CoreBundle/Resources/config/listeners.yml +++ b/src/CoreBundle/Resources/config/listeners.yml @@ -108,7 +108,9 @@ services: Chamilo\CoreBundle\EventListener\MessageStatusListener: ~ - Chamilo\CoreBundle\EventListener\ResourceLinkListener: ~ + Chamilo\CoreBundle\Entity\Listener\ResourceLinkListener: + tags: + - { name: doctrine.orm.entity_listener, entity_manager: default, lazy: true } Chamilo\CoreBundle\EventListener\UserRelCourseVoteListener: tags: diff --git a/src/CoreBundle/ServiceHelper/CidReqHelper.php b/src/CoreBundle/ServiceHelper/CidReqHelper.php index f29e16283c0..7d130220906 100644 --- a/src/CoreBundle/ServiceHelper/CidReqHelper.php +++ b/src/CoreBundle/ServiceHelper/CidReqHelper.php @@ -12,6 +12,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Doctrine\ORM\EntityManagerInterface; /** * @see CidReqListener::onKernelRequest() @@ -20,6 +21,7 @@ class CidReqHelper { public function __construct( private readonly RequestStack $requestStack, + private readonly EntityManagerInterface $em, ) {} private function getRequest(): ?Request @@ -62,4 +64,24 @@ public function getGroupId(): ?int $session = $this->getSessionHandler(); return $session?->get('gid'); } + + public function getDoctrineCourseEntity(): ?Course + { + $courseId = $this->getCourseId(); + if (empty($courseId)) { + return null; + } + + return $this->em->getRepository(Course::class)->find((int) $courseId); + } + + public function getDoctrineSessionEntity(): ?Session + { + $sessionId = $this->getSessionId(); + if (empty($sessionId)) { + return null; + } + + return $this->em->getRepository(Session::class)->find((int) $sessionId); + } }