Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/vue/components/basecomponents/BaseChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PrimeChart
:data="data"
:options="{}"
class="w-full md:w-30rem"
class="w-full md:w-30rem flex justify-center"
type="pie"
/>
</template>
Expand Down
31 changes: 26 additions & 5 deletions assets/vue/views/documents/DocumentsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,6 @@
:style="{ width: '28rem' }"
:title="t('Space available')"
>
<p>This feature is in development, this is a mockup with placeholder data!</p>
<BaseChart :data="usageData" />
</BaseDialog>

Expand Down Expand Up @@ -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
}
Expand Down
173 changes: 173 additions & 0 deletions src/CoreBundle/Controller/Api/DocumentUsageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

declare(strict_types=1);

namespace Chamilo\CoreBundle\Controller\Api;

use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\SessionRepository;
use Chamilo\CourseBundle\Repository\CDocumentRepository;
use Chamilo\CourseBundle\Repository\CGroupRepository;
use DocumentManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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
{
$courseId = (int) $cid;
$sessionId = api_get_session_id();
$groupId = api_get_group_id();

$courseEntity = $this->courseRepository->find($courseId);
if (!$courseEntity) {
return new JsonResponse(['error' => 'Course not found'], 404);
}

$sessionEntity = api_get_session_entity();

$totalQuotaBytes = ($courseEntity->getDiskQuota() * 1024 * 1024) ?? DEFAULT_DOCUMENT_QUOTA;

Check failure on line 39 in src/CoreBundle/Controller/Api/DocumentUsageAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test on ubuntu-latest

UndefinedConstant

src/CoreBundle/Controller/Api/DocumentUsageAction.php:39:77: UndefinedConstant: Const DEFAULT_DOCUMENT_QUOTA is not defined (see https://psalm.dev/020)
$usedQuotaBytes = $this->documentRepository->getTotalSpaceByCourse($courseEntity);

$chartData = [];

// Process sessions
$this->processCourseSessions($courseEntity, $sessionId, $totalQuotaBytes, $usedQuotaBytes, $chartData);

// Process groups
$this->processCourseGroups($courseEntity, $groupId, $totalQuotaBytes, $usedQuotaBytes, $chartData);

// Process user documents
$users = $this->courseRepository->getUsersByCourse($courseEntity);
foreach ($users as $user) {
$userId = $user->getId();
$userName = $user->getFullName();
$this->processUserDocuments($courseEntity, $sessionEntity, $userId, $userName, $totalQuotaBytes, $chartData);
}

// Add available space
$availableBytes = $totalQuotaBytes - $usedQuotaBytes;
$availablePercentage = $this->calculatePercentage($availableBytes, $totalQuotaBytes);

$chartData[] = [
'label' => addslashes(get_lang('Available space')).' ('.format_file_size($availableBytes).')',
'percentage' => $availablePercentage,
];

return new JsonResponse([
'datasets' => [
['data' => array_column($chartData, 'percentage')],
],
'labels' => array_column($chartData, 'label'),
]);
}

private function processCourseSessions($courseEntity, int $sessionId, int $totalQuotaBytes, int &$usedQuotaBytes, array &$chartData): void
{
$sessions = $this->sessionRepository->getSessionsByCourse($courseEntity);

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).')',
'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes),
];
}
}
}

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).')',
'percentage' => $this->calculatePercentage($quotaBytes, $totalQuotaBytes),
];
}
}
}

private function processUserDocuments($courseEntity, $sessionEntity, int $userId, string $userName, int $totalQuotaBytes, array &$chartData): void
{
$documentsList = $this->documentRepository->getAllDocumentDataByUserAndGroup($courseEntity);
$userQuotaBytes = 0;

foreach ($documentsList as $documentEntity) {
if ($documentEntity->getResourceNode()->getCreator()?->getId() === $userId
&& 'file' === $documentEntity->getFiletype()) {
$resourceFiles = $documentEntity->getResourceNode()->getResourceFiles();
if (!$resourceFiles->isEmpty()) {
$userQuotaBytes += $resourceFiles->first()->getSize();
}
}
}

if ($userQuotaBytes > 0) {
$chartData[] = [
'label' => addslashes(get_lang('Teacher').': '.$userName).' ('.format_file_size($userQuotaBytes).')',
'percentage' => $this->calculatePercentage($userQuotaBytes, $totalQuotaBytes),
];

// Handle session context
if ($sessionEntity) {
$sessionTotalQuota = $this->calculateSessionTotalQuota($sessionEntity);
if ($sessionTotalQuota > 0) {
$chartData[] = [
'label' => addslashes(\sprintf(get_lang('TeacherXInSession'), $userName)),
'percentage' => $this->calculatePercentage($userQuotaBytes, $sessionTotalQuota),
];
}
}
}
}

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 (0 === $totalBytes) {
return 0.0;
}

return round(($bytes / $totalBytes) * 100, 2);
}
}
1 change: 0 additions & 1 deletion src/CoreBundle/Migrations/AbstractMigrationChamilo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/CoreBundle/Repository/Node/CourseRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
12 changes: 12 additions & 0 deletions src/CoreBundle/Repository/SessionRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ 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<int, Session>
*
Expand Down
11 changes: 11 additions & 0 deletions src/CourseBundle/Entity/CDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'],
Expand Down
Loading
Loading