From d2777cc7c09516b154fe7f59b4302c4e38747c10 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:13:06 +0200 Subject: [PATCH 1/9] Fix: Add DocumentUsageAction API controller for course document usage tracking --- .../Controller/Api/DocumentUsageAction.php | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/CoreBundle/Controller/Api/DocumentUsageAction.php diff --git a/src/CoreBundle/Controller/Api/DocumentUsageAction.php b/src/CoreBundle/Controller/Api/DocumentUsageAction.php new file mode 100644 index 00000000000..083215d3b6f --- /dev/null +++ b/src/CoreBundle/Controller/Api/DocumentUsageAction.php @@ -0,0 +1,176 @@ +courseRepository->find($course_id); + if (!$course_entity) { + return new JsonResponse(['error' => 'Course not found'], 404); + } + + $session_entity = api_get_session_entity(); + + $total_quota_bytes = ($course_entity->getDiskQuota() * 1024 * 1024) ?? DEFAULT_DOCUMENT_QUOTA; + $used_quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity); + + $chartData = []; + + // Process sessions + $this->processCourseSessions($course_entity, $session_id, $total_quota_bytes, $used_quota_bytes, $chartData); + + // Process groups + $this->processCourseGroups($course_entity, $group_id, $total_quota_bytes, $used_quota_bytes, $chartData); + + // Process user documents + $users = $this->courseRepository->getUsersByCourse($course_entity); + foreach ($users as $user) { + $user_id = $user->getId(); + $user_name = $user->getFullName(); + $this->processUserDocuments($course_entity, $session_entity, $user_id, $user_name, $total_quota_bytes, $chartData); + } + + // Add available space + $available_bytes = $total_quota_bytes - $used_quota_bytes; + $available_percentage = $this->calculatePercentage($available_bytes, $total_quota_bytes); + + $chartData[] = [ + 'label' => addslashes(get_lang('Available space')) . ' (' . format_file_size($available_bytes) . ')', + 'percentage' => $available_percentage, + ]; + + return new JsonResponse([ + 'datasets' => [ + ['data' => array_column($chartData, 'percentage')], + ], + 'labels' => array_column($chartData, 'label'), + ]); + } + + private function processCourseSessions($course_entity, int $session_id, int $total_quota_bytes, int &$used_quota_bytes, array &$chartData): void + { + $sessions = $this->sessionRepository->getSessionsByCourse($course_entity); + + foreach ($sessions as $session) { + $quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity, null, $session); + + if ($quota_bytes > 0) { + $session_name = $session->getTitle(); + if ($session_id === $session->getId()) { + $session_name .= ' * '; + } + + $used_quota_bytes += $quota_bytes; + $chartData[] = [ + 'label' => addslashes(get_lang('Session') . ': ' . $session_name) . ' (' . format_file_size($quota_bytes) . ')', + 'percentage' => $this->calculatePercentage($quota_bytes, $total_quota_bytes), + ]; + } + } + } + + private function processCourseGroups($course_entity, int $group_id, int $total_quota_bytes, int &$used_quota_bytes, array &$chartData): void + { + $groups_list = $this->groupRepository->findAllByCourse($course_entity)->getQuery()->getResult(); + + foreach ($groups_list as $group_entity) { + $quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity, $group_entity->getIid()); + + if ($quota_bytes > 0) { + $group_name = $group_entity->getTitle(); + if ($group_id === $group_entity->getIid()) { + $group_name .= ' * '; + } + + $used_quota_bytes += $quota_bytes; + $chartData[] = [ + 'label' => addslashes(get_lang('Group') . ': ' . $group_name) . ' (' . format_file_size($quota_bytes) . ')', + 'percentage' => $this->calculatePercentage($quota_bytes, $total_quota_bytes), + ]; + } + } + } + + private function processUserDocuments($course_entity, $session_entity, int $user_id, string $user_name, int $total_quota_bytes, array &$chartData): void + { + $documents_list = $this->documentRepository->getAllDocumentDataByUserAndGroup($course_entity); + $user_quota_bytes = 0; + + foreach ($documents_list as $document_entity) { + if ($document_entity->getResourceNode()->getCreator()?->getId() === $user_id + && $document_entity->getFiletype() === 'file') { + $resourceFiles = $document_entity->getResourceNode()->getResourceFiles(); + if (!$resourceFiles->isEmpty()) { + $user_quota_bytes += $resourceFiles->first()->getSize(); + } + } + } + + if ($user_quota_bytes > 0) { + $chartData[] = [ + 'label' => addslashes(get_lang('Teacher') . ': ' . $user_name) . ' (' . format_file_size($user_quota_bytes) . ')', + 'percentage' => $this->calculatePercentage($user_quota_bytes, $total_quota_bytes), + ]; + + // Handle session context + if ($session_entity) { + $session_total_quota = $this->calculateSessionTotalQuota($session_entity); + if ($session_total_quota > 0) { + $chartData[] = [ + 'label' => addslashes(sprintf(get_lang('TeacherXInSession'), $user_name)), + 'percentage' => $this->calculatePercentage($user_quota_bytes, $session_total_quota), + ]; + } + } + } + } + + private function calculateSessionTotalQuota($session_entity): int + { + $total = 0; + $sessionCourses = $session_entity->getCourses(); + + foreach ($sessionCourses as $courseEntity) { + $total += DocumentManager::get_course_quota($courseEntity->getId()); + } + + return $total; + } + + private function calculatePercentage(int $bytes, int $total_bytes): float + { + if ($total_bytes === 0) { + return 0.0; + } + + return round(($bytes / $total_bytes) * 100, 2); + } +} From 236c64febe0d2e113a7389dde265ecdcc79740d8 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:13:32 +0200 Subject: [PATCH 2/9] Fix: Add API endpoint for the usage controller --- src/CourseBundle/Entity/CDocument.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CourseBundle/Entity/CDocument.php b/src/CourseBundle/Entity/CDocument.php index 03c7d29f15d..14392463208 100644 --- a/src/CourseBundle/Entity/CDocument.php +++ b/src/CourseBundle/Entity/CDocument.php @@ -19,6 +19,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter; use Chamilo\CoreBundle\Controller\Api\CreateDocumentFileAction; use Chamilo\CoreBundle\Controller\Api\DocumentLearningPathUsageAction; +use Chamilo\CoreBundle\Controller\Api\DocumentUsageAction; use Chamilo\CoreBundle\Controller\Api\DownloadSelectedDocumentsAction; use Chamilo\CoreBundle\Controller\Api\ReplaceDocumentFileAction; use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction; @@ -190,6 +191,16 @@ ], ] ), + new Get( + uriTemplate: '/documents/{cid}/usage', + controller: DocumentUsageAction::class, + openapiContext: [ + 'summary' => 'Get usage/quota information for documents.', + ], + security: "is_granted('ROLE_USER')", + read: false, + name: 'api_documents_usage' + ) ], normalizationContext: [ 'groups' => ['document:read', 'resource_node:read'], From 65a66f222b38828bf8b5404ac2d6e8e9d18b950d Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:35:55 +0200 Subject: [PATCH 3/9] Minor: center the chart --- assets/vue/components/basecomponents/BaseChart.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/vue/components/basecomponents/BaseChart.vue b/assets/vue/components/basecomponents/BaseChart.vue index 4a2d46d4d0f..84f5c65c88a 100644 --- a/assets/vue/components/basecomponents/BaseChart.vue +++ b/assets/vue/components/basecomponents/BaseChart.vue @@ -2,7 +2,7 @@ From 436678991c179cdf6131fc7de7f62414acebc1d3 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:36:25 +0200 Subject: [PATCH 4/9] Fix: Ask for data chart --- assets/vue/views/documents/DocumentsList.vue | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/assets/vue/views/documents/DocumentsList.vue b/assets/vue/views/documents/DocumentsList.vue index 639998f60e7..1a052f471da 100644 --- a/assets/vue/views/documents/DocumentsList.vue +++ b/assets/vue/views/documents/DocumentsList.vue @@ -392,7 +392,6 @@ :style="{ width: '28rem' }" :title="t('Space available')" > -

