Merge remote-tracking branch 'origin/master'

pull/5140/head^2
Angel Fernando Quiroz Campos 2 years ago
commit 8928373318
  1. 2
      assets/vue/components/links/LinkCategoryForm.vue
  2. 10
      assets/vue/components/links/LinkForm.vue
  3. 22
      assets/vue/components/links/LinkItem.vue
  4. 17
      assets/vue/services/linkService.js
  5. 24
      assets/vue/views/links/LinksList.vue
  6. 43
      public/main/inc/global.inc.php
  7. 49
      src/CoreBundle/Controller/Api/CLinkDetailsController.php
  8. 66
      src/CoreBundle/Controller/Api/CheckCLinkAction.php
  9. 37
      src/CoreBundle/Controller/Api/CreateCLinkAction.php
  10. 2
      src/CoreBundle/Controller/Api/CreateCLinkCategoryAction.php
  11. 5
      src/CoreBundle/Controller/Api/GetLinksCollectionController.php
  12. 44
      src/CoreBundle/Controller/Api/UpdateCLinkAction.php
  13. 2
      src/CoreBundle/Controller/Api/UpdateCLinkCategoryAction.php
  14. 29
      src/CoreBundle/Controller/ResourceController.php
  15. 72
      src/CoreBundle/Migrations/Schema/V200/Version20240202122300.php
  16. 18
      src/CourseBundle/Entity/CLink.php
  17. 6
      src/CourseBundle/Entity/CLinkCategory.php
  18. 2
      src/CourseBundle/Repository/CShortcutRepository.php
  19. 6
      tests/scripts/disable_user_conditions.php

@ -86,7 +86,7 @@ const fetchCategory = async () => {
if (props.categoryId) { if (props.categoryId) {
try { try {
let category = await linkService.getCategory(props.categoryId) let category = await linkService.getCategory(props.categoryId)
formData.title = category.categoryTitle formData.title = category.title
formData.description = category.description formData.description = category.description
} catch (error) { } catch (error) {
console.error('Error fetching category:', error) console.error('Error fetching category:', error)

@ -25,7 +25,7 @@
v-model="formData.category" v-model="formData.category"
:options="categories" :options="categories"
:label="t('Select a category')" :label="t('Select a category')"
option-label="categoryTitle" option-label="title"
option-value="iid" option-value="iid"
hast-empty-value hast-empty-value
/> />
@ -129,7 +129,7 @@ onMounted(() => {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
categories.value = await linkService.getCategories() categories.value = await linkService.getCategories(parentResourceNodeId.value)
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error) console.error('Error fetching categories:', error)
} }
@ -142,10 +142,10 @@ const fetchLink = async () => {
formData.url = response.url formData.url = response.url
formData.title = response.title formData.title = response.title
formData.description = response.description formData.description = response.description
formData.showOnHomepage = response.showOnHomepage formData.showOnHomepage = response.onHomepage
formData.target = response.target formData.target = response.target
formData.parentResourceNodeId = response.value formData.parentResourceNodeId = response.parentResourceNodeId
formData.resourceLinkList = response.value formData.resourceLinkList = response.resourceLinkList
if (response.category) { if (response.category) {
formData.category = parseInt(response.category["@id"].split("/").pop()) formData.category = parseInt(response.category["@id"].split("/").pop())
} }

@ -9,6 +9,20 @@
/> />
{{ link.title }} {{ link.title }}
</a> </a>
<BaseIcon
v-if="isLinkValid.isValid"
icon="check"
size="small"
class="text-green-500"
:title="t('Link is valid')"
/>
<BaseIcon
v-else-if="isLinkValid.isValid === false"
icon="alert"
size="small"
class="text-red-500"
:title="t('Link is not valid')"
/>
</h6> </h6>
</div> </div>
<div class="flex gap-2" v-if="securityStore.isAuthenticated && isCurrentTeacher"> <div class="flex gap-2" v-if="securityStore.isAuthenticated && isCurrentTeacher">
@ -65,7 +79,7 @@ import BaseIcon from "../basecomponents/BaseIcon.vue"
import { isVisible, VISIBLE } from "./linkVisibility" import { isVisible, VISIBLE } from "./linkVisibility"
import { useSecurityStore } from "../../store/securityStore" import { useSecurityStore } from "../../store/securityStore"
import { useStore } from "vuex" import { useStore } from "vuex"
import { computed } from "vue" import { computed, watch } from "vue"
const store = useStore() const store = useStore()
const securityStore = useSecurityStore() const securityStore = useSecurityStore()
@ -78,7 +92,11 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
}) isLinkValid: {
type: Object,
default: () => ({})
},
});
const emit = defineEmits(["check", "edit", "toggle", "moveUp", "moveDown", "delete"]) const emit = defineEmits(["check", "edit", "toggle", "moveUp", "moveDown", "delete"])
</script> </script>

@ -15,7 +15,7 @@ export default {
* @param {Number|String} linkId * @param {Number|String} linkId
*/ */
getLink: async (linkId) => { getLink: async (linkId) => {
const response = await axios.get(ENTRYPOINT + 'links/' + linkId) const response = await axios.get(ENTRYPOINT + 'links/' + linkId + '/details/')
return response.data return response.data
}, },
@ -70,8 +70,8 @@ export default {
return response.data return response.data
}, },
getCategories: async () => { getCategories: async (parentId) => {
const response = await axios.get(ENTRYPOINT + 'link_categories') const response = await axios.get(`${ENTRYPOINT}link_categories?resourceNode.parent=${parentId}`)
return response.data['hydra:member'] return response.data['hydra:member']
}, },
@ -120,4 +120,15 @@ export default {
const response = await axios.put(endpoint, {visible}) const response = await axios.put(endpoint, {visible})
return response.data return response.data
}, },
/**
* Checks if the URL is valid.
* @param {String} url The URL to be checked.
* @param linkId
*/
checkLink: async (url, linkId) => {
const endpoint = `${ENTRYPOINT}links/${linkId}/check`;
const response = await axios.get(endpoint, { params: { url } });
return response.data;
},
} }

