Skip to content

Commit 88f3823

Browse files
Internal: Implement course creation automation from file tree structure - refs BT#22644
1 parent 63b9c68 commit 88f3823

File tree

3 files changed

+234
-15
lines changed

3 files changed

+234
-15
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
declare(strict_types=1);
6+
7+
namespace Chamilo\CoreBundle\Command;
8+
9+
use Chamilo\CoreBundle\Entity\Course;
10+
use Chamilo\CoreBundle\Entity\User;
11+
use Chamilo\CoreBundle\Service\CourseService;
12+
use Chamilo\CoreBundle\Settings\SettingsManager;
13+
use Chamilo\CourseBundle\Entity\CDocument;
14+
use Chamilo\CourseBundle\Entity\CLp;
15+
use Chamilo\CourseBundle\Entity\CLpItem;
16+
use Doctrine\ORM\EntityManagerInterface;
17+
use Symfony\Component\Console\Attribute\AsCommand;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Input\InputArgument;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Finder\Finder;
24+
25+
26+
#[AsCommand(
27+
name: 'app:create-courses-from-structured-file',
28+
description: 'Create courses and learning paths from a folder containing files',
29+
)]
30+
class CreateCoursesFromStructuredFileCommand extends Command
31+
{
32+
public function __construct(
33+
private readonly EntityManagerInterface $em,
34+
private readonly CourseService $courseService,
35+
private readonly SettingsManager $settingsManager
36+
) {
37+
parent::__construct();
38+
}
39+
40+
protected function configure(): void
41+
{
42+
$this
43+
->addArgument('folder', InputArgument::REQUIRED, 'Path to folder with course files');
44+
}
45+
46+
protected function execute(InputInterface $input, OutputInterface $output): int
47+
{
48+
$io = new SymfonyStyle($input, $output);
49+
$adminUser = $this->getFirstAdmin();
50+
if (!$adminUser) {
51+
$io->error('No admin user found in the system.');
52+
return Command::FAILURE;
53+
}
54+
55+
$folder = $input->getArgument('folder');
56+
if (!is_dir($folder)) {
57+
$io->error("Invalid folder: $folder");
58+
return Command::FAILURE;
59+
}
60+
61+
$finder = new Finder();
62+
$finder->files()->in($folder);
63+
64+
foreach ($finder as $file) {
65+
$basename = $file->getBasename();
66+
$courseCode = pathinfo($basename, PATHINFO_FILENAME);
67+
$filePath = $file->getRealPath();
68+
69+
// Skip unsupported extensions
70+
$allowedExtensions = ['pdf', 'html', 'htm', 'mp4'];
71+
if (!in_array(strtolower($file->getExtension()), $allowedExtensions)) {
72+
$io->warning("Skipping unsupported file: $basename");
73+
continue;
74+
}
75+
76+
$io->section("Creating course: $courseCode");
77+
78+
// Step 1: Create the course
79+
$course = $this->courseService->createCourse([
80+
'title' => $courseCode,
81+
'wanted_code' => $courseCode,
82+
'add_user_as_teacher' => true,
83+
'course_language' => $this->settingsManager->getSetting('language.platform_language'),
84+
'visibility' => Course::OPEN_PLATFORM,
85+
'subscribe' => true,
86+
'unsubscribe' => true,
87+
'disk_quota' => $this->settingsManager->getSetting('document.default_document_quotum'),
88+
'expiration_date' => (new \DateTime('+1 year'))->format('Y-m-d H:i:s')
89+
]);
90+
91+
if (!$course) {
92+
throw new \RuntimeException('Error: Course could not be created.');
93+
}
94+
95+
// Step 2: Create learning path (CLp)
96+
$lp = (new CLp())
97+
->setLpType(1)
98+
->setTitle($courseCode)
99+
->setDescription('')
100+
->setPublishedOn(null)
101+
->setExpiredOn(null)
102+
->setCategory(null)
103+
->setParent($course)
104+
->addCourseLink($course);
105+
$lp->setCreator($adminUser);
106+
107+
$lpRepo = $this->em->getRepository(CLp::class);
108+
$lpRepo->createLp($lp);
109+
110+
// Step 3: Create CDocument from uploaded file
111+
$document = new CDocument();
112+
$document->setFiletype('file')
113+
->setTitle($basename)
114+
->setComment(null)
115+
->setReadonly(false)
116+
->setCreator($adminUser)
117+
->setParent($course)
118+
->addCourseLink($course);
119+
120+
$this->em->persist($document);
121+
$this->em->flush();
122+
123+
$documentRepo = $this->em->getRepository(CDocument::class);
124+
$documentRepo->addFileFromPath($document, $basename, $filePath);
125+
126+
// Step 4: Create LP item linked to the document
127+
// Ensure root item exists
128+
$lpItemRepo = $this->em->getRepository(CLpItem::class);
129+
$rootItem = $lpItemRepo->getRootItem((int) $lp->getIid());
130+
131+
if (!$rootItem) {
132+
$rootItem = (new CLpItem())
133+
->setTitle('root')
134+
->setPath('root')
135+
->setLp($lp)
136+
->setItemType('root');
137+
$this->em->persist($rootItem);
138+
$this->em->flush();
139+
}
140+
141+
$lpItem = (new CLpItem())
142+
->setLp($lp)
143+
->setTitle($basename)
144+
->setItemType('document')
145+
->setRef((string) $document->getIid())
146+
->setPath((string) $document->getIid())
147+
->setDisplayOrder(1)
148+
->setLaunchData('')
149+
->setMinScore(0)
150+
->setMaxScore(100)
151+
->setParent($rootItem)
152+
->setLvl(1)
153+
->setRoot($rootItem);
154+
155+
$this->em->persist($lpItem);
156+
$this->em->flush();
157+
158+
$io->success("Course '$courseCode' created with LP and document item '$basename'");
159+
}
160+
161+
return Command::SUCCESS;
162+
}
163+
164+
private function getFirstAdmin(): ?User
165+
{
166+
return $this->em->getRepository(User::class)
167+
->createQueryBuilder('u')
168+
->where('u.roles LIKE :role')
169+
->setParameter('role', '%ROLE_ADMIN%')
170+
->setMaxResults(1)
171+
->getQuery()
172+
->getOneOrNullResult();
173+
}
174+
}

