diff --git a/public/main/gradebook/lib/be/category.class.php b/public/main/gradebook/lib/be/category.class.php index eef0f996ae5..eb39341db84 100644 --- a/public/main/gradebook/lib/be/category.class.php +++ b/public/main/gradebook/lib/be/category.class.php @@ -2012,7 +2012,6 @@ public static function generateUserCertificate( bool $sendNotification = false, bool $skipGenerationIfExists = false ) { - $user_id = (int) $user_id; $categoryId = $category->getId(); $sessionId = $category->getSession() ? $category->getSession()->getId() : 0; $courseId = $category->getCourse()->getId(); diff --git a/public/main/lp/learnpath.class.php b/public/main/lp/learnpath.class.php index 5a1c9e64ab3..6530b7479f9 100644 --- a/public/main/lp/learnpath.class.php +++ b/public/main/lp/learnpath.class.php @@ -4356,211 +4356,197 @@ public function showBuildSideBar($updateAudio = false, $dropElementHere = false, $ajax_url = api_get_path(WEB_AJAX_PATH).'lp.ajax.php?lp_id='.$this->get_id().'&'.api_get_cidreq(); $content = ' - '; + }); + }); + '; $content .= " - "; + "; $content .= $this->return_new_tree($updateAudio, $dropElementHere); $documentId = isset($_GET['path_item']) ? (int) $_GET['path_item'] : 0; @@ -4657,11 +4643,18 @@ public function getBuildTree($noWrapper = false, $dropElement = false): string return ''; }, 'childOpen' => function($child) { - $id = $child['iid']; + $id = $child['iid']; + $type = $child['itemType'] ?? ($child['item_type'] ?? ''); + $isFinal = (TOOL_LP_FINAL_ITEM === $type); + $extraClass = $isFinal ? ' final-item disable_drag' : ''; + $extraAttr = $isFinal ? ' data-fixed="final"' : ''; + return '
  • '; + data-type="'.$type.'" + '.$extraAttr.' + class="flex flex-col list-group-item nested-'.$child['lvl'].$extraClass.'">'; }, 'childClose' => '', 'nodeDecorator' => function ($node) use ($mainUrl, $previewImage, $upIcon, $downIcon) { @@ -5031,7 +5024,8 @@ public function create_document( $title = '', $extension = 'html', $parentId = 0, - $creatorId = 0 + $creatorId = 0, + $docFiletype = 'file' ) { $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId; $sessionId = api_get_session_id(); @@ -5063,26 +5057,6 @@ public function create_document( api_get_path(REL_PATH).'courses/', $content ); - - // Change the path of mp3 to absolute. - // The first regexp deals with :// urls. - /*$content = preg_replace( - "|(flashvars=\"file=)([^:/]+)/|", - "$1".api_get_path( - REL_COURSE_PATH - ).$courseInfo['path'].'/document/', - $content - );*/ - // The second regexp deals with audio/ urls. - /*$content = preg_replace( - "|(flashvars=\"file=)([^/]+)/|", - "$1".api_get_path( - REL_COURSE_PATH - ).$courseInfo['path'].'/document/$2/', - $content - );*/ - // For flv player: To prevent edition problem with firefox, - // we have to use a strange tip (don't blame me please). $content = str_replace( '', '', @@ -5093,7 +5067,7 @@ public function create_document( $document = DocumentManager::addDocument( $courseInfo, null, - 'file', + $docFiletype, '', $tmp_filename, '', @@ -7830,14 +7804,22 @@ public function getFinalItemForm() $documentId = $this->create_document( $this->course_info, $values['content_lp_certificate'], - $values['title'] + $values['title'], + 'html', + 0, + 0, + 'certificate' ); + + $lpItemRepo = Container::getLpItemRepository(); + $root = $lpItemRepo->getRootItem($this->get_id()); + $this->add_item( - null, + $root, $lastItemId, - 'final_item', + TOOL_LP_FINAL_ITEM, $documentId, - $values['title'], + $values['title'] ); Display::addFlash( diff --git a/public/main/lp/lp_final_item.php b/public/main/lp/lp_final_item.php index 38bba3def15..f730396afa2 100644 --- a/public/main/lp/lp_final_item.php +++ b/public/main/lp/lp_final_item.php @@ -2,39 +2,35 @@ /* For licensing terms, see /license.txt */ +use Chamilo\CoreBundle\Entity\GradebookCategory; use Chamilo\CoreBundle\Entity\Skill; use Chamilo\CoreBundle\Framework\Container; /** - * Print a learning path finish page with details. - * - * @author Jose Loguercio + * LP Final Item: muestra certificado y skills al completar el LP. */ $_in_course = true; require_once __DIR__.'/../inc/global.inc.php'; $current_course_tool = TOOL_GRADEBOOK; - -// Make sure no anonymous user gets here without permission api_protect_course_script(true); -// Get environment variables $courseCode = api_get_course_id(); -$courseId = api_get_course_int_id(); -$userId = api_get_user_id(); -$sessionId = api_get_session_id(); -$id = isset($_GET['id']) ? intval($_GET['id']) : 0; -$lpId = isset($_GET['lp_id']) ? intval($_GET['lp_id']) : 0; +$courseId = api_get_course_int_id(); +$userId = api_get_user_id(); +$sessionId = api_get_session_id(); +$id = isset($_GET['id']) ? (int) $_GET['id'] : 0; +$lpId = isset($_GET['lp_id']) ? (int) $_GET['lp_id'] : 0; // This page can only be shown from inside a learning path if (!$id && !$lpId) { - Display::return_message(get_lang('The file was not found'), 'warning'); + echo Display::return_message(get_lang('The file was not found')); exit; } // Certificate and Skills Premium with Service check -$plugin = BuyCoursesPlugin::create(); +$plugin = BuyCoursesPlugin::create(); $checker = $plugin->isEnabled() && $plugin->get('include_services'); if ($checker) { @@ -63,216 +59,230 @@ } } -// Initialize variables required for the template -$downloadCertificateLink = ''; -$viewCertificateLink = ''; -$badgeLink = ''; -$finalItemTemplate = ''; - -// Check prerequisites and total completion of the learning path $lpEntity = api_get_lp_entity($lpId); -$lp = new Learnpath($lpEntity, [], $userId); -$count = $lp->getTotalItemsCountWithoutDirs(); -$completed = $lp->get_complete_items_count(true); -$currentItemId = $lp->get_current_item_id(); -$currentItem = $lp->items[$currentItemId]; -$currentItemStatus = $currentItem->get_status(); +$lp = new Learnpath($lpEntity, [], $userId); + +$count = $lp->getTotalItemsCountWithoutDirs(); +$completed = $lp->get_complete_items_count(true); +$currentItemId = $lp->get_current_item_id(); +$currentItem = $lp->items[$currentItemId] ?? null; +$currentItemStatus = $currentItem ? $currentItem->get_status() : 'not attempted'; + +$lpItemRepo = Container::getLpItemRepository(); +$isFinalThere = false; +$isFinalDone = false; +try { + $finalItem = $lpItemRepo->findOneBy(['lp' => $lpEntity, 'itemType' => TOOL_LP_FINAL_ITEM]); + if ($finalItem) { + $isFinalThere = true; + $fid = $finalItem->getIid(); + if (isset($lp->items[$fid])) { + $st = $lp->items[$fid]->get_status(); + $isFinalDone = in_array($st, ['completed','passed','succeeded'], true); + } + } +} catch (\Throwable $e) { + error_log('[LP_FINAL] final_item lookup error: '.$e->getMessage()); +} +$countAdj = max(0, $count - ($isFinalThere ? 1 : 0)); +$completedAdj = max(0, $completed - ($isFinalDone ? 1 : 0)); +$diff = $countAdj - $completedAdj; $accessGranted = false; - -if ((0 == $count - $completed) || - (1 == $count - $completed && ('incomplete' == $currentItemStatus) || ('not attempted' == $currentItemStatus)) -) { +if ($diff === 0 || ($diff === 1 && (('incomplete' === $currentItemStatus) || ('not attempted' === $currentItemStatus)))) { if ($lp->prerequisites_match($currentItemId)) { $accessGranted = true; } } -// Update the progress in DB from the items completed $lp->save_last(); +unset($lp, $currentItem); -// unset the (heavy) lp object to free memory - we don't need it anymore -unset($lp); -unset($currentItem); - -// If for some reason we consider the requirements haven't been completed yet, -// show a prerequisites warning -if (false == $accessGranted) { +if (!$accessGranted) { echo Display::return_message( - get_lang( - 'This learning object cannot display because the course prerequisites are not completed. This happens when a course imposes that you follow it step by step or get a minimum score in tests before you reach the next steps.' - ), + get_lang('This learning object cannot display because the course prerequisites are not completed. This happens when a course imposes that you follow it step by step or get a minimum score in tests before you reach the next steps.'), 'warning' ); - $finalItemTemplate = ''; + $finalHtml = ''; } else { - $catLoad = Category::load( - null, - null, - $courseId, - null, - null, - $sessionId, - 'ORDER BY id' - ); - // If not gradebook has been defined - if (empty($catLoad)) { - $finalItemTemplate = generateLPFinalItemTemplate( - $id, - $courseCode, - $sessionId, - $downloadCertificateLink, - $badgeLink - ); - } else { - // A gradebook was found, proceed... - /** @var Category $category */ - $category = $catLoad[0]; - $categoryId = $category->get_id(); - $link = LinkFactory::load( - null, - null, - $lpId, - null, - $courseId, - $categoryId - ); + $downloadBlock = ''; + $badgeBlock = ''; + $gbRepo = Container::getGradeBookCategoryRepository(); + $courseEntity = api_get_course_entity(); + $sessionEntity = api_get_session_entity(); - if ($link) { - $cat = new Category(); - $show_message = Category::show_message_resource_delete($courseId); - $repo = Container::getGradeBookCategoryRepository(); - $category = $repo->find($categoryId); - - if (empty($show_message) && !api_is_allowed_to_edit() && !api_is_excluded_user_type()) { - $certificate = Category::generateUserCertificate($category, $userId); - - if (!empty($certificate['pdf_url']) || - !empty($certificate['badge_link']) - ) { - if (is_array($certificate)) { - $downloadCertificateLink = Category::getDownloadCertificateBlock($certificate); - } - - if (is_array($certificate) && isset($certificate['badge_link'])) { - $courseId = api_get_course_int_id(); - $badgeLink = generateLPFinalItemTemplateBadgeLinks( - $userId, - $courseId, - $sessionId - ); - } - } - - $currentScore = Category::getCurrentScore($userId, $category, true); - Category::registerCurrentScore($currentScore, $userId, $categoryId); - } - } + /* @var GradebookCategory $gbCat */ + $gbCat = $gbRepo->findOneBy(['course' => $courseEntity, 'session' => $sessionEntity]); - $finalItemTemplate = generateLPFinalItemTemplate( - $id, - $courseCode, - $sessionId, - $downloadCertificateLink, - $badgeLink - ); + if (!$gbCat) { + $gbCat = $gbRepo->findOneBy(['course' => $courseEntity, 'session' => null]); + } - if (!$finalItemTemplate) { - echo Display::return_message(get_lang('The file was not found'), 'warning'); - } + if ($gbCat && !api_is_allowed_to_edit() && !api_is_excluded_user_type()) { + $cert = safeGenerateCertificateForCategory($gbCat, $userId); + $downloadBlock = buildCertificateBlock($cert); + $badgeBlock = generateBadgePanel($userId, $courseId, $sessionId); } + + $finalHtml = renderFinalItemDocument($id, $downloadBlock, $badgeBlock); } -// Instance a new template : No page tittle, No header, No footer $tpl = new Template(null, false, false); -$tpl->assign('content', $finalItemTemplate); +$tpl->assign('content', $finalHtml); $tpl->display_blank_template(); -// A few functions used only here... +/** + * Generates/ensures the certificate via Doctrine repositories and returns minimal link data. + */ +function safeGenerateCertificateForCategory(GradebookCategory $category, int $userId): array +{ + $course = $category->getCourse(); + $session = $category->getSession(); + $courseId = $course ? $course->getId() : 0; + $sessId = $session ? $session->getId() : 0; + $catId = (int) $category->getId(); + + $gb = GradebookUtils::get_user_certificate_content($userId, $courseId, $sessId); + $html = is_array($gb) && isset($gb['content']) ? $gb['content'] : ''; + + $fileName = hash('sha256', $userId.$catId).'.html'; + $certRepo = Container::getGradeBookCertificateRepository(); + $pf = $certRepo->generateCertificatePersonalFile($userId, $fileName, $html); + try { + $score = isset($gb['score']) ? (float) $gb['score'] : 100.0; + $certRepo->registerUserInfoAboutCertificate($catId, $userId, $score, $fileName); + } catch (\Throwable $e) { + error_log('[LP_FINAL] register cert error: '.$e->getMessage()); + } + + $hash = pathinfo($fileName, PATHINFO_FILENAME); + $htmlUrl = api_get_path(WEB_PATH).'certificates/'.$hash.'.html'; + $pdfUrl = api_get_path(WEB_PATH).'certificates/'.$hash.'.pdf'; + + return [ + 'path_certificate' => $fileName, + 'html_url' => $htmlUrl, + 'pdf_url' => $pdfUrl, + ]; +} /** - * Return a HTML string to show as final document in learning path. - * - * @param int $lpItemId - * @param string $courseCode - * @param int $sessionId - * @param string $downloadCertificateLink - * @param string $badgeLink - * - * @return mixed|string + * Builds the certificate download/view HTML block (if available). */ -function generateLPFinalItemTemplate( - $lpItemId, - $courseCode, - $sessionId = 0, - $downloadCertificateLink = '', - $badgeLink = '' -) { - $document = Container::getDocumentRepository()->find($lpItemId); - $finalItemTemplate = Container::getDocumentRepository()->getResourceFileContent($document); - - $finalItemTemplate = str_replace('((certificate))', $downloadCertificateLink, $finalItemTemplate); - $finalItemTemplate = str_replace('((skill))', $badgeLink, $finalItemTemplate); - - return $finalItemTemplate; +function buildCertificateBlock(array $cert): string +{ + $htmlUrl = $cert['html_url'] ?? ''; + $pdfUrl = $cert['pdf_url'] ?? ''; + if (!$htmlUrl && !$pdfUrl) { + return ''; + } + + $downloadBtn = $pdfUrl + ? Display::toolbarButton(get_lang('Download certificate in PDF'), $pdfUrl, 'file-pdf-box') + : ''; + + $viewBtn = $htmlUrl + ? Display::url(get_lang('View certificate'), $htmlUrl, ['class' => 'btn btn-default']) + : ''; + + return " +
    +
    +

    ".get_lang('You can now download your certificate by clicking here')."

    +
    {$downloadBtn} {$viewBtn}
    +
    +
    + "; } /** - * Return HTML string with badges list. - * - * @param int $userId - * @param int $courseId - * @param int $sessionId - * - * @return string HTML string for badges + * Returns the user's skills panel HTML (empty if none). */ -function generateLPFinalItemTemplateBadgeLinks($userId, $courseId, $sessionId = 0) +function generateBadgePanel(int $userId, int $courseId, int $sessionId = 0): string { - $em = Database::getManager(); + $em = Database::getManager(); $skillRelUser = new SkillRelUserModel(); - $userSkills = $skillRelUser->getUserSkills($userId, $courseId, $sessionId); - $skillList = ''; - $badgeLink = ''; - - if ($userSkills) { - foreach ($userSkills as $userSkill) { - $skill = $em->find(Skill::class, $userSkill['skill_id']); - if (!$skill) { - continue; - } - $skillList .= " -
    -
    -
    - -
    -
    -
    -
    ".$skill->getTitle()."
    - ".$skill->getDescription()." -
    -
    -
    ".get_lang('Share with your friends')."
    - - - - getTitle().'"', api_get_setting('siteName')).' - '.api_get_path(WEB_PATH).'badge/'.$skill->getId().'/user/'.$userId."' target='_new'> - - + $userSkills = $skillRelUser->getUserSkills($userId, $courseId, $sessionId); + if (!$userSkills) { + return ''; + } + + $items = ''; + foreach ($userSkills as $row) { + $skill = $em->find(Skill::class, $row['skill_id']); + if (!$skill) { + continue; + } + $items .= " +
    +
    +
    +
    - "; - } +
    +
    ".$skill->getTitle()."
    + ".$skill->getDescription()." +
    + +
    "; + } - if (!empty($skillList)) { - $badgeLink .= " -
    -
    -

    ".get_lang('Additionally, you have achieved the following skills')."

    - $skillList -
    -
    - "; - } + if (!$items) { + return ''; } - return $badgeLink; + return " +
    +
    +

    ".get_lang('Additionally, you have achieved the following skills')."

    + {$items} +
    +
    + "; +} + +/** + * Render the Learning Path final-item document. + */ +function renderFinalItemDocument(int $lpItemOrDocId, string $certificateBlock, string $badgeBlock): string +{ + $docRepo = Container::getDocumentRepository(); + $lpItemRepo = Container::getLpItemRepository(); + + $document = null; + try { $document = $docRepo->find($lpItemOrDocId); } catch (\Throwable $e) {} + if (!$document) { + try { + $lpItem = $lpItemRepo->find($lpItemOrDocId); + if ($lpItem) { + $document = $docRepo->find((int) $lpItem->getPath()); + } + } catch (\Throwable $e) {} + } + + if (!$document) { + return ''; + } + + try { + $content = $docRepo->getResourceFileContent($document); + } catch (\Throwable $e) { + error_log('[LP_FINAL] read doc error: '.$e->getMessage()); + return ''; + } + + $hasCert = str_contains($content, '((certificate))'); + $hasSkill = str_contains($content, '((skill))'); + + if ($hasCert) { $content = str_replace('((certificate))', $certificateBlock, $content); } + if ($hasSkill) { $content = str_replace('((skill))', $badgeBlock, $content); } + + return $content; }