@ -13,13 +13,13 @@
type="black" type="black"
@click="redirectToCreateLinkCategory" @click="redirectToCreateLinkCategory"
/> />
<BaseButton <!--BaseButton
:label="t('Export to PDF')" :label="t('Export to PDF')"
icon="file-pdf" icon="file-pdf"
type="black" type="black"
@click="exportToPDF" @click="exportToPDF"
/> />
<StudentViewButton /> <StudentViewButton /-->
</BaseToolbar> </BaseToolbar>
<LinkCategoryCard v-if="isLoading"> <LinkCategoryCard v-if="isLoading">
@ -69,6 +69,7 @@
> >
<LinkItem <LinkItem
:link="link" :link="link"
:isLinkValid="linkValidationResults[link.iid]"
@check="checkLink(link.iid, link.url)" @check="checkLink(link.iid, link.url)"
@delete="confirmDeleteLink(link)" @delete="confirmDeleteLink(link)"
@edit="editLink" @edit="editLink"
@ -128,6 +129,7 @@
> >
<LinkItem <LinkItem
:link="link" :link="link"
:isLinkValid="linkValidationResults[link.iid]"
@check="checkLink(link.iid, link.url)" @check="checkLink(link.iid, link.url)"
@delete="confirmDeleteLink(link)" @delete="confirmDeleteLink(link)"
@edit="editLink" @edit="editLink"
@ -214,6 +216,8 @@ const categoryToDelete = ref(null)
const isLoading = ref(true) const isLoading = ref(true)
const linkValidationResults = ref({});
onMounted(() => { onMounted(() => {
linksWithoutCategory.value = [] linksWithoutCategory.value = []
categories.value = {} categories.value = {}
@ -237,8 +241,8 @@ function confirmDeleteLink(link) {
async function deleteLink() { async function deleteLink() {
try { try {
await linkService.deleteLink(linkToDelete.value.id) await linkService.deleteLink(linkToDelete.value.id)
isDeleteLinkDialogVisible.value = true
linkToDelete.value = null linkToDelete.value = null
isDeleteLinkDialogVisible.value = false
notifications.showSuccessNotification(t("Link deleted")) notifications.showSuccessNotification(t("Link deleted"))
await fetchLinks() await fetchLinks()
} catch (error) { } catch (error) {
@ -247,8 +251,14 @@ async function deleteLink() {
} }
} }
function checkLink(id, url) { async function checkLink(id, url) {
// Implement the logic to check the link using the provided id and url try {
const result = await linkService.checkLink(url, id);
linkValidationResults.value = { ...linkValidationResults.value, [id]: { isValid: result.isValid } };
} catch (error) {
console.error("Error checking link:", error);
linkValidationResults.value = { ...linkValidationResults.value, [id]: { isValid: false, message: error.message || 'Link validation failed' } };
}
} }
async function toggleVisibility(link) { async function toggleVisibility(link) {
@ -256,16 +266,12 @@ async function toggleVisibility(link) {
const visibility = toggleVisibilityProperty(!link.linkVisible) const visibility = toggleVisibilityProperty(!link.linkVisible)
let newLink = await linkService.toggleLinkVisibility(link.iid, isVisible(visibility)) let newLink = await linkService.toggleLinkVisibility(link.iid, isVisible(visibility))
notifications.showSuccessNotification(t("Link visibility updated")) notifications.showSuccessNotification(t("Link visibility updated"))
linksWithoutCategory.value
.filter((l) => l.iid === link.iid)
.forEach((l) => (l.linkVisible = visibilityFromBoolean(newLink.linkVisible)))
Object.values(categories.value) Object.values(categories.value)
.map((c) => c.links) .map((c) => c.links)
.flat() .flat()
.filter((l) => l.iid === link.iid) .filter((l) => l.iid === link.iid)
.forEach((l) => (l.linkVisible = visibilityFromBoolean(newLink.linkVisible))) .forEach((l) => (l.linkVisible = visibilityFromBoolean(newLink.linkVisible)))
} catch (error) { } catch (error) {
console.error("Error deleting link:", error)
notifications.showErrorNotification(t("Could not change visibility of link")) notifications.showErrorNotification(t("Could not change visibility of link"))
} }
} }

@ -58,24 +58,37 @@ if (!empty($flashBag->keys())) {
$response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false); $response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false);
$context = Container::getRouter()->getContext(); $context = Container::getRouter()->getContext();
$pos = strpos($currentBaseUrl, 'main'); $isCli = 'cli' === php_sapi_name();
$posPlugin = strpos($currentBaseUrl, 'plugin'); $baseUrl = null;
$posCertificate = strpos($currentBaseUrl, 'certificate'); if ($isCli) {
$cliOptions = getopt('', ['url:']);
if (false === $pos && false === $posPlugin && false === $posCertificate) { if (!empty($cliOptions['url'])) {
echo 'Cannot load current URL'; $baseUrl = $cliOptions['url'];
exit; }
} }
if (false !== $pos) { if ($isCli && $baseUrl) {
$newBaseUrl = substr($currentBaseUrl, 0, $pos - 1); $context->setBaseUrl($baseUrl);
}elseif (false !== $posPlugin) { } else {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1); $pos = strpos($currentBaseUrl, 'main');
} elseif (false !== $posCertificate) { $posPlugin = strpos($currentBaseUrl, 'plugin');
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1); $posCertificate = strpos($currentBaseUrl, 'certificate');
}
$context->setBaseUrl($newBaseUrl); if (false === $pos && false === $posPlugin && false === $posCertificate) {
echo 'Cannot load current URL';
exit;
}
if (false !== $pos) {
$newBaseUrl = substr($currentBaseUrl, 0, $pos - 1);
}elseif (false !== $posPlugin) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1);
} elseif (false !== $posCertificate) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1);
}
$context->setBaseUrl($newBaseUrl);
}
try { try {
// Load legacy configuration.php // Load legacy configuration.php

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CourseBundle\Entity\CLink;
use Chamilo\CourseBundle\Repository\CShortcutRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class CLinkDetailsController extends AbstractController
{
public function __invoke(CLink $link, CShortcutRepository $shortcutRepository): Response
{
$shortcut = $shortcutRepository->getShortcutFromResource($link);
$isOnHomepage = null !== $shortcut;
$parentResourceNodeId = null;
if ($link->getResourceNode() && $link->getResourceNode()->getParent()) {
$parentResourceNodeId = $link->getResourceNode()->getParent()->getId();
}
$resourceLinkList = [];
if ($link->getResourceLinkEntityList()) {
foreach ($link->getResourceLinkEntityList() as $resourceLink) {
$resourceLinkList[] = [
'visibility' => $resourceLink->getVisibility(),
'cid' => $resourceLink->getCourse()->getId(),
'sid' => $resourceLink->getSession()->getId()
];
}
}
$details = [
'url' => $link->getUrl(),
'title' => $link->getTitle(),
'description' => $link->getDescription(),
'onHomepage' => $isOnHomepage,
'target' => $link->getTarget(),
'parentResourceNodeId' => $parentResourceNodeId,
'resourceLinkList' => $resourceLinkList,
'category' => $link->getCategory()?->getIid(),
];
return $this->json($details, Response::HTTP_OK);
}
}

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CourseBundle\Entity\CLink;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class CheckCLinkAction extends AbstractController
{
public function __invoke(CLink $link, Request $request, SettingsManager $settingsManager): JsonResponse
{
$url = $request->query->get('url');
$result = $this->checkUrl($url, $settingsManager);
return new JsonResponse(['isValid' => $result]);
}
private function checkUrl(string $url, SettingsManager $settingsManager): bool
{
// Check if curl is available.
if (!\in_array('curl', get_loaded_extensions())) {
return false;
}
// set URL and other appropriate options
$defaults = [
CURLOPT_URL => $url,
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_HEADER => 0,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 4,
];
// Check for proxy settings in your application configuration
$proxySettings = $settingsManager->getSetting('platform.proxy_settings', true);
if ($proxySettings && isset($proxySettings['curl_setopt_array'])) {
$defaults[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
$defaults[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
}
// Create a new cURL resource
$ch = curl_init();
curl_setopt_array($ch, $defaults);
// grab URL and pass it to the browser
ob_start();
$result = curl_exec($ch);
ob_get_clean();
// close cURL resource, and free up system resources
curl_close($ch);
// Check for any errors
if ($result === false || curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) {
return false;
}
return true;
}
}

@ -6,22 +6,27 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api; namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CourseBundle\Entity\CLink; use Chamilo\CourseBundle\Entity\CLink;
use Chamilo\CourseBundle\Entity\CLinkCategory; use Chamilo\CourseBundle\Entity\CLinkCategory;
use Chamilo\CourseBundle\Repository\CLinkRepository; use Chamilo\CourseBundle\Repository\CLinkRepository;
use Chamilo\CourseBundle\Repository\CShortcutRepository;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
class CreateCLinkAction extends BaseResourceFileAction class CreateCLinkAction extends BaseResourceFileAction
{ {
public function __invoke(Request $request, CLinkRepository $repo, EntityManager $em): CLink public function __invoke(Request $request, CLinkRepository $repo, EntityManager $em, CShortcutRepository $shortcutRepository, Security $security): CLink
{ {
$data = json_decode($request->getContent(), true); $data = json_decode($request->getContent(), true);
$url = $data['url']; $url = $data['url'];
$title = $data['title']; $title = $data['title'];
$description = $data['description']; $description = $data['description'];
$categoryId = (int) $data['category']; $categoryId = (int) $data['category'];
$onHomepage = isset($data['showOnHomepage']) ? (int) $data['showOnHomepage'] : 0; $onHomepage = isset($data['showOnHomepage']) && (bool) $data['showOnHomepage'];
$target = $data['target']; $target = $data['target'];
$parentResourceNodeId = $data['parentResourceNodeId']; $parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true); $resourceLinkList = json_decode($data['resourceLinkList'], true);
@ -52,6 +57,34 @@ class CreateCLinkAction extends BaseResourceFileAction
$link->setResourceLinkArray($resourceLinkList); $link->setResourceLinkArray($resourceLinkList);
} }
$em->persist($link);
$em->flush();
$this->handleShortcutCreation($resourceLinkList, $em, $security, $link, $shortcutRepository, $onHomepage);
return $link; return $link;
} }
private function handleShortcutCreation(
array $resourceLinkList,
EntityManager $em,
Security $security,
CLink $link,
CShortcutRepository $shortcutRepository,
bool $onHomepage
): void {
$firstLink = reset($resourceLinkList);
if (isset($firstLink['sid']) && isset($firstLink['cid'])) {
$sid = $firstLink['sid'];
$cid = $firstLink['cid'];
$course = $cid ? $em->getRepository(Course::class)->find($cid) : null;
$session = $sid ? $em->getRepository(Session::class)->find($sid) : null;
/** @var User $currentUser */
$currentUser = $security->getUser();
if ($onHomepage) {
$shortcutRepository->addShortCut($link, $currentUser, $course, $session);
}
}
}
} }

@ -22,7 +22,7 @@ class CreateCLinkCategoryAction extends BaseResourceFileAction
$resourceLinkList = json_decode($data['resourceLinkList'], true); $resourceLinkList = json_decode($data['resourceLinkList'], true);
$linkCategory = (new CLinkCategory()) $linkCategory = (new CLinkCategory())
->setCategoryTitle($title) ->setTitle($title)
->setDescription($description) ->setDescription($description)
; ;

@ -47,6 +47,7 @@ class GetLinksCollectionController extends BaseResourceFileAction
[ [
'id' => $link->getIid(), 'id' => $link->getIid(),
'title' => $link->getTitle(), 'title' => $link->getTitle(),
'description' => $link->getDescription(),
'url' => $link->getUrl(), 'url' => $link->getUrl(),
'iid' => $link->getIid(), 'iid' => $link->getIid(),
'linkVisible' => $link->getFirstResourceLink()->getVisibility(), 'linkVisible' => $link->getFirstResourceLink()->getVisibility(),
@ -67,7 +68,8 @@ class GetLinksCollectionController extends BaseResourceFileAction
$categoryInfo = [ $categoryInfo = [
'id' => $categoryId, 'id' => $categoryId,
'name' => $category->getCategoryTitle(), 'title' => $category->getTitle(),
'descritption' => $category->getDescription(),
'visible' => $category->getFirstResourceLink()->getVisibility(), 'visible' => $category->getFirstResourceLink()->getVisibility(),
]; ];
$dataResponse['categories'][$categoryId]['info'] = $categoryInfo; $dataResponse['categories'][$categoryId]['info'] = $categoryInfo;
@ -79,6 +81,7 @@ class GetLinksCollectionController extends BaseResourceFileAction
$items[] = [ $items[] = [
'id' => $link->getIid(), 'id' => $link->getIid(),
'title' => $link->getTitle(), 'title' => $link->getTitle(),
'description' => $link->getDescription(),
'url' => $link->getUrl(), 'url' => $link->getUrl(),
'iid' => $link->getIid(), 'iid' => $link->getIid(),
'linkVisible' => $link->getFirstResourceLink()->getVisibility(), 'linkVisible' => $link->getFirstResourceLink()->getVisibility(),

@ -6,24 +6,28 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api; namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CourseBundle\Entity\CLink; use Chamilo\CourseBundle\Entity\CLink;
use Chamilo\CourseBundle\Entity\CLinkCategory; use Chamilo\CourseBundle\Entity\CLinkCategory;
use Chamilo\CourseBundle\Repository\CLinkRepository; use Chamilo\CourseBundle\Repository\CLinkRepository;
use Chamilo\CourseBundle\Repository\CShortcutRepository;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
class UpdateCLinkAction extends BaseResourceFileAction class UpdateCLinkAction extends BaseResourceFileAction
{ {
public function __invoke(CLink $link, Request $request, CLinkRepository $repo, EntityManager $em): CLink public function __invoke(CLink $link, Request $request, CLinkRepository $repo, EntityManager $em, CShortcutRepository $shortcutRepository, Security $security): CLink
{ {
$data = json_decode($request->getContent(), true); $data = json_decode($request->getContent(), true);
$url = $data['url']; $url = $data['url'];
$title = $data['title']; $title = $data['title'];
$description = $data['description']; $description = $data['description'];
$categoryId = (int) $data['category']; $categoryId = (int) $data['category'];
$onHomepage = isset($data['showOnHomepage']) ? (int) $data['showOnHomepage'] : 0; $onHomepage = isset($data['showOnHomepage']) && (bool) $data['showOnHomepage'];
$target = $data['target']; $target = $data['target'];
$parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true); $resourceLinkList = json_decode($data['resourceLinkList'], true);
$link->setUrl($url); $link->setUrl($url);
@ -38,14 +42,36 @@ class UpdateCLinkAction extends BaseResourceFileAction
} }
} }
if (!empty($parentResourceNodeId)) { $em->persist($link);
$link->setParentResourceNode($parentResourceNodeId); $em->flush();
}
if (!empty($resourceLinkList)) { $this->handleShortcutCreationOrDeletion($resourceLinkList, $em, $security, $link, $shortcutRepository, $onHomepage);
$link->setResourceLinkArray($resourceLinkList);
}
return $link; return $link;
} }
private function handleShortcutCreationOrDeletion(
array $resourceLinkList,
EntityManager $em,
Security $security,
CLink $link,
CShortcutRepository $shortcutRepository,
bool $onHomepage
): void {
$firstLink = reset($resourceLinkList);
if (isset($firstLink['sid']) && isset($firstLink['cid'])) {
$sid = $firstLink['sid'];
$cid = $firstLink['cid'];
$course = $cid ? $em->getRepository(Course::class)->find($cid) : null;
$session = $sid ? $em->getRepository(Session::class)->find($sid) : null;
/** @var User $currentUser */
$currentUser = $security->getUser();
if ($onHomepage) {
$shorcut = $shortcutRepository->addShortCut($link, $currentUser, $course, $session);
} else {
$removed = $shortcutRepository->removeShortCut($link);
}
}
}
} }