src/CoreBundle/Service/CourseService.php

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
1919
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
2020
use Chamilo\CoreBundle\Repository\Node\UserRepository;
21+
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
2122
use Chamilo\CoreBundle\Settings\SettingsManager;
2223
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
2324
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
@@ -59,7 +60,8 @@ public function __construct(
5960
private readonly MailerInterface $mailer,
6061
private readonly EventLoggerService $eventLoggerService,
6162
private readonly ParameterBagInterface $parameterBag,
62-
private readonly RequestStack $requestStack
63+
private readonly RequestStack $requestStack,
64+
private readonly AccessUrlHelper $accessUrlHelper
6365
) {}
6466

6567
public function createCourse(array $params): ?Course
@@ -90,11 +92,16 @@ public function createCourse(array $params): ?Course
9092
public function registerCourse(array $rawParams): ?Course
9193
{
9294
try {
93-
/** @var User $currentUser */
95+
/** @var User|null $currentUser */
9496
$currentUser = $this->security->getUser();
9597

96-
$params = $this->prepareAndValidateCourseData($rawParams);
98+
// Fallback admin user if running from CLI
99+
if (!$currentUser instanceof User) {
100+
$currentUser = $this->getFallbackAdminUser();
101+
}
97102

103+
$params = $this->prepareAndValidateCourseData($rawParams);
104+
$accessUrl = $this->accessUrlHelper->getCurrent();
98105
$course = new Course();
99106
$course
100107
->setTitle($params['title'])
@@ -114,6 +121,7 @@ public function registerCourse(array $rawParams): ?Course
114121
->setUnsubscribe($params['unsubscribe'])
115122
->setCreator($currentUser)
116123
;
124+
$course->addAccessUrl($accessUrl);
117125

118126
if (!empty($params['categories'])) {
119127
foreach ($params['categories'] as $categoryId) {
@@ -125,7 +133,7 @@ public function registerCourse(array $rawParams): ?Course
125133
}
126134

127135
$addTeacher = $params['add_user_as_teacher'] ?? true;
128-
$user = $currentUser;
136+
$user = $currentUser ?? $this->getFallbackAdminUser();
129137
if (!empty($params['user_id'])) {
130138
$user = $this->userRepository->find((int) $params['user_id']);
131139
}
@@ -160,7 +168,7 @@ public function registerCourse(array $rawParams): ?Course
160168

161169
$this->courseRepository->create($course);
162170

163-
if ($rawParams['exemplary_content']) {
171+
if (!empty($rawParams['exemplary_content'])) {
164172
$this->fillCourse($course, $params);
165173
}
166174

@@ -173,7 +181,7 @@ public function registerCourse(array $rawParams): ?Course
173181

174182
return $course;
175183
} catch (Exception $e) {
176-
return null;
184+
throw $e;
177185
}
178186
}
179187

@@ -339,6 +347,10 @@ private function createRootGradebook(Course $course): GradebookCategory
339347
{
340348
/** @var User $currentUser */
341349
$currentUser = $this->security->getUser();
350+
if (!$currentUser instanceof User) {
351+
$currentUser = $this->getFallbackAdminUser();
352+
}
353+
342354
if (!$currentUser) {
343355
throw new LogicException('There is no user currently authenticated..');
344356
}
@@ -366,6 +378,9 @@ private function insertExampleContent(Course $course, GradebookCategory $gradebo
366378
{
367379
/** @var User $currentUser */
368380
$currentUser = $this->security->getUser();
381+
if (!$currentUser instanceof User) {
382+
$currentUser = $this->getFallbackAdminUser();
383+
}
369384

370385
$files = [
371386
['path' => '/audio', 'title' => $this->translator->trans('Audio'), 'filetype' => 'folder', 'size' => 0],
@@ -761,13 +776,37 @@ private function handlePostCourseCreation(Course $course, array $params): void
761776
$this->sendEmailToAdmin($course);
762777
}
763778

779+
$currentUser = $this->security->getUser();
780+
if (!$currentUser instanceof User) {
781+
$currentUser = $this->getFallbackAdminUser();
782+
}
783+
764784
$this->eventLoggerService->addEvent(
765785
'course_created',
766786
'course_id',
767787
$course->getId(),
768788
null,
769-
$this->security->getUser()->getId(),
789+
$currentUser->getId(),
770790
$course->getId()
771791
);
772792
}
793+
794+
private function getFallbackAdminUser(): User
795+
{
796+
$qb = $this->entityManager->createQueryBuilder();
797+
798+
$qb->select('u')
799+
->from(User::class, 'u')
800+
->where('u.roles LIKE :role')
801+
->setParameter('role', '%ROLE_ADMIN%')
802+
->setMaxResults(1);
803+
804+
$user = $qb->getQuery()->getOneOrNullResult();
805+
806+
if (!$user instanceof User) {
807+
throw new \RuntimeException('No admin user found for fallback.');
808+
}
809+
810+
return $user;
811+
}
773812
}

src/CoreBundle/ServiceHelper/CidReqHelper.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,39 @@ private function getRequest(): ?Request
2727
return $this->requestStack->getCurrentRequest();
2828
}
2929

30-
private function getSessionHandler(): SessionInterface
30+
private function getSessionHandler(): ?SessionInterface
3131
{
32-
return $this->getRequest()->getSession();
32+
$request = $this->getRequest();
33+
return $request?->getSession();
3334
}
3435

3536
public function getSessionId(): ?int
3637
{
37-
return $this->getSessionHandler()->get('sid');
38+
$session = $this->getSessionHandler();
39+
return $session?->get('sid');
3840
}
3941

4042
public function getSessionEntity(): ?Session
4143
{
42-
return $this->getSessionHandler()->get('session');
44+
$session = $this->getSessionHandler();
45+
return $session?->get('session');
4346
}
4447

45-
public function getCourseId()
48+
public function getCourseId(): mixed
4649
{
47-
return $this->getSessionHandler()->get('cid');
50+
$session = $this->getSessionHandler();
51+
return $session?->get('cid');
4852
}
4953

5054
public function getCourseEntity(): ?Course
5155
{
52-
return $this->getSessionHandler()->get('course');
56+
$session = $this->getSessionHandler();
57+
return $session?->get('course');
5358
}
5459

5560
public function getGroupId(): ?int
5661
{
57-
return $this->getSessionHandler()->get('gid');
62+
$session = $this->getSessionHandler();
63+
return $session?->get('gid');
5864
}
5965
}

0 commit comments

Comments
 (0)