Merge remote-tracking branch 'upstream/master' into storm-22103

pull/5859/head
christianbeeznst 10 months ago
commit 423482fcfc
  1. 49
      assets/vue/graphql/queries/CourseRelUser.js
  2. 98
      assets/vue/views/user/courses/List.vue
  3. 42
      package.json
  4. 13
      public/main/skills/skill_edit.php
  5. 300
      src/CoreBundle/Command/LpProgressReminderCommand.php
  6. 7
      src/CoreBundle/Entity/CourseRelUser.php
  7. 3
      src/CoreBundle/Entity/SessionRelCourseRelUser.php
  8. 9
      src/CoreBundle/Entity/TrackEDefault.php
  9. 42
      src/CoreBundle/Repository/CourseRelUserRepository.php
  10. 17
      src/CoreBundle/Repository/ExtraFieldValuesRepository.php
  11. 42
      src/CoreBundle/Repository/SessionRelCourseRelUserRepository.php
  12. 64
      src/CoreBundle/Repository/TrackEDefaultRepository.php
  13. 16
      src/CoreBundle/Resources/views/Mailer/Legacy/lp_progress_reminder_body.html.twig
  14. 1
      src/CoreBundle/Resources/views/Mailer/Legacy/lp_progress_reminder_subject.html.twig
  15. 587
      yarn.lock

@ -1,32 +1,37 @@
import gql from 'graphql-tag';
export const GET_COURSE_REL_USER = gql`
query getCourses($user: String!) {
courseRelUsers(user: $user) {
edges {
query getCourses($user: String!, $first: Int!, $after: String) {
courseRelUsers(user: $user, first: $first, after: $after) {
edges {
cursor
node {
course {
_id,
title,
illustrationUrl,
duration,
users(status: 1, first: 4) {
edges {
node {
course {
_id,
title,
illustrationUrl,
duration,
users(status: 1, first: 4) {
edges {
node {
id
status
user {
illustrationUrl,
username,
fullName
}
}
}
}
}
id
status
user {
illustrationUrl,
username,
fullName
}
}
}
}
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`;

@ -4,7 +4,7 @@
<hr />
<div
v-if="isLoading"
v-if="isLoading && courses.length === 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<Skeleton height="16rem" />
@ -23,10 +23,11 @@
</div>
<div
v-if="!isLoading && courses.length > 0"
v-if="courses.length > 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<CourseCardList :courses="courses" />
<div ref="lastCourseRef"></div>
</div>
<EmptyState
v-else-if="!isLoading && 0 === courses.length"
@ -37,7 +38,7 @@
</template>
<script setup>
import { onMounted, computed } from "vue"
import { ref, watch, onMounted, nextTick } from "vue"
import { useQuery } from "@vue/apollo-composable"
import { useI18n } from "vue-i18n"
import { GET_COURSE_REL_USER } from "../../../graphql/queries/CourseRelUser.js"
@ -50,15 +51,96 @@ import { useSecurityStore } from "../../../store/securityStore"
const securityStore = useSecurityStore()
const { t } = useI18n()
const { result, loading, refetch } = useQuery(GET_COURSE_REL_USER, () => ({
const courses = ref([])
const isLoading = ref(false)
const endCursor = ref(null)
const hasMore = ref(true)
const lastCourseRef = ref(null)
const { result, fetchMore } = useQuery(GET_COURSE_REL_USER, {
user: securityStore.user["@id"],
}))
first: 30,
after: null,
})
watch(result, (newResult) => {
if (newResult?.courseRelUsers) {
const newCourses = newResult.courseRelUsers.edges.map(({ node }) => node.course)
const filteredCourses = newCourses.filter(
(newCourse) => !courses.value.some((existingCourse) => existingCourse._id === newCourse._id)
)
courses.value.push(...filteredCourses)
endCursor.value = newResult.courseRelUsers.pageInfo.endCursor
hasMore.value = newResult.courseRelUsers.pageInfo.hasNextPage
nextTick(() => {
if (lastCourseRef.value) {
observer.observe(lastCourseRef.value)
}
})
}
isLoading.value = false
})
const isLoading = computed(() => loading.value)
const loadMoreCourses = () => {
if (!hasMore.value || isLoading.value) return
isLoading.value = true
const courses = computed(() => result.value?.courseRelUsers.edges.map(({ node }) => node.course) ?? [])
fetchMore({
variables: {
user: securityStore.user["@id"],
first: 10,
after: endCursor.value,
},
updateQuery: (previousResult, {fetchMoreResult}) => {
if (!fetchMoreResult) return previousResult
const newCourses = fetchMoreResult.courseRelUsers.edges.map(({ node }) => node.course)
const filteredCourses = newCourses.filter(
(newCourse) => !courses.value.some((existingCourse) => existingCourse._id === newCourse._id)
)
courses.value.push(...filteredCourses)
endCursor.value = fetchMoreResult.courseRelUsers.pageInfo.endCursor
hasMore.value = fetchMoreResult.courseRelUsers.pageInfo.hasNextPage
return {
...previousResult,
courseRelUsers: {
...fetchMoreResult.courseRelUsers,
edges: [...previousResult.courseRelUsers.edges, ...fetchMoreResult.courseRelUsers.edges],
},
}
},
}).finally(() => {
isLoading.value = false
})
}
let observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMoreCourses();
}
}, {
rootMargin: '300px',
})
onMounted(() => {
refetch()
courses.value = []
endCursor.value = null
hasMore.value = true
isLoading.value = false
if (observer) observer.disconnect()
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMoreCourses()
}
}, {
rootMargin: '300px',
})
loadMoreCourses()
})
</script>