@ -21,7 +21,7 @@ class UpdateCLinkCategoryAction extends BaseResourceFileAction
$parentResourceNodeId = $data['parentResourceNodeId']; $parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true); $resourceLinkList = json_decode($data['resourceLinkList'], true);
$linkCategory->setCategoryTitle($title); $linkCategory->setTitle($title);
$linkCategory->setDescription($description); $linkCategory->setDescription($description);
if (!empty($parentResourceNodeId)) { if (!empty($parentResourceNodeId)) {

@ -21,6 +21,7 @@ use Chamilo\CoreBundle\Traits\GradebookControllerTrait;
use Chamilo\CoreBundle\Traits\ResourceControllerTrait; use Chamilo\CoreBundle\Traits\ResourceControllerTrait;
use Chamilo\CourseBundle\Controller\CourseControllerInterface; use Chamilo\CourseBundle\Controller\CourseControllerInterface;
use Chamilo\CourseBundle\Entity\CTool; use Chamilo\CourseBundle\Entity\CTool;
use Chamilo\CourseBundle\Repository\CLinkRepository;
use Chamilo\CourseBundle\Repository\CShortcutRepository; use Chamilo\CourseBundle\Repository\CShortcutRepository;
use Chamilo\CourseBundle\Repository\CToolRepository; use Chamilo\CourseBundle\Repository\CToolRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -171,8 +172,10 @@ class ResourceController extends AbstractResourceController implements CourseCon
* @return RedirectResponse|void * @return RedirectResponse|void
*/ */
#[Route('/{tool}/{type}/{id}/link', name: 'chamilo_core_resource_link', methods: ['GET'])] #[Route('/{tool}/{type}/{id}/link', name: 'chamilo_core_resource_link', methods: ['GET'])]
public function linkAction(Request $request, RouterInterface $router) public function linkAction(Request $request, RouterInterface $router, CLinkRepository $cLinkRepository)
{ {
$tool = $request->get('tool');
$type = $request->get('type');
$id = $request->get('id'); $id = $request->get('id');
$resourceNode = $this->getResourceNodeRepository()->find($id); $resourceNode = $this->getResourceNodeRepository()->find($id);
@ -180,15 +183,25 @@ class ResourceController extends AbstractResourceController implements CourseCon
throw new FileNotFoundException('Resource not found'); throw new FileNotFoundException('Resource not found');
} }
$repo = $this->getRepositoryFromRequest($request); if ('course_tool' === $tool && 'links' === $type) {
if ($repo instanceof ResourceWithLinkInterface) { $cLink = $cLinkRepository->findOneBy(['resourceNode' => $resourceNode]);
$resource = $repo->getResourceFromResourceNode($resourceNode->getId()); if ($cLink) {
$url = $repo->getLink($resource, $router, $this->getCourseUrlQueryToArray()); $url = $cLink->getUrl();
return $this->redirect($url);
} else {
throw new FileNotFoundException('CLink not found for the given resource node');
}
} else {
$repo = $this->getRepositoryFromRequest($request);
if ($repo instanceof ResourceWithLinkInterface) {
$resource = $repo->getResourceFromResourceNode($resourceNode->getId());
$url = $repo->getLink($resource, $router, $this->getCourseUrlQueryToArray());
return $this->redirect($url); return $this->redirect($url);
} }
$this->abort('No redirect'); $this->abort('No redirect');
}
} }
/** /**

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Chamilo\CourseBundle\Entity\CLink;
use Chamilo\CourseBundle\Repository\CLinkRepository;
use Chamilo\CourseBundle\Repository\CShortcutRepository;
use Doctrine\DBAL\Schema\Schema;
class Version20240202122300 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Create shortcuts for c_link entries with on_homepage = 1';
}
public function up(Schema $schema): void
{
$container = $this->getContainer();
$doctrine = $container->get('doctrine');
$em = $doctrine->getManager();
$connection = $em->getConnection();
$admin = $this->getAdmin();
$linkRepo = $container->get(CLinkRepository::class);
$shortcutRepo = $container->get(CShortcutRepository::class);
$sql = 'SELECT * FROM c_link WHERE on_homepage = 1';
$stmt = $connection->prepare($sql);
$result = $stmt->executeQuery();
while ($row = $result->fetchAssociative()) {
$linkId = $row['iid'];
/* @var CLink $link */
$link = $linkRepo->find($linkId);
if (!$link) {
error_log("Link with ID $linkId not found");
continue;
}
$course = $link->getFirstResourceLink()->getCourse();
$session = $link->getFirstResourceLink()->getSession();
$shortcut = $shortcutRepo->getShortcutFromResource($link);
if (null === $shortcut) {
try {
$shortcutRepo->addShortCut($link, $admin, $course, $session);
error_log("Shortcut created for link ID $linkId");
} catch (\Exception $e) {
error_log("Failed to create shortcut for link ID $linkId: " . $e->getMessage());
}
} else {
error_log("Shortcut already exists for link ID $linkId");
}
}
$em->flush();
}
public function down(Schema $schema): void
{
}
}

