Skip to content

Commit 07c385f

Browse files
Merge pull request #7517 from christianbeeznest/fixes-updates256
User: Add course student info cards with per-tool new-content notifications & categories
2 parents bf535c8 + 2134056 commit 07c385f

8 files changed

Lines changed: 4200 additions & 212 deletions

File tree

assets/vue/components/course/CourseCard.vue

Lines changed: 1003 additions & 115 deletions
Large diffs are not rendered by default.

assets/vue/views/user/courses/List.vue

Lines changed: 282 additions & 94 deletions
Large diffs are not rendered by default.

src/CoreBundle/Controller/CourseController.php

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
2020
use Chamilo\CoreBundle\Helpers\CidReqHelper;
2121
use Chamilo\CoreBundle\Helpers\CourseHelper;
22+
use Chamilo\CoreBundle\Helpers\CourseStudentInfoHelper;
2223
use Chamilo\CoreBundle\Helpers\UserHelper;
2324
use Chamilo\CoreBundle\Repository\AssetRepository;
2425
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
@@ -1167,6 +1168,194 @@ public function getAutoLaunchLPId(
11671168
return new JsonResponse(['lpId' => $autoLaunchLPId], Response::HTTP_OK);
11681169
}
11691170

1171+
#[IsGranted('ROLE_USER')]
1172+
#[Route('/{cid}/student-info.json', name: 'chamilo_core_course_student_info_json', methods: ['GET'])]
1173+
public function studentInfoJson(
1174+
Request $request,
1175+
#[MapEntity(expr: 'repository.find(cid)')]
1176+
Course $course,
1177+
CourseStudentInfoHelper $studentInfoHelper
1178+
): JsonResponse {
1179+
$user = $this->userHelper->getCurrent();
1180+
1181+
if (!$user || !method_exists($user, 'getId')) {
1182+
return new JsonResponse(['error' => 'Authentication required.'], Response::HTTP_UNAUTHORIZED);
1183+
}
1184+
1185+
$userId = (int) $user->getId();
1186+
$sessionId = (int) $request->query->get('sid', 0);
1187+
1188+
if (0 === $sessionId) {
1189+
$this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
1190+
}
1191+
1192+
// Extra safety for invitees (same logic as home.json).
1193+
if (method_exists($user, 'isInvitee') && $user->isInvitee()) {
1194+
$isSubscribed = CourseManager::is_user_subscribed_in_course(
1195+
$userId,
1196+
$course->getCode(),
1197+
$sessionId > 0,
1198+
$sessionId
1199+
);
1200+
1201+
if (!$isSubscribed) {
1202+
throw $this->createAccessDeniedException('User is not subscribed to the course.');
1203+
}
1204+
}
1205+
1206+
$payload = $studentInfoHelper->getStudentInfoForCourse($userId, $course, $sessionId);
1207+
1208+
return new JsonResponse($payload, Response::HTTP_OK);
1209+
}
1210+
1211+
#[IsGranted('ROLE_USER')]
1212+
#[Route('/student-info-batch.json', name: 'chamilo_core_course_student_info_batch_json', methods: ['POST'])]
1213+
public function studentInfoBatchJson(
1214+
Request $request,
1215+
CourseStudentInfoHelper $studentInfoHelper
1216+
): JsonResponse {
1217+
$user = $this->userHelper->getCurrent();
1218+
1219+
if (!$user || !method_exists($user, 'getId')) {
1220+
return new JsonResponse(['error' => 'Authentication required.'], Response::HTTP_UNAUTHORIZED);
1221+
}
1222+
1223+
$payload = json_decode($request->getContent() ?: '', true) ?? [];
1224+
$sessionId = (int) ($payload['sid'] ?? 0);
1225+
1226+
$courseIds = $payload['courseIds'] ?? [];
1227+
if (!is_array($courseIds)) {
1228+
$courseIds = [];
1229+
}
1230+
1231+
$courseIds = array_values(array_unique(array_map('intval', $courseIds)));
1232+
$courseIds = array_filter($courseIds, static fn (int $id) => $id > 0);
1233+
if (count($courseIds) > 300) {
1234+
$courseIds = array_slice($courseIds, 0, 300);
1235+
}
1236+
1237+
if (empty($courseIds)) {
1238+
return new JsonResponse([
1239+
'sid' => $sessionId,
1240+
'items' => [],
1241+
], Response::HTTP_OK);
1242+
}
1243+
1244+
// Load courses in one go.
1245+
/** @var Course[] $courses */
1246+
$courses = $this->em->getRepository(Course::class)->findBy(['id' => $courseIds]);
1247+
1248+
$allowedCourseIds = [];
1249+
$courseMap = [];
1250+
foreach ($courses as $course) {
1251+
$cid = (int) $course->getId();
1252+
$courseMap[$cid] = $course;
1253+
}
1254+
1255+
$userId = (int) $user->getId();
1256+
1257+
foreach ($courseIds as $cid) {
1258+
$course = $courseMap[$cid] ?? null;
1259+
if (!$course instanceof Course) {
1260+
continue;
1261+
}
1262+
1263+
if (0 === $sessionId) {
1264+
try {
1265+
$this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
1266+
} catch (\Throwable $e) {
1267+
// Skip unauthorized courses without failing the whole batch.
1268+
continue;
1269+
}
1270+
}
1271+
1272+
// Extra safety for invitees (same logic as home.json / studentInfoJson).
1273+
if (method_exists($user, 'isInvitee') && $user->isInvitee()) {
1274+
$isSubscribed = CourseManager::is_user_subscribed_in_course(
1275+
$userId,
1276+
$course->getCode(),
1277+
$sessionId > 0,
1278+
$sessionId
1279+
);
1280+
1281+
if (!$isSubscribed) {
1282+
continue;
1283+
}
1284+
}
1285+
1286+
$allowedCourseIds[] = $cid;
1287+
}
1288+
1289+
if (empty($allowedCourseIds)) {
1290+
return new JsonResponse([
1291+
'sid' => $sessionId,
1292+
'items' => [],
1293+
], Response::HTTP_OK);
1294+
}
1295+
1296+
// Compute in batch
1297+
$items = $studentInfoHelper->getStudentInfoBatchForCourses($userId, $allowedCourseIds, $sessionId);
1298+
1299+
return new JsonResponse([
1300+
'sid' => $sessionId,
1301+
'items' => $items, // courseId => studentInfo
1302+
], Response::HTTP_OK);
1303+
}
1304+
1305+
#[IsGranted('ROLE_USER')]
1306+
#[Route('/{cid}/new-content-tools.json', name: 'chamilo_core_course_new_content_tools_json', methods: ['GET'])]
1307+
public function newContentToolsJson(
1308+
Request $request,
1309+
#[MapEntity(expr: 'repository.find(cid)')]
1310+
Course $course,
1311+
CourseStudentInfoHelper $studentInfoHelper
1312+
): JsonResponse {
1313+
$user = $this->userHelper->getCurrent();
1314+
1315+
if (!$user || !method_exists($user, 'getId')) {
1316+
return new JsonResponse(['error' => 'Authentication required.'], Response::HTTP_UNAUTHORIZED);
1317+
}
1318+
1319+
$userId = (int) $user->getId();
1320+
$sessionId = (int) $request->query->get('sid', 0);
1321+
$limit = (int) $request->query->get('limit', 20);
1322+
if ($limit <= 0) {
1323+
$limit = 20;
1324+
}
1325+
if ($limit > 50) {
1326+
$limit = 50;
1327+
}
1328+
1329+
if (0 === $sessionId) {
1330+
$this->denyAccessUnlessGranted(CourseVoter::VIEW, $course);
1331+
}
1332+
1333+
if (method_exists($user, 'isInvitee') && $user->isInvitee()) {
1334+
$isSubscribed = CourseManager::is_user_subscribed_in_course(
1335+
$userId,
1336+
$course->getCode(),
1337+
$sessionId > 0,
1338+
$sessionId
1339+
);
1340+
1341+
if (!$isSubscribed) {
1342+
throw $this->createAccessDeniedException('User is not subscribed to the course.');
1343+
}
1344+
}
1345+
1346+
$items = $studentInfoHelper->getNewContentToolsForCourse($userId, $course, $sessionId, $limit);
1347+
1348+
return new JsonResponse([
1349+
'cid' => (int) $course->getId(),
1350+
'sid' => $sessionId,
1351+
'items' => $items,
1352+
'meta' => [
1353+
// Optional debug/help info for UI:
1354+
'lastAccess' => $studentInfoHelper->getLastAccessLabelForCourse($userId, (int) $course->getId(), $sessionId),
1355+
],
1356+
], Response::HTTP_OK);
1357+
}
1358+
11701359
private function autoLaunch(): void
11711360
{
11721361
$autoLaunchWarning = '';

src/CoreBundle/Controller/PlatformConfigurationController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ public function list(
9797
$configuration['settings']['platform.session_admin_access_to_all_users_on_all_urls'] = $settingsManager->getSetting('platform.session_admin_access_to_all_users_on_all_urls', true);
9898
$configuration['settings']['profile.login_is_email'] = $settingsManager->getSetting('profile.login_is_email', true);
9999
$configuration['settings']['platform.timepicker_increment'] = $settingsManager->getSetting('platform.timepicker_increment', true);
100+
$rawCourseStudentInfoSetting = $settingsManager->getSetting('course.course_student_info', true);
101+
$configuration['settings']['course.course_student_info'] = 'false' !== $rawCourseStudentInfoSetting ? $this->decodeSettingArray($rawCourseStudentInfoSetting) : 'false';
100102

101103
$variables = [];
102104

src/CoreBundle/Entity/Course.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
267267
'course:read',
268268
'session:read',
269269
'course_catalogue:read',
270+
'course_rel_user:read',
270271
])]
271272
#[Assert\NotBlank]
272273
#[ORM\Column(name: 'course_language', type: 'string', length: 20, unique: false, nullable: false)]
@@ -1250,6 +1251,38 @@ public function setPopularity(int $popularity): self
12501251
return $this;
12511252
}
12521253

1254+
#[SerializedName('categoryTitles')]
1255+
#[Groups([
1256+
'course:read',
1257+
'course_rel_user:read',
1258+
'course_catalogue:read',
1259+
])]
1260+
public function getCategoryTitles(): array
1261+
{
1262+
$titles = [];
1263+
1264+
foreach ($this->categories as $category) {
1265+
if (null === $category) {
1266+
continue;
1267+
}
1268+
1269+
$title = \method_exists($category, 'getTitle')
1270+
? (string) $category->getTitle()
1271+
: (string) $category;
1272+
1273+
$title = \trim(\strip_tags($title));
1274+
1275+
if ('' !== $title) {
1276+
$titles[] = $title;
1277+
}
1278+
}
1279+
1280+
$titles = \array_values(\array_unique($titles));
1281+
\sort($titles, \SORT_NATURAL | \SORT_FLAG_CASE);
1282+
1283+
return $titles;
1284+
}
1285+
12531286
public function getResourceIdentifier(): int
12541287
{
12551288
return $this->getId();

0 commit comments

Comments
 (0)