@ -9,7 +9,7 @@
"build": "encore production --progress"
},
"dependencies": {
"@apollo/client": "^3.11.8",
"@apollo/client": "^3.11.10",
"@fancyapps/fancybox": "^3.5.7",
"@fullcalendar/core": "^5.11.5",
"@fullcalendar/daygrid": "^5.11.5",
@ -17,7 +17,7 @@
"@fullcalendar/timegrid": "^5.11.5",
"@fullcalendar/vue3": "^5.11.5",
"@tinymce/tinymce-vue": "^5.1.1",
"@types/lodash": "^4.17.12",
"@types/lodash": "^4.17.13",
"@uppy/audio": "^1.1.9",
"@uppy/core": "^2.1.10",
"@uppy/dashboard": "^2.1.4",
@ -52,12 +52,12 @@
"glob-all": "^3.3.1",
"graphql": "^16.9.0",
"graphql-tag": "^2.12.6",
"highlight.js": "^11.7.0",
"highlight.js": "^11.10.0",
"hljs": "^6.2.3",
"html2canvas": "^1.4.1",
"image-map-resizer": "^1.0.10",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
"jquery-ui": "^1.14.1",
"jquery-ui-dist": "^1.13.3",
"jquery-ui-timepicker-addon": "^1.6.3",
"jquery-ui-touch-punch": "^0.2.3",
@ -77,7 +77,7 @@
"mxgraph": "^4.2.2",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"path": "^0.12.7",
"pinia": "^2.2.4",
"pinia": "^2.2.6",
"pretty-bytes": "^5.6.0",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
@ -88,7 +88,7 @@
"recordrtc": "^5.6.2",
"select2": "^4.1.0-rc.0",
"signature_pad": "^3.0.0-beta.4",
"sortablejs": "^1.15.3",
"sortablejs": "^1.15.4",
"sweetalert2": "^11.6.15",
"textcomplete": "^0.18.2",
"timeago": "^1.6.7",
@ -105,9 +105,9 @@
"vuex-map-fields": "^1.4.1"
},
"devDependencies": {
"@babel/core": "^7.25.9",
"@babel/preset-env": "^7.25.9",
"@eslint/js": "^9.13.0",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@eslint/js": "^9.15.0",
"@mdi/font": "^7.4.47",
"@symfony/webpack-encore": "^5.0.1",
"@tailwindcss/forms": "^0.5.9",
@ -115,28 +115,28 @@
"@tailwindcss/typography": "^0.5.15",
"@vue/compiler-sfc": "^3.5.12",
"autoprefixer": "^10.4.20",
"core-js": "3.38.1",
"core-js": "3.39.0",
"deepmerge": "^4.3.1",
"eslint": "^9.13.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.29.1",
"eslint-plugin-vue": "^9.31.0",
"file-loader": "^6.2.0",
"globals": "^15.11.0",
"postcss": "^8.4.47",
"globals": "^15.12.0",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"postcss-prefix-selector": "^2.1.0",
"prettier": "3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
"prettier-plugin-tailwindcss": "^0.6.9",
"purgecss-webpack-plugin": "^5.0.0",
"sass": "^1.80.4",
"sass-loader": "^16.0.2",
"tailwindcss": "^3.4.14",
"sass": "^1.81.0",
"sass-loader": "^16.0.3",
"tailwindcss": "^3.4.15",
"ts-loader": "^9.5.1",
"typescript": "^5.6.3",
"vue": "^3.5.12",
"typescript": "^5.7.2",
"vue": "^3.5.13",
"vue-eslint-parser": "^9.4.3",
"vue-loader": "^17.4.2",
"webpack": "^5.95.0",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0",
"webpack-notifier": "^1.15.0"

@ -6,6 +6,10 @@
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
use Chamilo\CoreBundle\Entity\Skill;
use Chamilo\CoreBundle\Framework\Container;
$cidReset = true;
require_once __DIR__.'/../inc/global.inc.php';
@ -25,6 +29,11 @@ $objSkill = new SkillModel();
$objGradebook = new Gradebook();
$skillInfo = $objSkill->getSkillInfo($skillId);
$em = Database::getManager();
$skill = $em->find(Skill::class, $skillId);
$skill->setLocale(Container::getParameter('locale'));
$em->refresh($skill);
if (empty($skillInfo)) {
api_not_allowed(true);
}
@ -33,8 +42,8 @@ $allGradebooks = $objGradebook->find('all');
$skillDefaultInfo = [
'id' => $skillInfo['id'],
'title' => $skillInfo['title'],
'short_code' => $skillInfo['short_code'],
'title' => $skill->getTitle(),
'short_code' => $skill->getShortCode(),
'description' => $skillInfo['description'],
'parent_id' => $skillInfo['extra']['parent_id'],
'criteria' => $skillInfo['criteria'],

@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Repository\CourseRelUserRepository;
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use Chamilo\CoreBundle\Repository\SessionRelCourseRelUserRepository;
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
use Chamilo\CoreBundle\ServiceHelper\MessageHelper;
use DateTime;
use DateTimeZone;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class LpProgressReminderCommand extends Command
{
protected static $defaultName = 'app:lp-progress-reminder';
private const NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION = 3;
public function __construct(
private readonly CourseRepository $courseRepository,
private readonly CourseRelUserRepository $courseRelUserRepository,
private readonly SessionRelCourseRelUserRepository $sessionRelCourseRelUserRepository,
private readonly ExtraFieldValuesRepository $extraFieldValuesRepository,
private readonly TrackEDefaultRepository $trackEDefaultRepository,
private readonly UserRepository $userRepository,
private readonly Environment $twig,
private readonly TranslatorInterface $translator,
private readonly MessageHelper $messageHelper,
private readonly UrlGeneratorInterface $urlGenerator
) {
parent::__construct();
}
protected function configure()
{
$this
->setDescription('Send LP progress reminders to users based on "number_of_days_for_completion".')
->addOption(
'debug',
null,
InputOption::VALUE_NONE,
'If set, will output detailed debug information'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$debugMode = $input->getOption('debug');
$output->writeln('Starting the LP progress reminder process...');
// Retrieve LPs with completion days
$lpItems = $this->extraFieldValuesRepository->getLpIdWithDaysForCompletion();
if ($debugMode && !empty($lpItems)) {
$output->writeln('LP Items retrieved: ' . print_r($lpItems, true));
}
if (empty($lpItems)) {
$output->writeln('No learning paths with days for completion found.');
return Command::SUCCESS;
}
$lpMap = [];
foreach ($lpItems as $lpItem) {
$lpMap[$lpItem['lp_id']] = $lpItem['ndays'];
}
$lpIds = array_keys($lpMap);
// Retrieve all courses from the CourseRepository
$courses = $this->courseRepository->findAll();
if ($debugMode && !empty($courses)) {
$output->writeln('Courses retrieved: ' . count($courses));
}
foreach ($courses as $course) {
$courseId = $course->getId();
// Retrieve users for the course (without session)
$courseUsers = $this->courseRelUserRepository->getCourseUsers($courseId, $lpIds);
// Retrieve users for the course session
$sessionCourseUsers = $this->sessionRelCourseRelUserRepository->getSessionCourseUsers($courseId, $lpIds);
if ($debugMode && (!empty($courseUsers) || !empty($sessionCourseUsers))) {
$output->writeln('Processing course ID: ' . $courseId);
if (!empty($courseUsers)) {
$output->writeln('Course users retrieved: ' . count($courseUsers));
//$output->writeln('Course retrieved: ' . print_r($courseUsers, true));
}
if (!empty($sessionCourseUsers)) {
$output->writeln('Session users retrieved: ' . count($sessionCourseUsers));
//$output->writeln('Session retrieved: ' . print_r($sessionCourseUsers, true));
}
}
// Process users from the main course (sessionId = 0 or null)
$this->processCourseUsers($courseUsers, $lpMap, $courseId, $debugMode);
// Process users from the course session (sessionId > 0)
$this->processCourseUsers($sessionCourseUsers, $lpMap, $courseId, $debugMode, true);
}
$output->writeln('LP progress reminder process finished.');
return Command::SUCCESS;
}
/**
* Processes users from a course or session to check if a reminder needs to be sent.
*/
private function processCourseUsers(array $users, array $lpItems, int $courseId, bool $debugMode = false, bool $checkSession = false): void
{
foreach ($users as $user) {
$userId = $user['userId'];
$courseTitle = $user['courseTitle'];
$lpId = $user['lpId'];
$progress = (int) $user['progress'];
$sessionId = $checkSession ? ($user['sessionId'] ?? 0) : 0;
if ($lpId === null) {
foreach ($lpItems as $lpId => $nbDaysForLpCompletion) {
$this->sendReminderIfNeeded(
$userId,
$courseTitle,
$courseId,
$sessionId,
(int) $nbDaysForLpCompletion,
$debugMode,
$lpId,
$progress
);
}
} else {
$nbDaysForLpCompletion = (int) ($lpItems[$lpId] ?? self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION);
$this->sendReminderIfNeeded(
$userId,
$courseTitle,
$courseId,
$sessionId,
$nbDaysForLpCompletion,
$debugMode,
$lpId,
$progress
);
}
}
}
/**
* Sends a progress reminder to a user if the conditions for reminder timing are met,
* based on their registration date and learning path completion criteria.
*/
private function sendReminderIfNeeded(
int $userId,
string $courseTitle,
int $courseId,
int $sessionId,
int $nbDaysForLpCompletion,
bool $debugMode,
?int $lpId,
int $progress
): void {
$registrationDate = $this->trackEDefaultRepository->getUserCourseRegistrationAt($courseId, $userId, $sessionId);
if (!$registrationDate) {
if ($debugMode) {
echo "No registration date found for user $userId in course $courseId (session ID: $sessionId).\n";
}
return;
}
if ($debugMode) {
$sessionInfo = $sessionId > 0 ? "in session ID $sessionId" : "without a session";
echo "Registration date: {$registrationDate->format('Y-m-d H:i:s')}, Days for completion: $nbDaysForLpCompletion, LP ID: {$lpId}, $sessionInfo\n";
}
if ($this->isTimeToRemindUser($registrationDate, $nbDaysForLpCompletion)) {
$nbRemind = $this->getNbReminder($registrationDate, $nbDaysForLpCompletion);
if ($debugMode) {
echo "Sending reminder to user $userId for course $courseTitle (LP ID: {$lpId})\n";
}
$this->logReminderSent($userId, $courseTitle, $nbRemind, $debugMode, $lpId, $sessionId);
$this->sendLpReminder($userId, $courseTitle, $progress, $registrationDate, $nbRemind);
}
}
/**
* Logs the reminder details if debug mode is enabled.
*/
private function logReminderSent(int $userId, string $courseTitle, int $nbRemind, bool $debugMode, int $lpId, int $sessionId = 0): void
{
if ($debugMode) {
$sessionInfo = $sessionId > 0 ? sprintf("in session ID %d", $sessionId) : "without a session";
echo sprintf(
"Reminder number %d sent to user ID %d for the course %s (LP ID: %d) %s.\n",
$nbRemind,
$userId,
$courseTitle,
$lpId,
$sessionInfo
);
}
}
/**
* Calculates the number of reminders to be sent based on registration date and days for completion.
*/
private function getNbReminder(DateTime $registrationDate, int $nbDaysForLpCompletion): int
{
$reminderStartDate = (clone $registrationDate)->modify("+$nbDaysForLpCompletion days");
$currentDate = new DateTime('now', new DateTimeZone('UTC'));
$reminderStartDate->setTime(0, 0, 0);
$currentDate->setTime(0, 0, 0);
$interval = $reminderStartDate->diff($currentDate);
$diffDays = (int) $interval->format('%a');
return (int) floor($diffDays / self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION) + 1;
}
/**
* Checks if it is time to remind the user based on their registration date and LP completion days.
*/
private function isTimeToRemindUser(DateTime $registrationDate, int $nbDaysForLpCompletion): bool
{
$reminderStartDate = (clone $registrationDate)->modify("+$nbDaysForLpCompletion days");
$reminderStartDate->setTime(0, 0, 0);
$currentDate = new DateTime('now', new DateTimeZone('UTC'));
$currentDate->setTime(0, 0, 0);
$interval = $reminderStartDate->diff($currentDate);
$diffDays = (int) $interval->format('%a');
return ($diffDays >= self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION &&
$diffDays % self::NUMBER_OF_DAYS_TO_RESEND_NOTIFICATION === 0) || $diffDays === 0;
}
/**
* Sends a reminder email to the user regarding their LP progress.
*/
private function sendLpReminder(int $toUserId, string $courseName, int $lpProgress, DateTime $registrationDate, int $nbRemind): bool
{
$user = $this->userRepository->find($toUserId);
if (!$user) {
throw new \Exception("User not found");
}
$platformUrl = $this->urlGenerator->generate('index', [], UrlGeneratorInterface::ABSOLUTE_URL);
$recoverPasswordUrl = $platformUrl.'/main/auth/lostPassword.php';
$trainingCenterName = 'Your Training Center';
$trainers = 'Trainer Name';
$hello = $this->translator->trans("Hello %s");
$youAreRegCourse = $this->translator->trans("You are registered in the training %s since the %s");
$thisMessageIsAbout = $this->translator->trans("You are receiving this message because you have completed a learning path with a %s progress of your training.<br/>Your progress must be 100 to consider that your training was carried out.<br/>If you have the slightest problem, you should contact with your trainer.");
$stepsToRemind = $this->translator->trans("As a reminder, to access the training platform:<br/>1. Connect to the platform at the address: %s <br/>2. Then enter: <br/>Your username: %s <br/>Your password: This was emailed to you.<br/>if you forgot it and can't find it, you can retrieve it by going to %s <br/><br/>Thank you for doing what is necessary.");
$lpRemindFooter = $this->translator->trans("The training center<p>%s</p>Trainers:<br/>%s");
$hello = sprintf($hello, $user->getFullName());
$youAreRegCourse = sprintf($youAreRegCourse, $courseName, $registrationDate->format('Y-m-d'));
$thisMessageIsAbout = sprintf($thisMessageIsAbout, $lpProgress);
$stepsToRemind = sprintf($stepsToRemind, $platformUrl, $user->getUsername(), $recoverPasswordUrl);
$lpRemindFooter = sprintf($lpRemindFooter, $trainingCenterName, $trainers);
$messageContent = $this->twig->render('@ChamiloCore/Mailer/Legacy/lp_progress_reminder_body.html.twig', [
'HelloX' => $hello,
'YouAreRegCourseXFromDateX' => $youAreRegCourse,
'ThisMessageIsAboutX' => $thisMessageIsAbout,
'StepsToRemindX' => $stepsToRemind,
'LpRemindFooterX' => $lpRemindFooter,
]);
try {
$this->messageHelper->sendMessageSimple(
$toUserId,
sprintf("Reminder number %d for the course %s", $nbRemind, $courseName),
$messageContent
);
return true;
} catch (\Exception $e) {
throw new \Exception('Error sending reminder: ' . $e->getMessage());
}
}
}

@ -12,6 +12,7 @@ use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Chamilo\CoreBundle\Repository\CourseRelUserRepository;
use Chamilo\CoreBundle\Traits\UserTrait;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
@ -34,9 +35,9 @@ use Symfony\Component\Validator\Constraints as Assert;
security: "is_granted('ROLE_USER')"
)]
#[ORM\Table(name: 'course_rel_user')]
#[ORM\Index(name: 'course_rel_user_user_id', columns: ['id', 'user_id'])]
#[ORM\Index(name: 'course_rel_user_c_id_user_id', columns: ['id', 'c_id', 'user_id'])]
#[ORM\Entity]
#[ORM\Index(columns: ['id', 'user_id'], name: 'course_rel_user_user_id')]
#[ORM\Index(columns: ['id', 'c_id', 'user_id'], name: 'course_rel_user_c_id_user_id')]
#[ORM\Entity(repositoryClass: CourseRelUserRepository::class)]
#[ApiFilter(
filterClass: SearchFilter::class,
properties: [

@ -10,6 +10,7 @@ use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use Chamilo\CoreBundle\Repository\SessionRelCourseRelUserRepository;
use Chamilo\CoreBundle\Traits\UserTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
@ -32,7 +33,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Index(columns: ['user_id'], name: 'idx_session_rel_course_rel_user_id_user')]
#[ORM\Index(columns: ['c_id'], name: 'idx_session_rel_course_rel_user_course_id')]
#[ORM\UniqueConstraint(name: 'course_session_unique', columns: ['session_id', 'c_id', 'user_id', 'status'])]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: SessionRelCourseRelUserRepository::class)]
#[ApiFilter(
filterClass: SearchFilter::class,
properties: [

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
@ -13,10 +14,10 @@ use Doctrine\ORM\Mapping as ORM;
* TrackEDefault.
*/
#[ORM\Table(name: 'track_e_default')]
#[ORM\Index(name: 'course', columns: ['c_id'])]
#[ORM\Index(name: 'session', columns: ['session_id'])]
#[ORM\Index(name: 'idx_default_user_id', columns: ['default_user_id'])]
#[ORM\Entity]
#[ORM\Index(columns: ['c_id'], name: 'course')]
#[ORM\Index(columns: ['session_id'], name: 'session')]
#[ORM\Index(columns: ['default_user_id'], name: 'idx_default_user_id')]
#[ORM\Entity(repositoryClass: TrackEDefaultRepository::class)]
class TrackEDefault
{
#[ORM\Column(name: 'default_id', type: 'integer')]

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpView;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class CourseRelUserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CourseRelUser::class);
}
/**
* Retrieves users from a course and their LP progress (without session).
*/
public function getCourseUsers(int $courseId, array $lpIds): array
{
$qb = $this->createQueryBuilder('cu')
->select('u.id AS userId, c.title AS courseTitle, lp.iid AS lpId, COALESCE(lpv.progress, 0) AS progress')
->innerJoin('cu.user', 'u')
->innerJoin('cu.course', 'c')
->leftJoin(CLpView::class, 'lpv', 'WITH', 'lpv.user = u.id AND lpv.course = cu.course AND lpv.lp IN (:lpIds)')
->leftJoin(CLp::class, 'lp', 'WITH', 'lp.iid IN (:lpIds)')
->innerJoin('lp.resourceNode', 'rn')
->where('cu.course = :courseId')
->andWhere('rn.parent = c.resourceNode')
->andWhere('(lpv.progress < 100 OR lpv.progress IS NULL)')
->setParameter('courseId', $courseId)
->setParameter('lpIds', $lpIds);
return $qb->getQuery()->getResult();
}
}