This feature is in development, this is a mockup with placeholder data!

@@ -947,10 +946,32 @@ function showSlideShowWithFirstImage() { document.querySelector('button.fancybox-button--play')?.click() } -function showUsageDialog() { - usageData.value = { - datasets: [{ data: [83, 14, 5] }], - labels: ["Course", "Teacher", "Available space"], +async function showUsageDialog() { + try { + const response = await axios.get(`/api/documents/${cid}/usage`, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }) + + usageData.value = response.data + } catch (error) { + console.error("Error fetching storage usage:", error) + + usageData.value = { + datasets: [{ + data: [100], + backgroundColor: ['#CCCCCC', '#CCCCCC', '#CCCCCC'], + borderWidth: 2, + borderColor: '#E0E0E0' + }], + labels: [ + t('Course storage (unavailable)'), + t('Teacher storage (unavailable)'), + t('Total storage (unavailable)') + ], + } } isFileUsageDialogVisible.value = true } From f260fa31d1bedf486f42160433e4208b00a0c229 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:36:51 +0200 Subject: [PATCH 5/9] Fix: Add method to retrieve sessions by course --- src/CoreBundle/Repository/SessionRepository.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index 61a1a049e03..38532491b87 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -86,6 +86,17 @@ public function getSessionsByUser(User $user, AccessUrl $url): QueryBuilder return $qb; } + public function getSessionsByCourse(Course $course): array + { + $qb = $this->createQueryBuilder('s'); + + return $qb + ->innerJoin('s.courses', 'src') + ->where($qb->expr()->eq('src.course', ':course')) + ->setParameter('course', $course) + ->getQuery()->getResult(); + } + /** * @return array * From c002a1c6769c7be10bf4f656d3303a9c5aa42eb1 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:37:13 +0200 Subject: [PATCH 6/9] Fix: Add method to retrieve users by course --- .../Repository/Node/CourseRepository.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/CoreBundle/Repository/Node/CourseRepository.php b/src/CoreBundle/Repository/Node/CourseRepository.php index 4bff028b532..02e93199e04 100644 --- a/src/CoreBundle/Repository/Node/CourseRepository.php +++ b/src/CoreBundle/Repository/Node/CourseRepository.php @@ -449,4 +449,23 @@ public function getCoursesByAccessUrl(AccessUrl $url): array ->getResult() ; } + + public function getUsersByCourse(Course $course): array + { + $qb = $this->getEntityManager()->createQueryBuilder(); + + $qb + ->select('DISTINCT user') + ->from(User::class, 'user') + ->innerJoin(CourseRelUser::class, 'courseRelUser', Join::WITH, 'courseRelUser.user = user.id') + ->where('courseRelUser.course = :course') + ->setParameter('course', $course) + ->orderBy('user.lastname', 'ASC') + ->addOrderBy('user.firstname', 'ASC') + ; + + $query = $qb->getQuery(); + + return $query->getResult(); + } } From 86362285fd0c0f044c5c79288c8a47166fb56db7 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:38:58 +0200 Subject: [PATCH 7/9] Fix: Refactor old version of getAllDocumentDataByUserAndGroup and add it to the repository --- .../Repository/CDocumentRepository.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/CourseBundle/Repository/CDocumentRepository.php b/src/CourseBundle/Repository/CDocumentRepository.php index 8149ea5733c..3d83ce0e9b8 100644 --- a/src/CourseBundle/Repository/CDocumentRepository.php +++ b/src/CourseBundle/Repository/CDocumentRepository.php @@ -366,6 +366,86 @@ public function findChildNodeByTitle(ResourceNode $parent, string $title): ?Reso ; } + /** + * Fetches all document data for the given user/group using Doctrine ORM. + * + * @param Course $course + * @param string $path + * @param int $toGroupId + * @param int|null $toUserId + * @param bool $search + * @param Session|null $session + * + * @return CDocument[] + */ + public function getAllDocumentDataByUserAndGroup( + Course $course, + string $path = '/', + int $toGroupId = 0, + ?int $toUserId = null, + bool $search = false, + ?Session $session = null + ): array { + $qb = $this->createQueryBuilder('d'); + + $qb->innerJoin('d.resourceNode', 'rn') + ->innerJoin('rn.resourceLinks', 'rl') + ->where('rl.course = :course') + ->setParameter('course', $course); + + // Session filtering + if ($session) { + $qb->andWhere('(rl.session = :session OR rl.session IS NULL)') + ->setParameter('session', $session); + } else { + $qb->andWhere('rl.session IS NULL'); + } + + // Path filtering - convert document.lib.php logic to Doctrine + if ($path !== '/') { + // The original uses LIKE with path patterns + $pathPattern = rtrim($path, '/') . '/%'; + $qb->andWhere('rn.title LIKE :pathPattern OR rn.title = :exactPath') + ->setParameter('pathPattern', $pathPattern) + ->setParameter('exactPath', ltrim($path, '/')); + + // Exclude deeper nested paths if not searching + if (!$search) { + // Exclude paths with additional slashes beyond the current level + $excludePattern = rtrim($path, '/') . '/%/%'; + $qb->andWhere('rn.title NOT LIKE :excludePattern') + ->setParameter('excludePattern', $excludePattern); + } + } + + // User/Group filtering + if ($toUserId !== null) { + if ($toUserId > 0) { + $qb->andWhere('rl.user = :userId') + ->setParameter('userId', $toUserId); + } else { + $qb->andWhere('rl.user IS NULL'); + } + } else { + if ($toGroupId > 0) { + $qb->andWhere('rl.group = :groupId') + ->setParameter('groupId', $toGroupId); + } else { + $qb->andWhere('rl.group IS NULL'); + } + } + + // Exclude deleted documents (like %_DELETED_% in original) + $qb->andWhere('rn.title NOT LIKE :deletedPattern') + ->setParameter('deletedPattern', '%_DELETED_%'); + + // Order by creation date (equivalent to last.iid DESC) + $qb->orderBy('rn.createdAt', 'DESC') + ->addOrderBy('rn.id', 'DESC'); + + return $qb->getQuery()->getResult(); + } + /** * Ensure "Learning paths" exists directly under the course resource node. * Links are created for course (and optional session) context. From 7270c699956ab18e6638284d9a935f5413b9dc9a Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 15:53:08 +0200 Subject: [PATCH 8/9] Minor : Refactor variable usin camelCase --- .../Controller/Api/DocumentUsageAction.php | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/CoreBundle/Controller/Api/DocumentUsageAction.php b/src/CoreBundle/Controller/Api/DocumentUsageAction.php index 083215d3b6f..c04b35d63e1 100644 --- a/src/CoreBundle/Controller/Api/DocumentUsageAction.php +++ b/src/CoreBundle/Controller/Api/DocumentUsageAction.php @@ -28,43 +28,43 @@ public function __construct( public function __invoke($cid): JsonResponse { - $course_id = (int) $cid; - $session_id = api_get_session_id(); - $group_id = api_get_group_id(); + $courseId = (int) $cid; + $sessionId = api_get_session_id(); + $groupId = api_get_group_id(); - $course_entity = $this->courseRepository->find($course_id); - if (!$course_entity) { + $courseEntity = $this->courseRepository->find($courseId); + if (!$courseEntity) { return new JsonResponse(['error' => 'Course not found'], 404); } - $session_entity = api_get_session_entity(); + $sessionEntity = api_get_session_entity(); - $total_quota_bytes = ($course_entity->getDiskQuota() * 1024 * 1024) ?? DEFAULT_DOCUMENT_QUOTA; - $used_quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity); + $totalQuotaBytes = ($courseEntity->getDiskQuota() * 1024 * 1024) ?? DEFAULT_DOCUMENT_QUOTA; + $usedQuotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity); $chartData = []; // Process sessions - $this->processCourseSessions($course_entity, $session_id, $total_quota_bytes, $used_quota_bytes, $chartData); + $this->processCourseSessions($courseEntity, $sessionId, $totalQuotaBytes, $usedQuotaBytes, $chartData); // Process groups - $this->processCourseGroups($course_entity, $group_id, $total_quota_bytes, $used_quota_bytes, $chartData); + $this->processCourseGroups($courseEntity, $groupId, $totalQuotaBytes, $usedQuotaBytes, $chartData); // Process user documents - $users = $this->courseRepository->getUsersByCourse($course_entity); + $users = $this->courseRepository->getUsersByCourse($courseEntity); foreach ($users as $user) { - $user_id = $user->getId(); - $user_name = $user->getFullName(); - $this->processUserDocuments($course_entity, $session_entity, $user_id, $user_name, $total_quota_bytes, $chartData); + $userId = $user->getId(); + $userName = $user->getFullName(); + $this->processUserDocuments($courseEntity, $sessionEntity, $userId, $userName, $totalQuotaBytes, $chartData); } // Add available space - $available_bytes = $total_quota_bytes - $used_quota_bytes; - $available_percentage = $this->calculatePercentage($available_bytes, $total_quota_bytes); + $availableBytes = $totalQuotaBytes - $usedQuotaBytes; + $availablePercentage = $this->calculatePercentage($availableBytes, $totalQuotaBytes); $chartData[] = [ - 'label' => addslashes(get_lang('Available space')) . ' (' . format_file_size($available_bytes) . ')', - 'percentage' => $available_percentage, + 'label' => addslashes(get_lang('Available space')) . ' (' . format_file_size($availableBytes) . ')', + 'percentage' => $availablePercentage, ]; return new JsonResponse([ @@ -75,88 +75,88 @@ public function __invoke($cid): JsonResponse ]); } - private function processCourseSessions($course_entity, int $session_id, int $total_quota_bytes, int &$used_quota_bytes, array &$chartData): void + private function processCourseSessions($courseEntity, int $sessionId, int $totalQuotaBytes, int &$usedQuotaBytes, array &$chartData): void { - $sessions = $this->sessionRepository->getSessionsByCourse($course_entity); + $sessions = $this->sessionRepository->getSessionsByCourse($courseEntity); foreach ($sessions as $session) { - $quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity, null, $session); + $quotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity, null, $session); - if ($quota_bytes > 0) { - $session_name = $session->getTitle(); - if ($session_id === $session->getId()) { - $session_name .= ' * '; + if ($quotaBytes > 0) { + $sessionName = $session->getTitle(); + if ($sessionId === $session->getId()) { + $sessionName .= ' * '; } - $used_quota_bytes += $quota_bytes; + $usedQuotaBytes += $quotaBytes; $chartData[] = [ - 'label' => addslashes(get_lang('Session') . ': ' . $session_name) . ' (' . format_file_size($quota_bytes) . ')', - 'percentage' => $this->calculatePercentage($quota_bytes, $total_quota_bytes), + 'label' => addslashes(get_lang('Session') . ': ' . $sessionName) . ' (' . format_file_size($quotaBytes) . ')', + 'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes), ]; } } } - private function processCourseGroups($course_entity, int $group_id, int $total_quota_bytes, int &$used_quota_bytes, array &$chartData): void + private function processCourseGroups($courseEntity, int $groupId, int $totalQuotaBytes, int &$usedQuotaBytes, array &$chartData): void { - $groups_list = $this->groupRepository->findAllByCourse($course_entity)->getQuery()->getResult(); + $groupsList = $this->groupRepository->findAllByCourse($courseEntity)->getQuery()->getResult(); - foreach ($groups_list as $group_entity) { - $quota_bytes = $this->documentRepository->getTotalSpaceByCourse($course_entity, $group_entity->getIid()); + foreach ($groupsList as $groupEntity) { + $quotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity, $groupEntity->getIid()); - if ($quota_bytes > 0) { - $group_name = $group_entity->getTitle(); - if ($group_id === $group_entity->getIid()) { - $group_name .= ' * '; + if ($quotaBytes > 0) { + $groupName = $groupEntity->getTitle(); + if ($groupId === $groupEntity->getIid()) { + $groupName .= ' * '; } - $used_quota_bytes += $quota_bytes; + $usedQuotaBytes += $quotaBytes; $chartData[] = [ - 'label' => addslashes(get_lang('Group') . ': ' . $group_name) . ' (' . format_file_size($quota_bytes) . ')', - 'percentage' => $this->calculatePercentage($quota_bytes, $total_quota_bytes), + 'label' => addslashes(get_lang('Group') . ': ' . $groupName) . ' (' . format_file_size($quotaBytes) . ')', + 'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes), ]; } } } - private function processUserDocuments($course_entity, $session_entity, int $user_id, string $user_name, int $total_quota_bytes, array &$chartData): void + private function processUserDocuments($courseEntity, $sessionEntity, int $userId, string $userName, int $totalQuotaBytes, array &$chartData): void { - $documents_list = $this->documentRepository->getAllDocumentDataByUserAndGroup($course_entity); - $user_quota_bytes = 0; + $documentsList = $this->documentRepository->getAllDocumentDataByUserAndGroup($courseEntity); + $userQuotaBytes = 0; - foreach ($documents_list as $document_entity) { - if ($document_entity->getResourceNode()->getCreator()?->getId() === $user_id - && $document_entity->getFiletype() === 'file') { - $resourceFiles = $document_entity->getResourceNode()->getResourceFiles(); + foreach ($documentsList as $documentEntity) { + if ($documentEntity->getResourceNode()->getCreator()?->getId() === $userId + && $documentEntity->getFiletype() === 'file') { + $resourceFiles = $documentEntity->getResourceNode()->getResourceFiles(); if (!$resourceFiles->isEmpty()) { - $user_quota_bytes += $resourceFiles->first()->getSize(); + $userQuotaBytes += $resourceFiles->first()->getSize(); } } } - if ($user_quota_bytes > 0) { + if ($userQuotaBytes > 0) { $chartData[] = [ - 'label' => addslashes(get_lang('Teacher') . ': ' . $user_name) . ' (' . format_file_size($user_quota_bytes) . ')', - 'percentage' => $this->calculatePercentage($user_quota_bytes, $total_quota_bytes), + 'label' => addslashes(get_lang('Teacher') . ': ' . $userName) . ' (' . format_file_size($userQuotaBytes) . ')', + 'percentage' => $this->calculatePercentage($userQuotaBytes, $totalQuotaBytes), ]; // Handle session context - if ($session_entity) { - $session_total_quota = $this->calculateSessionTotalQuota($session_entity); - if ($session_total_quota > 0) { + if ($sessionEntity) { + $sessionTotalQuota = $this->calculateSessionTotalQuota($sessionEntity); + if ($sessionTotalQuota > 0) { $chartData[] = [ - 'label' => addslashes(sprintf(get_lang('TeacherXInSession'), $user_name)), - 'percentage' => $this->calculatePercentage($user_quota_bytes, $session_total_quota), + 'label' => addslashes(sprintf(get_lang('TeacherXInSession'), $userName)), + 'percentage' => $this->calculatePercentage($userQuotaBytes, $sessionTotalQuota), ]; } } } } - private function calculateSessionTotalQuota($session_entity): int + private function calculateSessionTotalQuota($sessionEntity): int { $total = 0; - $sessionCourses = $session_entity->getCourses(); + $sessionCourses = $sessionEntity->getCourses(); foreach ($sessionCourses as $courseEntity) { $total += DocumentManager::get_course_quota($courseEntity->getId()); @@ -165,12 +165,12 @@ private function calculateSessionTotalQuota($session_entity): int return $total; } - private function calculatePercentage(int $bytes, int $total_bytes): float + private function calculatePercentage(int $bytes, int $totalBytes): float { - if ($total_bytes === 0) { + if ($totalBytes === 0) { return 0.0; } - return round(($bytes / $total_bytes) * 100, 2); + return round(($bytes / $totalBytes) * 100, 2); } } From 7e73bf5f9439a08bf6d6dbdbee26e62f5edc9b95 Mon Sep 17 00:00:00 2001 From: erika Date: Fri, 10 Oct 2025 16:06:13 +0200 Subject: [PATCH 9/9] Minor: ECS ---fix --- .../Controller/Api/DocumentUsageAction.php | 41 ++++++------- .../Migrations/AbstractMigrationChamilo.php | 1 - .../Schema/V200/Version20250306101000.php | 3 +- .../Schema/V200/Version20251009111300.php | 7 +-- .../Repository/SessionRepository.php | 3 +- src/CourseBundle/Entity/CDocument.php | 2 +- .../Repository/CDocumentRepository.php | 59 ++++++++++--------- 7 files changed, 55 insertions(+), 61 deletions(-) diff --git a/src/CoreBundle/Controller/Api/DocumentUsageAction.php b/src/CoreBundle/Controller/Api/DocumentUsageAction.php index c04b35d63e1..81e2ed98101 100644 --- a/src/CoreBundle/Controller/Api/DocumentUsageAction.php +++ b/src/CoreBundle/Controller/Api/DocumentUsageAction.php @@ -13,18 +13,15 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Attribute\AsController; - #[AsController] class DocumentUsageAction extends AbstractController { - public function __construct( private readonly CourseRepository $courseRepository, private readonly CDocumentRepository $documentRepository, private readonly CGroupRepository $groupRepository, private readonly SessionRepository $sessionRepository, - ) { - } + ) {} public function __invoke($cid): JsonResponse { @@ -41,7 +38,7 @@ public function __invoke($cid): JsonResponse $totalQuotaBytes = ($courseEntity->getDiskQuota() * 1024 * 1024) ?? DEFAULT_DOCUMENT_QUOTA; $usedQuotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity); - + $chartData = []; // Process sessions @@ -61,9 +58,9 @@ public function __invoke($cid): JsonResponse // Add available space $availableBytes = $totalQuotaBytes - $usedQuotaBytes; $availablePercentage = $this->calculatePercentage($availableBytes, $totalQuotaBytes); - + $chartData[] = [ - 'label' => addslashes(get_lang('Available space')) . ' (' . format_file_size($availableBytes) . ')', + 'label' => addslashes(get_lang('Available space')).' ('.format_file_size($availableBytes).')', 'percentage' => $availablePercentage, ]; @@ -81,16 +78,16 @@ private function processCourseSessions($courseEntity, int $sessionId, int $total foreach ($sessions as $session) { $quotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity, null, $session); - + if ($quotaBytes > 0) { $sessionName = $session->getTitle(); if ($sessionId === $session->getId()) { $sessionName .= ' * '; } - + $usedQuotaBytes += $quotaBytes; $chartData[] = [ - 'label' => addslashes(get_lang('Session') . ': ' . $sessionName) . ' (' . format_file_size($quotaBytes) . ')', + 'label' => addslashes(get_lang('Session').': '.$sessionName).' ('.format_file_size($quotaBytes).')', 'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes), ]; } @@ -100,19 +97,19 @@ private function processCourseSessions($courseEntity, int $sessionId, int $total private function processCourseGroups($courseEntity, int $groupId, int $totalQuotaBytes, int &$usedQuotaBytes, array &$chartData): void { $groupsList = $this->groupRepository->findAllByCourse($courseEntity)->getQuery()->getResult(); - + foreach ($groupsList as $groupEntity) { $quotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity, $groupEntity->getIid()); - + if ($quotaBytes > 0) { $groupName = $groupEntity->getTitle(); if ($groupId === $groupEntity->getIid()) { $groupName .= ' * '; } - + $usedQuotaBytes += $quotaBytes; $chartData[] = [ - 'label' => addslashes(get_lang('Group') . ': ' . $groupName) . ' (' . format_file_size($quotaBytes) . ')', + 'label' => addslashes(get_lang('Group').': '.$groupName).' ('.format_file_size($quotaBytes).')', 'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes), ]; } @@ -125,8 +122,8 @@ private function processUserDocuments($courseEntity, $sessionEntity, int $userId $userQuotaBytes = 0; foreach ($documentsList as $documentEntity) { - if ($documentEntity->getResourceNode()->getCreator()?->getId() === $userId - && $documentEntity->getFiletype() === 'file') { + if ($documentEntity->getResourceNode()->getCreator()?->getId() === $userId + && 'file' === $documentEntity->getFiletype()) { $resourceFiles = $documentEntity->getResourceNode()->getResourceFiles(); if (!$resourceFiles->isEmpty()) { $userQuotaBytes += $resourceFiles->first()->getSize(); @@ -136,7 +133,7 @@ private function processUserDocuments($courseEntity, $sessionEntity, int $userId if ($userQuotaBytes > 0) { $chartData[] = [ - 'label' => addslashes(get_lang('Teacher') . ': ' . $userName) . ' (' . format_file_size($userQuotaBytes) . ')', + 'label' => addslashes(get_lang('Teacher').': '.$userName).' ('.format_file_size($userQuotaBytes).')', 'percentage' => $this->calculatePercentage($userQuotaBytes, $totalQuotaBytes), ]; @@ -145,7 +142,7 @@ private function processUserDocuments($courseEntity, $sessionEntity, int $userId $sessionTotalQuota = $this->calculateSessionTotalQuota($sessionEntity); if ($sessionTotalQuota > 0) { $chartData[] = [ - 'label' => addslashes(sprintf(get_lang('TeacherXInSession'), $userName)), + 'label' => addslashes(\sprintf(get_lang('TeacherXInSession'), $userName)), 'percentage' => $this->calculatePercentage($userQuotaBytes, $sessionTotalQuota), ]; } @@ -157,20 +154,20 @@ private function calculateSessionTotalQuota($sessionEntity): int { $total = 0; $sessionCourses = $sessionEntity->getCourses(); - + foreach ($sessionCourses as $courseEntity) { $total += DocumentManager::get_course_quota($courseEntity->getId()); } - + return $total; } private function calculatePercentage(int $bytes, int $totalBytes): float { - if ($totalBytes === 0) { + if (0 === $totalBytes) { return 0.0; } - + return round(($bytes / $totalBytes) * 100, 2); } } diff --git a/src/CoreBundle/Migrations/AbstractMigrationChamilo.php b/src/CoreBundle/Migrations/AbstractMigrationChamilo.php index 227b255e221..44eda5420f7 100644 --- a/src/CoreBundle/Migrations/AbstractMigrationChamilo.php +++ b/src/CoreBundle/Migrations/AbstractMigrationChamilo.php @@ -29,7 +29,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\File\UploadedFile; abstract class AbstractMigrationChamilo extends AbstractMigration diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20250306101000.php b/src/CoreBundle/Migrations/Schema/V200/Version20250306101000.php index 61c92946f6d..712467be6c5 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20250306101000.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20250306101000.php @@ -11,7 +11,6 @@ use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Schema\Schema; -use Symfony\Component\Finder\Finder; final class Version20250306101000 extends AbstractMigrationChamilo { @@ -29,7 +28,7 @@ public function up(Schema $schema): void $pluginTitle = (string) $pluginTitle['title']; } - if (!array_key_exists($pluginTitle, $replacements)) { + if (!\array_key_exists($pluginTitle, $replacements)) { continue; } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20251009111300.php b/src/CoreBundle/Migrations/Schema/V200/Version20251009111300.php index 53cf48d5ede..9ed54235da5 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20251009111300.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20251009111300.php @@ -18,20 +18,17 @@ public function getDescription(): string return 'Fix plugin titles and remove plugins without a corresponding directory'; } - /** - * @inheritDoc - */ public function up(Schema $schema): void { $replacements = self::pluginNameReplacements(); $idListToDelete = []; - $pluginRows = $this->connection->executeQuery("SELECT id, title, source FROM plugin")->fetchAllAssociative(); + $pluginRows = $this->connection->executeQuery('SELECT id, title, source FROM plugin')->fetchAllAssociative(); foreach ($pluginRows as $pluginRow) { $title = $pluginRow['title']; - if (!array_key_exists($title, $replacements)) { + if (!\array_key_exists($title, $replacements)) { $idListToDelete[] = $pluginRow['id']; continue; diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index 38532491b87..71fec29573d 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -94,7 +94,8 @@ public function getSessionsByCourse(Course $course): array ->innerJoin('s.courses', 'src') ->where($qb->expr()->eq('src.course', ':course')) ->setParameter('course', $course) - ->getQuery()->getResult(); + ->getQuery()->getResult() + ; } /** diff --git a/src/CourseBundle/Entity/CDocument.php b/src/CourseBundle/Entity/CDocument.php index 14392463208..570f8ac8731 100644 --- a/src/CourseBundle/Entity/CDocument.php +++ b/src/CourseBundle/Entity/CDocument.php @@ -200,7 +200,7 @@ security: "is_granted('ROLE_USER')", read: false, name: 'api_documents_usage' - ) + ), ], normalizationContext: [ 'groups' => ['document:read', 'resource_node:read'], diff --git a/src/CourseBundle/Repository/CDocumentRepository.php b/src/CourseBundle/Repository/CDocumentRepository.php index 3d83ce0e9b8..285e0d6c788 100644 --- a/src/CourseBundle/Repository/CDocumentRepository.php +++ b/src/CourseBundle/Repository/CDocumentRepository.php @@ -369,13 +369,6 @@ public function findChildNodeByTitle(ResourceNode $parent, string $title): ?Reso /** * Fetches all document data for the given user/group using Doctrine ORM. * - * @param Course $course - * @param string $path - * @param int $toGroupId - * @param int|null $toUserId - * @param bool $search - * @param Session|null $session - * * @return CDocument[] */ public function getAllDocumentDataByUserAndGroup( @@ -387,65 +380,73 @@ public function getAllDocumentDataByUserAndGroup( ?Session $session = null ): array { $qb = $this->createQueryBuilder('d'); - + $qb->innerJoin('d.resourceNode', 'rn') ->innerJoin('rn.resourceLinks', 'rl') ->where('rl.course = :course') - ->setParameter('course', $course); - + ->setParameter('course', $course) + ; + // Session filtering if ($session) { $qb->andWhere('(rl.session = :session OR rl.session IS NULL)') - ->setParameter('session', $session); + ->setParameter('session', $session) + ; } else { $qb->andWhere('rl.session IS NULL'); } - + // Path filtering - convert document.lib.php logic to Doctrine - if ($path !== '/') { + if ('/' !== $path) { // The original uses LIKE with path patterns - $pathPattern = rtrim($path, '/') . '/%'; + $pathPattern = rtrim($path, '/').'/%'; $qb->andWhere('rn.title LIKE :pathPattern OR rn.title = :exactPath') - ->setParameter('pathPattern', $pathPattern) - ->setParameter('exactPath', ltrim($path, '/')); - + ->setParameter('pathPattern', $pathPattern) + ->setParameter('exactPath', ltrim($path, '/')) + ; + // Exclude deeper nested paths if not searching if (!$search) { // Exclude paths with additional slashes beyond the current level - $excludePattern = rtrim($path, '/') . '/%/%'; + $excludePattern = rtrim($path, '/').'/%/%'; $qb->andWhere('rn.title NOT LIKE :excludePattern') - ->setParameter('excludePattern', $excludePattern); + ->setParameter('excludePattern', $excludePattern) + ; } } - + // User/Group filtering - if ($toUserId !== null) { + if (null !== $toUserId) { if ($toUserId > 0) { $qb->andWhere('rl.user = :userId') - ->setParameter('userId', $toUserId); + ->setParameter('userId', $toUserId) + ; } else { $qb->andWhere('rl.user IS NULL'); } } else { if ($toGroupId > 0) { $qb->andWhere('rl.group = :groupId') - ->setParameter('groupId', $toGroupId); + ->setParameter('groupId', $toGroupId) + ; } else { $qb->andWhere('rl.group IS NULL'); } } - + // Exclude deleted documents (like %_DELETED_% in original) $qb->andWhere('rn.title NOT LIKE :deletedPattern') - ->setParameter('deletedPattern', '%_DELETED_%'); - + ->setParameter('deletedPattern', '%_DELETED_%') + ; + // Order by creation date (equivalent to last.iid DESC) $qb->orderBy('rn.createdAt', 'DESC') - ->addOrderBy('rn.id', 'DESC'); - + ->addOrderBy('rn.id', 'DESC') + ; + return $qb->getQuery()->getResult(); } - + /** * Ensure "Learning paths" exists directly under the course resource node. * Links are created for course (and optional session) context.