1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* For licensing terms, see /license.txt */ |
6
|
|
|
|
7
|
|
|
namespace Chamilo\CoreBundle\Command; |
8
|
|
|
|
9
|
|
|
use Chamilo\CoreBundle\Entity\GradebookCategory; |
10
|
|
|
use Chamilo\CoreBundle\Entity\GradebookEvaluation; |
11
|
|
|
use Chamilo\CoreBundle\Entity\GradebookLink; |
12
|
|
|
use Chamilo\CoreBundle\Entity\Session; |
13
|
|
|
use Chamilo\CoreBundle\Entity\User; |
14
|
|
|
use Chamilo\CoreBundle\Repository\SessionRepository; |
15
|
|
|
use Chamilo\CoreBundle\ServiceHelper\MessageHelper; |
16
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
17
|
|
|
use Exception; |
18
|
|
|
use Symfony\Component\Console\Command\Command; |
19
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
20
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
21
|
|
|
use Symfony\Component\Console\Input\InputOption; |
22
|
|
|
use Symfony\Contracts\Translation\TranslatorInterface; |
23
|
|
|
|
24
|
|
|
class SessionRepetitionCommand extends Command |
25
|
|
|
{ |
26
|
|
|
protected static $defaultName = 'app:session-repetition'; |
27
|
|
|
private string $baseUrl; |
28
|
|
|
|
29
|
|
|
public function __construct( |
30
|
|
|
private readonly SessionRepository $sessionRepository, |
31
|
|
|
private readonly EntityManagerInterface $entityManager, |
32
|
|
|
private readonly TranslatorInterface $translator, |
33
|
|
|
private readonly MessageHelper $messageHelper |
34
|
|
|
) { |
35
|
|
|
parent::__construct(); |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
protected function configure(): void |
39
|
|
|
{ |
40
|
|
|
$this |
41
|
|
|
->setDescription('Automatically duplicates sessions that meet the repetition criteria.') |
42
|
|
|
->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') |
43
|
|
|
->addOption('base-url', null, InputOption::VALUE_REQUIRED, 'Base URL for generating session links'); |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int |
47
|
|
|
{ |
48
|
|
|
$debug = $input->getOption('debug'); |
49
|
|
|
$this->baseUrl = $input->getOption('base-url'); |
50
|
|
|
|
51
|
|
|
if (!$this->baseUrl) { |
52
|
|
|
$output->writeln('<error>Error: You must provide --base-url</error>'); |
53
|
|
|
return Command::FAILURE; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
if ($debug) { |
57
|
|
|
$output->writeln('<info>Debug mode enabled</info>'); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// Find sessions that meet the repetition criteria |
61
|
|
|
$sessions = $this->sessionRepository->findSessionsWithoutChildAndReadyForRepetition(); |
62
|
|
|
|
63
|
|
|
if ($debug) { |
64
|
|
|
$output->writeln(sprintf('Found %d session(s) ready for repetition.', count($sessions))); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
foreach ($sessions as $session) { |
68
|
|
|
if ($debug) { |
69
|
|
|
$output->writeln(sprintf('Processing session: %d', $session->getId())); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
// Duplicate session |
73
|
|
|
$newSession = $this->duplicateSession($session, $debug, $output); |
74
|
|
|
|
75
|
|
|
// Notify general coach of the new session |
76
|
|
|
$this->notifyGeneralCoach($newSession, $debug, $output); |
77
|
|
|
|
78
|
|
|
$output->writeln('Created new session: ' . $newSession->getId() . ' from session: ' . $session->getId()); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
return Command::SUCCESS; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Duplicates a session and creates a new session with adjusted dates. |
86
|
|
|
* @throws Exception |
87
|
|
|
*/ |
88
|
|
|
private function duplicateSession(Session $session, bool $debug, OutputInterface $output): Session |
89
|
|
|
{ |
90
|
|
|
// Calculate new session dates based on the duration of the original session |
91
|
|
|
$duration = $session->getAccessEndDate()->diff($session->getAccessStartDate())->days; |
|
|
|
|
92
|
|
|
$newStartDate = (clone $session->getAccessEndDate())->modify('+1 day'); |
93
|
|
|
$newEndDate = (clone $newStartDate)->modify("+{$duration} days"); |
94
|
|
|
|
95
|
|
|
if ($debug) { |
96
|
|
|
$output->writeln(sprintf( |
97
|
|
|
'Duplicating session %d. New start date: %s, New end date: %s', |
98
|
|
|
$session->getId(), |
99
|
|
|
$newStartDate->format('Y-m-d H:i:s'), |
100
|
|
|
$newEndDate->format('Y-m-d H:i:s') |
101
|
|
|
)); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
// Create a new session with the same details as the original session |
105
|
|
|
$newSession = new Session(); |
106
|
|
|
$newSession |
107
|
|
|
->setTitle($session->getTitle() . ' (Repetition ' . $session->getId() . ' - ' . time() . ')') |
108
|
|
|
->setAccessStartDate($newStartDate) |
109
|
|
|
->setAccessEndDate($newEndDate) |
110
|
|
|
->setDisplayStartDate($newStartDate) |
111
|
|
|
->setDisplayEndDate($newEndDate) |
112
|
|
|
->setCoachAccessStartDate($newStartDate) |
113
|
|
|
->setCoachAccessEndDate($newEndDate) |
114
|
|
|
->setVisibility($session->getVisibility()) |
115
|
|
|
->setDuration(0) |
116
|
|
|
->setDescription($session->getDescription() ?? '') |
117
|
|
|
->setShowDescription($session->getShowDescription() ?? false) |
118
|
|
|
->setCategory($session->getCategory()) |
119
|
|
|
->setPromotion($session->getPromotion()) |
120
|
|
|
->setDaysToReinscription($session->getDaysToReinscription()) |
121
|
|
|
->setDaysToNewRepetition($session->getDaysToNewRepetition()) |
122
|
|
|
->setParentId($session->getId()) |
123
|
|
|
->setLastRepetition(false); |
124
|
|
|
|
125
|
|
|
// Copy the AccessUrls from the original session |
126
|
|
|
foreach ($session->getUrls() as $accessUrl) { |
127
|
|
|
$newSession->addAccessUrl($accessUrl->getUrl()); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
// Save the new session |
131
|
|
|
$this->entityManager->persist($newSession); |
132
|
|
|
$this->entityManager->flush(); |
133
|
|
|
|
134
|
|
|
if ($debug) { |
135
|
|
|
$output->writeln(sprintf('New session %d created successfully.', $newSession->getId())); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
$courses = $session->getCourses()->toArray(); |
139
|
|
|
|
140
|
|
|
if ($debug) { |
141
|
|
|
$output->writeln('Courses retrieved: ' . count($courses)); |
142
|
|
|
foreach ($courses as $index => $sessionRelCourse) { |
143
|
|
|
$course = $sessionRelCourse->getCourse(); |
144
|
|
|
$output->writeln(sprintf( |
145
|
|
|
'Course #%d: %s (Course ID: %s)', |
146
|
|
|
$index + 1, |
147
|
|
|
$course ? $course->getTitle() : 'NULL', |
148
|
|
|
$course ? $course->getId() : 'NULL' |
149
|
|
|
)); |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
// Extract course IDs |
154
|
|
|
$courseList = array_map(function ($sessionRelCourse) { |
155
|
|
|
$course = $sessionRelCourse->getCourse(); |
156
|
|
|
return $course?->getId(); |
157
|
|
|
}, $courses); |
158
|
|
|
|
159
|
|
|
// Remove null values |
160
|
|
|
$courseList = array_filter($courseList); |
161
|
|
|
|
162
|
|
|
if ($debug) { |
163
|
|
|
$output->writeln(sprintf( |
164
|
|
|
'Extracted course IDs: %s', |
165
|
|
|
json_encode($courseList) |
166
|
|
|
)); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
if (empty($courseList)) { |
170
|
|
|
$output->writeln(sprintf('Warning: No courses found in the original session %d.', $session->getId())); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
// Add courses to the new session |
174
|
|
|
$courseCount = 0; |
175
|
|
|
foreach ($courses as $sessionRelCourse) { |
176
|
|
|
$course = $sessionRelCourse->getCourse(); |
177
|
|
|
if ($course) { |
178
|
|
|
$newSession->addCourse($course); |
179
|
|
|
$this->entityManager->persist($newSession); |
180
|
|
|
|
181
|
|
|
if ($debug) { |
182
|
|
|
$output->writeln(sprintf('Added course ID %d to session ID %d.', $course->getId(), $newSession->getId())); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$this->copyEvaluationsAndCategories($course->getId(), $session->getId(), $newSession->getId(), $debug, $output); |
186
|
|
|
|
187
|
|
|
$courseCount++; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
foreach ($session->getGeneralCoaches() as $coach) { |
192
|
|
|
$newSession->addGeneralCoach($coach); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
$newSession->setNbrCourses($courseCount); |
196
|
|
|
$this->entityManager->persist($newSession); |
197
|
|
|
|
198
|
|
|
$this->entityManager->flush(); |
199
|
|
|
|
200
|
|
|
return $newSession; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Notifies the general coach of the session about the new repetition. |
205
|
|
|
*/ |
206
|
|
|
private function notifyGeneralCoach(Session $newSession, bool $debug, OutputInterface $output): void |
207
|
|
|
{ |
208
|
|
|
$generalCoach = $newSession->getGeneralCoaches()->first(); |
209
|
|
|
if ($generalCoach) { |
210
|
|
|
$messageSubject = $this->translator->trans('New Session Repetition Created'); |
211
|
|
|
$messageContent = sprintf( |
212
|
|
|
'A new repetition of the session "%s" has been created. Please review the details: %s', |
213
|
|
|
$newSession->getTitle(), |
214
|
|
|
$this->generateSessionSummaryLink($newSession) |
215
|
|
|
); |
216
|
|
|
|
217
|
|
|
if ($debug) { |
218
|
|
|
$output->writeln(sprintf('Notifying coach (ID: %d) for session %d', $generalCoach->getId(), $newSession->getId())); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
$senderId = $this->getFirstAdminId(); |
222
|
|
|
$this->messageHelper->sendMessageSimple( |
223
|
|
|
$generalCoach->getId(), |
224
|
|
|
$messageSubject, |
225
|
|
|
$messageContent, |
226
|
|
|
$senderId |
227
|
|
|
); |
228
|
|
|
|
229
|
|
|
if ($debug) { |
230
|
|
|
$output->writeln('Notification sent using MessageHelper.'); |
231
|
|
|
} |
232
|
|
|
} elseif ($debug) { |
233
|
|
|
$output->writeln('No general coach found for session ' . $newSession->getId()); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
private function getFirstAdminId(): int |
238
|
|
|
{ |
239
|
|
|
$admin = $this->entityManager->getRepository(User::class)->findOneBy([]); |
240
|
|
|
|
241
|
|
|
return $admin && ($admin->hasRole('ROLE_ADMIN') || $admin->hasRole('ROLE_SUPER_ADMIN')) |
242
|
|
|
? $admin->getId() |
243
|
|
|
: 1; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Generates a link to the session summary page. |
248
|
|
|
*/ |
249
|
|
|
private function generateSessionSummaryLink(Session $session): string |
250
|
|
|
{ |
251
|
|
|
return sprintf('%s/main/session/resume_session.php?id_session=%d', $this->baseUrl, $session->getId()); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Copies gradebook categories, evaluations, and links from the old session to the new session. |
256
|
|
|
*/ |
257
|
|
|
private function copyEvaluationsAndCategories( |
258
|
|
|
int $courseId, |
259
|
|
|
int $oldSessionId, |
260
|
|
|
int $newSessionId, |
261
|
|
|
bool $debug, |
262
|
|
|
OutputInterface $output |
263
|
|
|
): void { |
264
|
|
|
// Get existing categories of the original course and session |
265
|
|
|
$categories = $this->entityManager->getRepository(GradebookCategory::class) |
266
|
|
|
->findBy(['course' => $courseId, 'session' => $oldSessionId]); |
267
|
|
|
|
268
|
|
|
if ($debug) { |
269
|
|
|
$output->writeln(sprintf('Found %d category(ies) for course ID %d in session ID %d.', count($categories), $courseId, $oldSessionId)); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
foreach ($categories as $category) { |
273
|
|
|
// Create new category for the new session |
274
|
|
|
$newCategory = new GradebookCategory(); |
275
|
|
|
$newCategory->setTitle($category->getTitle()) |
276
|
|
|
->setDescription($category->getDescription()) |
277
|
|
|
->setWeight($category->getWeight()) |
278
|
|
|
->setVisible($category->getVisible()) |
279
|
|
|
->setCertifMinScore($category->getCertifMinScore()) |
280
|
|
|
->setGenerateCertificates($category->getGenerateCertificates()) |
281
|
|
|
->setIsRequirement($category->getIsRequirement()) |
282
|
|
|
->setCourse($category->getCourse()) |
283
|
|
|
->setSession($this->entityManager->getReference(Session::class, $newSessionId)) |
284
|
|
|
->setParent($category->getParent()); |
285
|
|
|
|
286
|
|
|
$this->entityManager->persist($newCategory); |
287
|
|
|
$this->entityManager->flush(); |
288
|
|
|
|
289
|
|
|
if ($debug) { |
290
|
|
|
$output->writeln(sprintf('Created new category ID %d for session ID %d.', $newCategory->getId(), $newSessionId)); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
// Copy links |
294
|
|
|
$links = $this->entityManager->getRepository(GradebookLink::class) |
295
|
|
|
->findBy(['category' => $category->getId()]); |
296
|
|
|
|
297
|
|
|
foreach ($links as $link) { |
298
|
|
|
$newLink = clone $link; |
299
|
|
|
$newLink->setCategory($newCategory); |
300
|
|
|
$this->entityManager->persist($newLink); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
// Copy evaluations |
304
|
|
|
$evaluations = $this->entityManager->getRepository(GradebookEvaluation::class) |
305
|
|
|
->findBy(['category' => $category->getId()]); |
306
|
|
|
|
307
|
|
|
foreach ($evaluations as $evaluation) { |
308
|
|
|
$newEvaluation = clone $evaluation; |
309
|
|
|
$newEvaluation->setCategory($newCategory); |
310
|
|
|
$this->entityManager->persist($newEvaluation); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
$this->entityManager->flush(); |
314
|
|
|
|
315
|
|
|
if ($debug) { |
316
|
|
|
$output->writeln(sprintf('Copied links and evaluations for category ID %d to new category ID %d.', $category->getId(), $newCategory->getId())); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.