Passed
Pull Request — master (#6396)
by Angel Fernando Quiroz
08:39
created

CourseHelper::createCourse()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 7
nop 1
dl 0
loc 23
rs 9.5555
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Agenda;
10
use AnnouncementManager;
11
use Answer;
12
use AppPlugin;
13
use Chamilo\CoreBundle\Entity\Course;
14
use Chamilo\CoreBundle\Entity\CourseRelUser;
15
use Chamilo\CoreBundle\Entity\GradebookCategory;
16
use Chamilo\CoreBundle\Entity\GradebookLink;
17
use Chamilo\CoreBundle\Entity\User;
18
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
19
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
20
use Chamilo\CoreBundle\Repository\Node\UserRepository;
21
use Chamilo\CoreBundle\Settings\SettingsManager;
22
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
23
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
24
use Chamilo\CourseBundle\Entity\CCourseSetting;
25
use Chamilo\CourseBundle\Entity\CForum;
26
use Chamilo\CourseBundle\Entity\CGroupCategory;
27
use DateTime;
28
use DateTimeZone;
29
use Doctrine\ORM\EntityManager;
30
use DocumentManager;
31
use Exception;
32
use Exercise;
33
use InvalidArgumentException;
34
use Link;
35
use LogicException;
36
use MultipleAnswer;
37
use Symfony\Bundle\SecurityBundle\Security;
38
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
39
use Symfony\Component\Finder\Finder;
40
use Symfony\Component\Finder\SplFileInfo;
41
use Symfony\Component\HttpFoundation\RequestStack;
42
use Symfony\Component\Mailer\MailerInterface;
43
use Symfony\Component\Mime\Email;
44
use Symfony\Contracts\Translation\TranslatorInterface;
45
use Throwable;
46
47
class CourseHelper
48
{
49
    public const MAX_COURSE_LENGTH_CODE = 40;
50
51
    public function __construct(
52
        private readonly EntityManager $entityManager,
53
        private readonly CourseRepository $courseRepository,
54
        private readonly Security $security,
55
        private readonly CourseCategoryRepository $courseCategoryRepository,
56
        private readonly UserRepository $userRepository,
57
        private readonly SettingsManager $settingsManager,
58
        private readonly TranslatorInterface $translator,
59
        private readonly MailerInterface $mailer,
60
        private readonly EventLoggerHelper $eventLoggerHelper,
61
        private readonly ParameterBagInterface $parameterBag,
62
        private readonly RequestStack $requestStack,
63
        private readonly AccessUrlHelper $accessUrlHelper
64
    ) {}
65
66
    public function createCourse(array $params): ?Course
67
    {
68
        if (empty($params['title'])) {
69
            throw new InvalidArgumentException('The course title cannot be empty.');
70
        }
71
72
        if (empty($params['wanted_code'])) {
73
            $params['wanted_code'] = $this->generateCourseCode($params['title']);
74
        }
75
76
        if ($this->courseRepository->courseCodeExists($params['wanted_code'])) {
77
            throw new Exception('The course code already exists: '.$params['wanted_code']);
78
        }
79
80
        $keys = $this->defineCourseKeys($params['wanted_code']);
81
82
        $params = array_merge($params, $keys);
83
        $course = $this->registerCourse($params);
84
        if ($course) {
85
            $this->handlePostCourseCreation($course, $params);
86
        }
87
88
        return $course;
89
    }
90
91
    public function registerCourse(array $rawParams): ?Course
92
    {
93
        try {
94
            /** @var User|null $currentUser */
95
            $currentUser = $this->security->getUser();
96
97
            // Fallback admin user if running from CLI
98
            if (!$currentUser instanceof User) {
99
                $currentUser = $this->getFallbackAdminUser();
100
            }
101
102
            $params = $this->prepareAndValidateCourseData($rawParams);
103
            $accessUrl = $this->accessUrlHelper->getCurrent();
104
            $course = new Course();
105
            $course
106
                ->setTitle($params['title'])
107
                ->setCode($params['code'])
108
                ->setVisualCode($params['visualCode'])
109
                ->setCourseLanguage($params['courseLanguage'])
110
                ->setDescription($this->translator->trans('Course Description'))
111
                ->setVisibility((int) $params['visibility'])
112
                ->setShowScore(1)
113
                ->setDiskQuota((int) $params['diskQuota'])
114
                ->setExpirationDate($params['expirationDate'])
115
                ->setDepartmentName($params['departmentName'] ?? '')
116
                ->setDepartmentUrl($params['departmentUrl'])
117
                ->setSubscribe($params['subscribe'])
118
                ->setSticky($params['sticky'] ?? false)
119
                ->setVideoUrl($params['videoUrl'] ?? '')
120
                ->setUnsubscribe($params['unsubscribe'])
121
                ->setCreator($currentUser)
122
            ;
123
            $course->addAccessUrl($accessUrl);
124
125
            if (!empty($params['categories'])) {
126
                foreach ($params['categories'] as $categoryId) {
127
                    $category = $this->courseCategoryRepository->find($categoryId);
128
                    if ($category) {
129
                        $course->addCategory($category);
130
                    }
131
                }
132
            }
133
134
            $addTeacher = $params['add_user_as_teacher'] ?? true;
135
            $user = $currentUser ?? $this->getFallbackAdminUser();
136
            if (!empty($params['user_id'])) {
137
                $user = $this->userRepository->find((int) $params['user_id']);
138
            }
139
            if ($addTeacher) {
140
                $courseRelTutor = (new CourseRelUser())
141
                    ->setCourse($course)
142
                    ->setUser($user)
143
                    ->setStatus(1)
144
                    ->setTutor(true)
145
                    ->setRelationType(0)
146
                    ->setUserCourseCat(0)
147
                ;
148
                $course->addSubscription($courseRelTutor);
149
            }
150
151
            if (!empty($params['teachers'])) {
152
                foreach ($params['teachers'] as $teacherId) {
153
                    $teacher = $this->userRepository->find($teacherId);
154
                    if ($teacher) {
155
                        $courseRelTeacher = (new CourseRelUser())
156
                            ->setCourse($course)
157
                            ->setUser($teacher)
158
                            ->setStatus(1)
159
                            ->setTutor(false)
160
                            ->setRelationType(0)
161
                            ->setUserCourseCat(0)
162
                        ;
163
                        $course->addSubscription($courseRelTeacher);
164
                    }
165
                }
166
            }
167
168
            $this->courseRepository->create($course);
169
170
            if (!empty($rawParams['exemplary_content'])) {
171
                $this->fillCourse($course, $params);
172
            }
173
174
            if (isset($rawParams['course_template'])) {
175
                $this->useTemplateAsBasisIfRequired(
176
                    $course->getCode(),
177
                    (int) $rawParams['course_template']
178
                );
179
            }
180
181
            return $course;
182
        } catch (Exception $e) {
183
            throw $e;
184
        }
185
    }
186
187
    public function sendEmailToAdmin(Course $course): void
188
    {
189
        $siteName = $this->getDefaultSetting('platform.site_name');
190
        $recipientEmail = $this->getDefaultSetting('admin.administrator_email');
191
        $recipientName = $this->getDefaultSetting('admin.administrator_name').' '.$this->getDefaultSetting('admin.administrator_surname');
192
        $institutionName = $this->getDefaultSetting('platform.institution');
193
        $courseName = $course->getTitle();
194
195
        $subject = $this->translator->trans('New Course Created in')."$siteName - $institutionName";
196
197
        $greeting = $this->translator->trans('email.greeting');
198
        $intro = $this->translator->trans('email.course_created_intro');
199
        $courseNameLabel = $this->translator->trans('email.course_name');
200
201
        $message = "$greeting $recipientName,\n\n";
202
        $message .= "$intro $siteName - $institutionName.\n";
203
        $message .= "$courseNameLabel $courseName\n";
204
        $message .= $this->translator->trans('Course name: ').$course->getTitle()."\n";
205
206
        foreach ($course->getCategories() as $category) {
207
            $message .= $this->translator->trans('Category: ').$category->getCode()."\n";
208
        }
209
210
        $message .= $this->translator->trans('Coach: ').$course->getTutorName()."\n";
211
        $message .= $this->translator->trans('Language: ').$course->getCourseLanguage();
212
213
        $email = (new Email())
214
            ->from($recipientEmail)
215
            ->to($recipientEmail)
216
            ->subject($subject)
217
            ->text($message)
218
        ;
219
220
        $this->mailer->send($email);
221
    }
222
223
    public function defineCourseKeys(
224
        string $wantedCode,
225
        string $prefixForAll = '',
226
        string $prefixForPath = '',
227
        bool $addUniquePrefix = false,
228
        bool $useCodeIndependentKeys = true
229
    ): array {
230
        $wantedCode = $this->generateCourseCode($wantedCode);
231
        $keysCourseCode = $useCodeIndependentKeys ? $wantedCode : '';
232
233
        $uniquePrefix = $addUniquePrefix ? substr(md5(uniqid((string) rand(), true)), 0, 10) : '';
234
235
        $keys = [];
236
        $finalSuffix = ['CourseId' => '', 'CourseDir' => ''];
237
        $limitNumTry = 100;
238
        $tryCount = 0;
239
240
        $keysAreUnique = false;
241
242
        while (!$keysAreUnique && $tryCount < $limitNumTry) {
243
            $keysCourseId = $prefixForAll.$uniquePrefix.$keysCourseCode.$finalSuffix['CourseId'];
244
            $keysCourseRepository = $prefixForPath.$uniquePrefix.$wantedCode.$finalSuffix['CourseDir'];
245
246
            if ($this->courseRepository->courseCodeExists($keysCourseId)) {
247
                $finalSuffix['CourseId'] = substr(md5(uniqid((string) rand(), true)), 0, 4);
248
                $tryCount++;
249
            } else {
250
                $keysAreUnique = true;
251
            }
252
        }
253
254
        if ($keysAreUnique) {
255
            $keys = [
256
                'code' => $keysCourseCode,
257
                'visual_code' => $keysCourseId,
258
                'directory' => $keysCourseRepository,
259
            ];
260
        }
261
262
        return $keys;
263
    }
264
265
    public function fillCourse(Course $course, array $params): void
266
    {
267
        $entityManager = $this->entityManager;
268
269
        $this->insertCourseSettings($course);
270
271
        $this->createGroupCategory($course);
272
273
        $gradebook = $this->createRootGradebook($course);
274
275
        if ('true' === $this->settingsManager->getSetting('course.example_material_course_creation')) {
276
            $this->insertExampleContent($course, $gradebook);
277
        }
278
279
        $this->installCoursePlugins($course->getId());
280
281
        $entityManager->flush();
282
    }
283
284
    private function insertCourseSettings(Course $course): void
285
    {
286
        $defaultEmailExerciseAlert = 0;
287
        if ('true' === $this->settingsManager->getSetting('exercise.email_alert_manager_on_new_quiz')) {
288
            $defaultEmailExerciseAlert = 1;
289
        }
290
291
        $settings = [
292
            'email_alert_manager_on_new_doc' => ['title' => '', 'default' => 0, 'category' => 'work'],
293
            'email_alert_on_new_doc_dropbox' => ['default' => 0, 'category' => 'dropbox'],
294
            'allow_user_edit_agenda' => ['default' => 0, 'category' => 'agenda'],
295
            'allow_user_edit_announcement' => ['default' => 0, 'category' => 'announcement'],
296
            'email_alert_manager_on_new_quiz' => ['default' => $defaultEmailExerciseAlert, 'category' => 'quiz'],
297
            'allow_user_image_forum' => ['default' => 1, 'category' => 'forum'],
298
            'course_theme' => ['default' => '', 'category' => 'theme'],
299
            'allow_learning_path_theme' => ['default' => 1, 'category' => 'theme'],
300
            'allow_open_chat_window' => ['default' => 1, 'category' => 'chat'],
301
            'email_alert_to_teacher_on_new_user_in_course' => ['default' => 0, 'category' => 'registration'],
302
            'allow_user_view_user_list' => ['default' => 1, 'category' => 'user'],
303
            'display_info_advance_inside_homecourse' => ['default' => 1, 'category' => 'thematic_advance'],
304
            'email_alert_students_on_new_homework' => ['default' => 0, 'category' => 'work'],
305
            'enable_lp_auto_launch' => ['default' => 0, 'category' => 'learning_path'],
306
            'enable_exercise_auto_launch' => ['default' => 0, 'category' => 'exercise'],
307
            'enable_document_auto_launch' => ['default' => 0, 'category' => 'document'],
308
            'pdf_export_watermark_text' => ['default' => '', 'category' => 'learning_path'],
309
            'allow_public_certificates' => [
310
                'default' => 'true' === $this->settingsManager->getSetting('course.allow_public_certificates') ? 1 : '',
311
                'category' => 'certificates',
312
            ],
313
            'documents_default_visibility' => ['default' => 'visible', 'category' => 'document'],
314
            'show_course_in_user_language' => ['default' => 2, 'category' => null],
315
            'email_to_teachers_on_new_work_feedback' => ['default' => 1, 'category' => null],
316
        ];
317
318
        foreach ($settings as $variable => $setting) {
319
            $courseSetting = new CCourseSetting();
320
            $courseSetting->setCId($course->getId());
321
            $courseSetting->setVariable($variable);
322
            $courseSetting->setTitle($setting['title'] ?? '');
323
            $courseSetting->setValue((string) $setting['default']);
324
            $courseSetting->setCategory($setting['category'] ?? '');
325
326
            $this->entityManager->persist($courseSetting);
327
        }
328
329
        $this->entityManager->flush();
330
    }
331
332
    private function createGroupCategory(Course $course): void
333
    {
334
        $groupCategory = new CGroupCategory();
335
        $groupCategory
336
            ->setTitle($this->translator->trans('Default groups'))
337
            ->setParent($course)
338
            ->addCourseLink($course)
339
        ;
340
341
        $this->entityManager->persist($groupCategory);
342
        $this->entityManager->flush();
343
    }
344
345
    private function createRootGradebook(Course $course): GradebookCategory
346
    {
347
        /** @var User $currentUser */
348
        $currentUser = $this->security->getUser();
349
        if (!$currentUser instanceof User) {
350
            $currentUser = $this->getFallbackAdminUser();
351
        }
352
353
        if (!$currentUser) {
354
            throw new LogicException('There is no user currently authenticated..');
355
        }
356
357
        $gradebookCategory = new GradebookCategory();
358
        $gradebookCategory
359
            ->setTitle($course->getCode())
360
            ->setLocked(0)
361
            ->setGenerateCertificates(false)
362
            ->setDescription('')
363
            ->setCourse($course)
364
            ->setWeight(100)
365
            ->setVisible(false)
366
            ->setCertifMinScore(75)
367
            ->setUser($currentUser)
368
        ;
369
370
        $this->entityManager->persist($gradebookCategory);
371
        $this->entityManager->flush();
372
373
        return $gradebookCategory;
374
    }
375
376
    private function insertExampleContent(Course $course, GradebookCategory $gradebook): void
377
    {
378
        /** @var User $currentUser */
379
        $currentUser = $this->security->getUser();
380
        if (!$currentUser instanceof User) {
381
            $currentUser = $this->getFallbackAdminUser();
382
        }
383
384
        $files = [
385
            ['path' => '/audio', 'title' => $this->translator->trans('Audio'), 'filetype' => 'folder', 'size' => 0],
386
            ['path' => '/images', 'title' => $this->translator->trans('Images'), 'filetype' => 'folder', 'size' => 0],
387
            ['path' => '/images/gallery', 'title' => $this->translator->trans('Gallery'), 'filetype' => 'folder', 'size' => 0],
388
            ['path' => '/video', 'title' => $this->translator->trans('Video'), 'filetype' => 'folder', 'size' => 0],
389
        ];
390
        $paths = [];
391
        $courseInfo = ['real_id' => $course->getId(), 'code' => $course->getCode()];
392
        foreach ($files as $file) {
393
            $doc = DocumentManager::addDocument(
394
                $courseInfo,
395
                $file['path'],
396
                $file['filetype'],
397
                $file['size'],
398
                $file['title'],
399
                null,
400
                0,
401
                null,
402
                0,
403
                0,
404
                0,
405
                false
406
            );
407
            $paths[$file['path']] = $doc->getIid();
408
        }
409
410
        $projectDir = $this->parameterBag->get('kernel.project_dir');
411
        $defaultPath = $projectDir.'/public/img/document';
412
413
        $request = $this->requestStack->getCurrentRequest();
414
        $baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
415
416
        $finder = new Finder();
417
        $finder->in($defaultPath);
418
419
        /** @var SplFileInfo $file */
420
        foreach ($finder as $file) {
421
            $parentName = \dirname(str_replace($defaultPath, '', $file->getRealPath()));
422
            if ('/' === $parentName || '/certificates' === $parentName) {
423
                continue;
424
            }
425
426
            $title = $file->getFilename();
427
            $parentId = $paths[$parentName];
428
429
            if ($file->isDir()) {
430
                $realPath = str_replace($defaultPath, '', $file->getRealPath());
431
                $document = DocumentManager::addDocument(
432
                    $courseInfo,
433
                    $realPath,
434
                    'folder',
435
                    null,
436
                    $title,
437
                    '',
438
                    null,
439
                    null,
440
                    null,
441
                    null,
442
                    null,
443
                    false,
444
                    null,
445
                    $parentId,
446
                    $file->getRealPath()
447
                );
448
                $paths[$realPath] = $document->getIid();
449
            } else {
450
                $realPath = str_replace($defaultPath, '', $file->getRealPath());
451
                $document = DocumentManager::addDocument(
452
                    $courseInfo,
453
                    $realPath,
454
                    'file',
455
                    $file->getSize(),
456
                    $title,
457
                    '',
458
                    null,
459
                    null,
460
                    null,
461
                    null,
462
                    null,
463
                    false,
464
                    null,
465
                    $parentId,
466
                    $file->getRealPath()
467
                );
468
            }
469
        }
470
471
        $now = new DateTime('now', new DateTimeZone('UTC'));
472
        $formattedNow = $now->format('Y-m-d H:i:s');
473
474
        $agenda = new Agenda('course');
475
        $agenda->set_course($courseInfo);
476
        $agenda->addEvent(
477
            $formattedNow,
478
            $formattedNow,
479
            0,
480
            $this->translator->trans('Course creation'),
481
            $this->translator->trans('This course was created at this time'),
482
            ['everyone' => 'everyone']
483
        );
484
485
        /*  Links tool */
486
        $link = new Link();
487
        $link->setCourse($courseInfo);
488
        $links = [
489
            [
490
                'c_id' => $course->getId(),
491
                'url' => 'http://www.google.com',
492
                'title' => 'Quick and powerful search engine',
493
                'description' => $this->translator->trans('Quick and powerful search engine'),
494
                'category_id' => 0,
495
                'on_homepage' => 0,
496
                'target' => '_self',
497
                'session_id' => 0,
498
            ],
499
            [
500
                'c_id' => $course->getId(),
501
                'url' => 'http://www.wikipedia.org',
502
                'title' => 'Free online encyclopedia',
503
                'description' => $this->translator->trans('Free online encyclopedia'),
504
                'category_id' => 0,
505
                'on_homepage' => 0,
506
                'target' => '_self',
507
                'session_id' => 0,
508
            ],
509
        ];
510
511
        foreach ($links as $params) {
512
            $link->save($params, false, false);
513
        }
514
515
        /* Announcement tool */
516
        AnnouncementManager::add_announcement(
517
            $courseInfo,
518
            0,
519
            $this->translator->trans('This is an announcement example'),
520
            $this->translator->trans('This is an announcement example. Only trainers are allowed to publish announcements.'),
521
            ['everyone' => 'everyone'],
522
            null,
523
            null,
524
            $formattedNow
525
        );
526
527
        /*  Exercise tool */
528
        $exercise = new Exercise($course->getId());
529
        $exercise->exercise = $this->translator->trans('Sample test');
530
        $html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
531
                        <tr>
532
                        <td width="220" valign="top" align="left">
533
                            <img src="'.$baseUrl.'/public/img/document/images/mr_chamilo/doubts.png">
534
                        </td>
535
                        <td valign="top" align="left">'.$this->translator->trans('Irony').'</td></tr>
536
                    </table>';
537
        $exercise->type = 1;
538
        $exercise->setRandom(0);
539
        $exercise->active = 1;
540
        $exercise->results_disabled = 0;
541
        $exercise->description = $html;
542
        $exercise->save();
543
544
        $question = new MultipleAnswer();
545
        $question->course = $courseInfo;
546
        $question->question = $this->translator->trans('Socratic irony is...');
547
        $question->description = $this->translator->trans('(more than one answer can be true)');
548
        $question->weighting = 10;
549
        $question->position = 1;
550
        $question->course = $courseInfo;
551
        $question->save($exercise);
552
        $questionId = $question->id;
553
554
        $answer = new Answer($questionId, $courseInfo['real_id']);
555
        $answer->createAnswer($this->translator->trans('Ridiculise one\'s interlocutor in order to have him concede he is wrong.'), 0, $this->translator->trans('No. Socratic irony is not a matter of psychology, it concerns argumentation.'), -5, 1);
556
        $answer->createAnswer($this->translator->trans('Admit one\'s own errors to invite one\'s interlocutor to do the same.'), 0, $this->translator->trans('No. Socratic irony is not a seduction strategy or a method based on the example.'), -5, 2);
557
        $answer->createAnswer($this->translator->trans('Compell one\'s interlocutor, by a series of questions and sub-questions, to admit he doesn\'t know what he claims to know.'), 1, $this->translator->trans('Indeed'), 5, 3);
558
        $answer->createAnswer($this->translator->trans('Use the Principle of Non Contradiction to force one\'s interlocutor into a dead end.'), 1, $this->translator->trans('This answer is not false. It is true that the revelation of the interlocutor\'s ignorance means showing the contradictory conclusions where lead his premisses.'), 5, 4);
559
        $answer->save();
560
561
        // Forums.
562
        $params = [
563
            'forum_category_title' => $this->translator->trans('Example Forum Category'),
564
            'forum_category_comment' => '',
565
        ];
566
567
        $forumCategoryId = saveForumCategory($params, $courseInfo, false);
568
569
        $params = [
570
            'forum_category' => $forumCategoryId,
571
            'forum_title' => $this->translator->trans('Example Forum'),
572
            'forum_comment' => '',
573
            'default_view_type_group' => ['default_view_type' => 'flat'],
574
        ];
575
576
        $forumId = store_forum($params, $courseInfo, true);
577
        $repo = $this->entityManager->getRepository(CForum::class);
578
        $forumEntity = $repo->find($forumId);
579
580
        $params = [
581
            'post_title' => $this->translator->trans('Example Thread'),
582
            'forum_id' => $forumId,
583
            'post_text' => $this->translator->trans('Example ThreadContent'),
584
            'calification_notebook_title' => '',
585
            'numeric_calification' => '',
586
            'weight_calification' => '',
587
            'forum_category' => $forumCategoryId,
588
            'thread_peer_qualify' => 0,
589
        ];
590
591
        saveThread($forumEntity, $params, $courseInfo, false);
592
593
        $this->createExampleGradebookContent($course, $gradebook, $exercise->id);
594
    }
595
596
    private function createExampleGradebookContent(Course $course, GradebookCategory $parentCategory, int $refId): void
597
    {
598
        $manager = $this->entityManager;
599
600
        /* Gradebook tool */
601
        $courseCode = $course->getCode();
602
603
        $childGradebookCategory = new GradebookCategory();
604
        $childGradebookCategory->setTitle($courseCode);
605
        $childGradebookCategory->setLocked(0);
606
        $childGradebookCategory->setGenerateCertificates(false);
607
        $childGradebookCategory->setDescription('');
608
        $childGradebookCategory->setCourse($course);
609
        $childGradebookCategory->setWeight(100);
610
        $childGradebookCategory->setVisible(true);
611
        $childGradebookCategory->setCertifMinScore(75);
612
        $childGradebookCategory->setParent($parentCategory);
613
        $childGradebookCategory->setUser(api_get_user_entity());
614
615
        $manager->persist($childGradebookCategory);
616
        $manager->flush();
617
618
        $gradebookLink = new GradebookLink();
619
620
        $gradebookLink->setType(1);
621
        $gradebookLink->setRefId($refId);
622
        $gradebookLink->setUser(api_get_user_entity());
623
        $gradebookLink->setCourse($course);
624
        $gradebookLink->setCategory($childGradebookCategory);
625
        $gradebookLink->setCreatedAt(new DateTime());
626
        $gradebookLink->setWeight(100);
627
        $gradebookLink->setVisible(1);
628
        $gradebookLink->setLocked(0);
629
630
        $manager->persist($gradebookLink);
631
        $manager->flush();
632
    }
633
634
    private function installCoursePlugins(int $courseId): void
635
    {
636
        $app_plugin = new AppPlugin();
637
        $app_plugin->install_course_plugins($courseId);
638
    }
639
640
    public function useTemplateAsBasisIfRequired($courseCode, $courseTemplate): void
641
    {
642
        $templateSetting = $this->settingsManager->getSetting('course.course_creation_use_template');
643
        $teacherCanSelectCourseTemplate = 'true' === $this->settingsManager->getSetting('course.teacher_can_select_course_template');
644
        $courseTemplate = isset($courseTemplate) ? (int) $courseTemplate : 0;
645
646
        $useTemplate = false;
647
        if ($teacherCanSelectCourseTemplate && $courseTemplate > 0) {
648
            $useTemplate = true;
649
            $originCourse = $this->courseRepository->findCourseAsArray((int) $courseTemplate);
650
        } elseif (!empty($templateSetting)) {
651
            $useTemplate = true;
652
            $originCourse = $this->courseRepository->findCourseAsArray((int) $templateSetting);
653
        }
654
655
        if ($useTemplate && !empty($originCourse)) {
656
            try {
657
                $originCourse['official_code'] = $originCourse['code'];
658
                $cb = new CourseBuilder(null, $originCourse);
659
                $course = $cb->build(null, $originCourse['code']);
660
                $cr = new CourseRestorer($course);
661
                $cr->set_file_option();
662
                $cr->restore($courseCode);
663
            } catch (Exception $e) {
664
                error_log('Error during course template application: '.$e->getMessage());
665
            } catch (Throwable $t) {
666
                error_log('Unexpected error during course template application: '.$t->getMessage());
667
            }
668
        }
669
    }
670
671
    private function prepareAndValidateCourseData(array $params): array
672
    {
673
        $title = str_replace('&amp;', '&', $params['title'] ?? '');
674
        $code = $params['code'] ?? '';
675
        $visualCode = $params['visual_code'] ?? '';
676
        $directory = $params['directory'] ?? '';
677
        $tutorName = $params['tutor_name'] ?? null;
678
        $courseLanguage = !empty($params['course_language']) ? $params['course_language'] : $this->getDefaultSetting('language.platform_language');
679
        $departmentName = $params['department_name'] ?? null;
680
        $departmentUrl = $this->fixDepartmentUrl($params['department_url'] ?? '');
681
        $diskQuota = $params['disk_quota'] ?? $this->getDefaultSetting('document.default_document_quotum');
682
        $visibility = $params['visibility'] ?? $this->getDefaultSetting('course.courses_default_creation_visibility', Course::OPEN_PLATFORM);
683
        $subscribe = $params['subscribe'] ?? (Course::OPEN_PLATFORM == $visibility);
684
        $unsubscribe = $params['unsubscribe'] ?? false;
685
        $expirationDate = $params['expiration_date'] ?? $this->getFutureExpirationDate();
686
        $teachers = $params['teachers'] ?? [];
687
        $categories = $params['course_categories'] ?? [];
688
        $notifyAdmins = $this->getDefaultSetting('course.send_email_to_admin_when_create_course');
689
690
        $errors = [];
691
        if (empty($code)) {
692
            $errors[] = 'courseSysCode is missing';
693
        }
694
        if (empty($visualCode)) {
695
            $errors[] = 'courseScreenCode is missing';
696
        }
697
        if (empty($directory)) {
698
            $errors[] = 'courseRepository is missing';
699
        }
700
        if (empty($title)) {
701
            $errors[] = 'title is missing';
702
        }
703
        if ($visibility < 0 || $visibility > 4) {
704
            $errors[] = 'visibility is invalid';
705
        }
706
707
        if (!empty($errors)) {
708
            throw new Exception(implode(', ', $errors));
709
        }
710
711
        return [
712
            'title' => $title,
713
            'code' => $code,
714
            'visualCode' => $visualCode,
715
            'directory' => $directory,
716
            'tutorName' => $tutorName,
717
            'courseLanguage' => $courseLanguage,
718
            'departmentName' => $departmentName,
719
            'departmentUrl' => $departmentUrl,
720
            'diskQuota' => $diskQuota,
721
            'visibility' => $visibility,
722
            'subscribe' => $subscribe,
723
            'unsubscribe' => $unsubscribe,
724
            'expirationDate' => new DateTime($expirationDate),
725
            'teachers' => $teachers,
726
            'categories' => $categories,
727
            'notifyAdmins' => $notifyAdmins,
728
        ];
729
    }
730
731
    private function getDefaultSetting(string $name, $default = null)
732
    {
733
        $settingValue = $this->settingsManager->getSetting($name);
734
735
        return null !== $settingValue ? $settingValue : $default;
736
    }
737
738
    private function fixDepartmentUrl(string $url): string
739
    {
740
        if (!empty($url) && !str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
741
            return 'http://'.$url;
742
        }
743
744
        return 'http://' === $url ? '' : $url;
745
    }
746
747
    private function getFutureExpirationDate(): string
748
    {
749
        return (new DateTime())->modify('+1 year')->format('Y-m-d H:i:s');
750
    }
751
752
    private function generateCourseCode(string $title): string
753
    {
754
        $cleanTitle = preg_replace('/[^A-Z0-9]/', '', strtoupper($this->replaceDangerousChar($title)));
755
756
        return substr($cleanTitle, 0, self::MAX_COURSE_LENGTH_CODE);
757
    }
758
759
    private function replaceDangerousChar(string $text): string
760
    {
761
        $encoding = mb_detect_encoding($text, mb_detect_order(), true);
762
        if ('UTF-8' !== $encoding) {
763
            $text = mb_convert_encoding($text, 'UTF-8', $encoding);
764
        }
765
766
        $text = str_replace(' ', '-', $text);
767
        $text = preg_replace('/[^-\w]+/', '', $text);
768
769
        return preg_replace('/\.+$/', '', $text);
770
    }
771
772
    private function handlePostCourseCreation(Course $course, array $params): void
773
    {
774
        if ($params['notifyAdmins'] ?? false) {
775
            $this->sendEmailToAdmin($course);
776
        }
777
778
        $currentUser = $this->security->getUser();
779
        if (!$currentUser instanceof User) {
780
            $currentUser = $this->getFallbackAdminUser();
781
        }
782
783
        $this->eventLoggerHelper->addEvent(
784
            'course_created',
785
            'course_id',
786
            $course->getId(),
787
            null,
788
            $currentUser->getId(),
789
            $course->getId()
790
        );
791
    }
792
793
    private function getFallbackAdminUser(): User
794
    {
795
        $qb = $this->entityManager->createQueryBuilder();
796
797
        $qb->select('u')
798
            ->from(User::class, 'u')
799
            ->where('u.roles LIKE :role')
800
            ->setParameter('role', '%ROLE_ADMIN%')
801
            ->setMaxResults(1);
802
803
        $user = $qb->getQuery()->getOneOrNullResult();
804
805
        if (!$user instanceof User) {
806
            throw new \RuntimeException('No admin user found for fallback.');
807
        }
808
809
        return $user;
810
    }
811
}
812