@ -9,6 +9,7 @@ namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldItemInterface;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\CourseBundle\Entity\CLp;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\Expr\Join;
@ -180,4 +181,20 @@ class ExtraFieldValuesRepository extends ServiceEntityRepository
return $query->getOneOrNullResult();
}
/**
* Retrieves the LP IDs that have a value for 'number_of_days_for_completion'.
*/
public function getLpIdWithDaysForCompletion(): array
{
$qb = $this->createQueryBuilder('efv')
->select('efv.itemId as lp_id, efv.fieldValue as ndays')
->innerJoin('efv.field', 'ef')
->innerJoin(CLp::class, 'lp', 'WITH', 'lp.iid = efv.itemId')
->where('ef.variable = :variable')
->andWhere('efv.fieldValue > 0')
->setParameter('variable', 'number_of_days_for_completion');
return $qb->getQuery()->getResult();
}
}

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpView;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class SessionRelCourseRelUserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SessionRelCourseRelUser::class);
}
/**
* Retrieves users from a course session and their LP progress.
*/
public function getSessionCourseUsers(int $courseId, array $lpIds): array
{
$qb = $this->createQueryBuilder('scu')
->select('u.id AS userId, c.title AS courseTitle, lp.iid AS lpId, COALESCE(lpv.progress, 0) AS progress, IDENTITY(scu.session) AS sessionId')
->innerJoin('scu.user', 'u')
->innerJoin('scu.course', 'c')
->leftJoin(CLpView::class, 'lpv', 'WITH', 'lpv.user = u.id AND lpv.course = scu.course AND lpv.lp IN (:lpIds)')
->leftJoin(CLp::class, 'lp', 'WITH', 'lp.iid IN (:lpIds)')
->innerJoin('lp.resourceNode', 'rn')
->where('scu.course = :courseId')
->andWhere('rn.parent = c.resourceNode')
->andWhere('(lpv.progress < 100 OR lpv.progress IS NULL)')
->setParameter('courseId', $courseId)
->setParameter('lpIds', $lpIds);
return $qb->getQuery()->getResult();
}
}

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\TrackEDefault;
use DateTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TrackEDefaultRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TrackEDefault::class);
}
/**
* Retrieves the registration date of a user in a specific course or session.
*/
public function getUserCourseRegistrationAt(int $courseId, int $userId, ?int $sessionId = 0): ?\DateTime
{
$serializedPattern = sprintf('s:2:"id";i:%d;', $userId);
$qb = $this->createQueryBuilder('te')
->select('te.defaultDate')
->where('te.cId = :courseId')
->andWhere('te.defaultValueType = :valueType')
->andWhere('te.defaultEventType = :eventType')
->andWhere('te.defaultValue LIKE :serializedPattern')
->setParameter('courseId', $courseId)
->setParameter('valueType', 'user_object')
->setParameter('eventType', 'user_subscribed')
->setParameter('serializedPattern', '%' . $serializedPattern . '%');
if ($sessionId > 0) {
$qb->andWhere('te.sessionId = :sessionId')
->setParameter('sessionId', $sessionId);
} elseif ($sessionId === 0) {
$qb->andWhere('te.sessionId = 0');
} else {
$qb->andWhere('te.sessionId IS NULL');
}
$qb->setMaxResults(1);
$query = $qb->getQuery();
try {
$result = $query->getOneOrNullResult();
if ($result && isset($result['defaultDate'])) {
return $result['defaultDate'] instanceof \DateTime
? $result['defaultDate']
: new \DateTime($result['defaultDate']);
}
} catch (\Exception $e) {
throw new \RuntimeException('Error fetching registration date: ' . $e->getMessage());
}
return null;
}
}

@ -0,0 +1,16 @@
<p>
{{HelloX}}
</p>
<p>
{{YouAreRegCourseXFromDateX}}<br />
{{ThisMessageIsAboutX}}
</p>
<p>
{{StepsToRemindX}}
</p>
<p>
{{LpRemindFooterX}}
</p>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save