@ -15,6 +15,8 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\Controller\Api\CheckCLinkAction;
use Chamilo\CoreBundle\Controller\Api\CLinkDetailsController;
use Chamilo\CoreBundle\Controller\Api\CreateCLinkAction; use Chamilo\CoreBundle\Controller\Api\CreateCLinkAction;
use Chamilo\CoreBundle\Controller\Api\GetLinksCollectionController; use Chamilo\CoreBundle\Controller\Api\GetLinksCollectionController;
use Chamilo\CoreBundle\Controller\Api\UpdateCLinkAction; use Chamilo\CoreBundle\Controller\Api\UpdateCLinkAction;
@ -52,6 +54,22 @@ use Symfony\Component\Validator\Constraints as Assert;
deserialize: false deserialize: false
), ),
new Get(security: "is_granted('VIEW', object.resourceNode)"), new Get(security: "is_granted('VIEW', object.resourceNode)"),
new Get(
uriTemplate: '/links/{iid}/details',
controller: CLinkDetailsController::class,
openapiContext: [
'summary' => 'Gets the details of a link, including whether it is on the homepage',
],
security: "is_granted('VIEW', object.resourceNode)"
),
new Get(
uriTemplate: '/links/{iid}/check',
controller: CheckCLinkAction::class,
openapiContext: [
'summary' => 'Check if a link URL is valid',
],
security: "is_granted('VIEW', object.resourceNode)"
),
new Delete(security: "is_granted('DELETE', object.resourceNode)"), new Delete(security: "is_granted('DELETE', object.resourceNode)"),
new Post( new Post(
controller: CreateCLinkAction::class, controller: CreateCLinkAction::class,

@ -20,6 +20,7 @@ use Chamilo\CoreBundle\Controller\Api\CreateCLinkCategoryAction;
use Chamilo\CoreBundle\Controller\Api\UpdateCLinkCategoryAction; use Chamilo\CoreBundle\Controller\Api\UpdateCLinkCategoryAction;
use Chamilo\CoreBundle\Controller\Api\UpdateVisibilityLinkCategory; use Chamilo\CoreBundle\Controller\Api\UpdateVisibilityLinkCategory;
use Chamilo\CoreBundle\Entity\AbstractResource; use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\Listener\ResourceListener;
use Chamilo\CoreBundle\Entity\ResourceInterface; use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CourseBundle\Repository\CLinkCategoryRepository; use Chamilo\CourseBundle\Repository\CLinkCategoryRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -125,10 +126,11 @@ use Symfony\Component\Validator\Constraints as Assert;
'groups' => ['link_category:write'], 'groups' => ['link_category:write'],
], ],
)] )]
#[ApiFilter(SearchFilter::class, properties: ['category_title' => 'partial', 'resourceNode.parent' => 'exact'])] #[ApiFilter(SearchFilter::class, properties: ['title' => 'partial', 'resourceNode.parent' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['iid', 'resourceNode.title', 'resourceNode.createdAt', 'resourceNode.updatedAt'])] #[ApiFilter(OrderFilter::class, properties: ['iid', 'resourceNode.title', 'resourceNode.createdAt', 'resourceNode.updatedAt'])]
#[ORM\Table(name: 'c_link_category')] #[ORM\Table(name: 'c_link_category')]
#[ORM\Entity(repositoryClass: CLinkCategoryRepository::class)] #[ORM\Entity(repositoryClass: CLinkCategoryRepository::class)]
#[ORM\EntityListeners([ResourceListener::class])]
class CLinkCategory extends AbstractResource implements ResourceInterface, Stringable class CLinkCategory extends AbstractResource implements ResourceInterface, Stringable
{ {
#[ApiProperty(identifier: true)] #[ApiProperty(identifier: true)]
@ -138,7 +140,7 @@ class CLinkCategory extends AbstractResource implements ResourceInterface, Strin
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
protected ?int $iid = null; protected ?int $iid = null;
#[Groups(['link_category:read', 'link_category:write'])] #[Groups(['link_category:read', 'link_category:write', 'link_category:browse'])]
#[Assert\NotBlank] #[Assert\NotBlank]
#[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)] #[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)]
protected string $title; protected string $title;

@ -55,7 +55,7 @@ final class CShortcutRepository extends ResourceRepository
$shortcut = $this->getShortcutFromResource($resource); $shortcut = $this->getShortcutFromResource($resource);
if (null !== $shortcut) { if (null !== $shortcut) {
$em->remove($shortcut); $em->remove($shortcut);
$em->flush(); //$em->flush();
return true; return true;
} }

@ -18,7 +18,7 @@
require_once __DIR__.'/../../public/main/inc/global.inc.php'; require_once __DIR__.'/../../public/main/inc/global.inc.php';
$senderId = api_get_setting('disable_user_conditions_sender_id'); $senderId = api_get_setting('platform.disable_user_conditions_sender_id');
if (empty($senderId)) { if (empty($senderId)) {
exit; exit;
@ -56,7 +56,7 @@ $sql = "SELECT u.id
LEFT JOIN extra_field_values ev LEFT JOIN extra_field_values ev
ON u.id = ev.item_id AND field_id = $fieldId ON u.id = ev.item_id AND field_id = $fieldId
WHERE WHERE
(ev.value IS NULL OR ev.value = '') AND (ev.field_value IS NULL OR ev.field_value = '') AND
u.active = 1 u.active = 1
$statusCondition $statusCondition
"; ";
@ -170,7 +170,7 @@ $sql = "SELECT u.id
INNER JOIN extra_field_values ev INNER JOIN extra_field_values ev
ON u.id = ev.item_id AND field_id = $fieldId ON u.id = ev.item_id AND field_id = $fieldId
WHERE WHERE
ev.value = 1 AND ev.field_value = 1 AND
u.active = 1 u.active = 1
$statusCondition $statusCondition
"; ";

Loading…
Cancel
Save