Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions public/main/inc/lib/SkillModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,9 @@ public function addSkillToUser(
'acquired_skill_at' => api_get_utc_datetime(),
'course_id' => (int) $courseId,
'session_id' => $sessionId ? (int) $sessionId : null,
'validation_status' => 0,
'argumentation' => '',
'argumentation_author_id' => 0,
];
$skill_rel_user->save($params);
}
Expand Down Expand Up @@ -1352,37 +1355,36 @@ public function getCoursesBySkill($skillId)
*
* @return bool Whether the user has the skill return true. Otherwise return false
*/
public function userHasSkill($userId, $skillId, $courseId = 0, $sessionId = 0)
public function userHasSkill($userId, $skillId, $courseId = 0, $sessionId = 0): bool
{
$userId = (int) $userId;
$skillId = (int) $skillId;
$courseId = (int) $courseId;
$sessionId = (int) $sessionId;

$whereConditions = [
'user_id = ? ' => (int) $userId,
'AND skill_id = ? ' => (int) $skillId,
];
// Base query: user + skill
$sql = "SELECT COUNT(1) AS qty
FROM {$this->table_skill_rel_user}
WHERE user_id = $userId
AND skill_id = $skillId";

// If course is provided, filter by course and session
if ($courseId > 0) {
$whereConditions['AND course_id = ? '] = $courseId;
$whereConditions['AND session_id = ? '] = $sessionId ? $sessionId : null;
}
$sql .= " AND course_id = $courseId";

$result = Database::select(
'COUNT(1) AS qty',
$this->table_skill_rel_user,
[
'where' => $whereConditions,
],
'first'
);

if (false != $result) {
if ($result['qty'] > 0) {
return true;
if ($sessionId > 0) {
// Skill linked to a specific session
$sql .= " AND session_id = $sessionId";
} else {
// Course-level skill, no session (NULL)
$sql .= " AND session_id IS NULL";
}
}

return false;
$result = Database::query($sql);
$row = Database::fetch_assoc($result);

return !empty($row) && (int) $row['qty'] > 0;
}

/**
Expand Down
34 changes: 19 additions & 15 deletions public/main/inc/lib/SkillRelUserModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class SkillRelUserModel extends Model
'acquired_skill_at',
'course_id',
'session_id',
'acquired_level',
'validation_status',
'argumentation',
'argumentation_author_id',
];

public function __construct()
Expand Down Expand Up @@ -58,27 +62,27 @@ public function getUserSkills($userId, $courseId = 0, $sessionId = 0)
return [];
}

$userId = (int) $userId;
$courseId = (int) $courseId;
$sessionId = $sessionId ? (int) $sessionId : null;
$whereConditions = [
'user_id = ? ' => (int) $userId,
];
$sessionId = (int) $sessionId;

$sql = "SELECT skill_id FROM {$this->table} WHERE user_id = $userId";

if ($courseId > 0) {
$whereConditions['AND course_id = ? '] = $courseId;
$whereConditions['AND session_id = ?'] = $sessionId;
$sql .= " AND course_id = $courseId";

if ($sessionId > 0) {
// Skill linked to a specific session
$sql .= " AND session_id = $sessionId";
} else {
// Course-level skill, no session → match NULL
$sql .= " AND session_id IS NULL";
}
}

$result = Database::select(
'skill_id',
$this->table,
[
'where' => $whereConditions,
],
'all'
);
$result = Database::query($sql);

return $result;
return Database::store_result($result, 'ASSOC');
}

/**
Expand Down
52 changes: 49 additions & 3 deletions public/main/lp/learnpath.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8079,6 +8079,11 @@ public function getFinalItemForm()
$finalItem = $this->getFinalItem();
$title = '';

$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
$resourceId = (int) $this->lp_id;
$tableLpItem = Database::get_course_table(TABLE_LP_ITEM);

if ($finalItem) {
$title = $finalItem->get_title();
$buttonText = get_lang('Save');
Expand Down Expand Up @@ -8111,6 +8116,14 @@ public function getFinalItemForm()
)
);

// Advanced settings: only gradebook category/evaluation selector
if (api_is_allowed_to_edit(null, true)) {
$form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
$form->addElement('html', '<div id="advanced_params_options" style="display:none">');
GradebookUtils::load_gradebook_select_in_tool($form);
$form->addElement('html', '</div>');
}

$renderer = $form->defaultRenderer();
$renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');

Expand All @@ -8124,9 +8137,30 @@ public function getFinalItemForm()
$form->addHidden('action', 'add_final_item');
$form->addHidden('path', Session::read('pathItem'));
$form->addHidden('previous', $this->get_last());
$form->setDefaults(
['title' => $title, 'content_lp_certificate' => $content]
);

// Default values
$defaults = [
'title' => $title,
'content_lp_certificate' => $content,
];

// Preselect the gradebook category from c_lp_item.ref (only for final_item)
if (api_is_allowed_to_edit(null, true)) {
$sql = "SELECT ref
FROM $tableLpItem
WHERE lp_id = ".(int) $resourceId."
AND item_type = '".Database::escape_string(TOOL_LP_FINAL_ITEM)."'
LIMIT 1";
$result = Database::query($sql);
if (Database::num_rows($result) > 0) {
$row = Database::fetch_array($result);
if (!empty($row['ref'])) {
$defaults['category_id'] = (int) $row['ref'];
}
}
}

$form->setDefaults($defaults);

if ($form->validate()) {
$values = $form->exportValues();
Expand Down Expand Up @@ -8160,6 +8194,18 @@ public function getFinalItemForm()
} else {
$this->edit_document();
}

// Store the gradebook category id in c_lp_item.ref ONLY for final_item
if (api_is_allowed_to_edit(null, true)) {
$categoryId = isset($values['category_id']) ? (int) $values['category_id'] : 0;
$refValue = $categoryId > 0 ? (string) $categoryId : '';

Database::update(
$tableLpItem,
['ref' => $refValue],
['lp_id = ? AND item_type = ?' => [$resourceId, TOOL_LP_FINAL_ITEM]]
);
}
}

return $form->returnForm();
Expand Down
109 changes: 94 additions & 15 deletions public/main/lp/lp_final_item.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
$lpItemRepo = Container::getLpItemRepository();
$isFinalThere = false;
$isFinalDone = false;
$finalItem = null;

try {
$finalItem = $lpItemRepo->findOneBy(['lp' => $lpEntity, 'itemType' => TOOL_LP_FINAL_ITEM]);
if ($finalItem) {
Expand Down Expand Up @@ -109,19 +111,80 @@
$courseEntity = api_get_course_entity();
$sessionEntity = api_get_session_entity();

/* @var GradebookCategory $gbCat */
$gbCat = $gbRepo->findOneBy(['course' => $courseEntity, 'session' => $sessionEntity]);
// Resolve GradebookCategory using lp_item.ref when item_type = final_item.
// We store the gradebook category id in c_lp_item.ref (string).
$categoryIdFromRef = 0;

if (!empty($finalItem) && method_exists($finalItem, 'getRef')) {
try {
$refRaw = trim((string) $finalItem->getRef());
if ($refRaw !== '' && $refRaw !== '0') {
$categoryIdFromRef = (int) $refRaw;
}
} catch (\Throwable $e) {
error_log('[LP_FINAL] Unable to read lp_item.ref for final_item: '.$e->getMessage());
}
}

/** @var GradebookCategory|null $gbCat */
$gbCat = null;

// 1) First, try the explicit category id stored in c_lp_item.ref.
if ($categoryIdFromRef > 0) {
$gbCat = $gbRepo->find($categoryIdFromRef);

// Safety check: ensure the referenced category belongs to the same course/session context.
if ($gbCat && $courseEntity) {
$catCourse = $gbCat->getCourse();
$catSession = $gbCat->getSession();

// If course does not match, discard this category and let the fallback logic handle it.
if (!$catCourse || $catCourse->getId() !== $courseEntity->getId()) {
$gbCat = null;
} elseif ($sessionEntity) {
// If we are in a session context, ensure the category session matches.
if ($catSession && $catSession->getId() !== $sessionEntity->getId()) {
$gbCat = null;
}
}
}
}

// 2) Fallback: keep legacy behaviour (root course/session category).
if (!$gbCat && $courseEntity) {
if ($sessionEntity) {
$gbCat = $gbRepo->findOneBy([
'course' => $courseEntity,
'session' => $sessionEntity,
]);
}

if (!$gbCat) {
$gbCat = $gbRepo->findOneBy(['course' => $courseEntity, 'session' => null]);
if (!$gbCat) {
$gbCat = $gbRepo->findOneBy([
'course' => $courseEntity,
'session' => null,
]);
}
}

if ($gbCat && !api_is_allowed_to_edit() && !api_is_excluded_user_type()) {
$cert = safeGenerateCertificateForCategory($gbCat, $userId);
$downloadBlock = buildCertificateBlock($cert);
// Use legacy Category business object to generate certificate + skills
// for this specific gradebook category.
// NOTE: Category::generateUserCertificate() is expected to know how to
// work with the Doctrine GradebookCategory entity.
$certificate = Category::generateUserCertificate($gbCat, $userId);
if (!empty($certificate)) {
// Build the HTML panel to replace ((certificate)).
$downloadBlock = Category::getDownloadCertificateBlock($certificate);
}

// Skills: Category::generateUserCertificate() already assigns skills
// to the user for this course/session/category when enabled.
// Here we just render the user's skills panel.
$badgeBlock = generateBadgePanel($userId, $courseId, $sessionId);
}

// Replace ((certificate)) and ((skill)) tokens in the final-item document.
$finalHtml = renderFinalItemDocument($id, $downloadBlock, $badgeBlock);
}

Expand All @@ -140,7 +203,7 @@ function safeGenerateCertificateForCategory(GradebookCategory $category, int $us
$sessId = $session ? $session->getId() : 0;
$catId = (int) $category->getId();

// Build certificate content & score
// Build certificate content & score.
$gb = GradebookUtils::get_user_certificate_content($userId, $courseId, $sessId);
$html = (is_array($gb) && isset($gb['content'])) ? $gb['content'] : '';
$score = isset($gb['score']) ? (float) $gb['score'] : 100.0;
Expand All @@ -149,23 +212,23 @@ function safeGenerateCertificateForCategory(GradebookCategory $category, int $us

$htmlUrl = '';
$pdfUrl = '';
$cert = null;

try {
// Store/refresh as Resource (controlled access; not shown in "My personal files")
// Store/refresh as Resource (controlled access; not shown in "My personal files").
$cert = $certRepo->upsertCertificateResource($catId, $userId, $score, $html);

// (Optional) keep metadata (created_at/score). Filename is not required anymore.
$certRepo->registerUserInfoAboutCertificate($catId, $userId, $score);

// Build URLs from the Resource layer
// View URL (first resource file assigned to the node – here the HTML we just uploaded)
// Build URLs from the Resource layer.
$htmlUrl = $certRepo->getResourceFileUrl($cert);
} catch (\Throwable $e) {
error_log('[LP_FINAL] register cert error: '.$e->getMessage());
}

return [
'path_certificate' => (string) ($cert->getPathCertificate() ?? ''),
'path_certificate' => $cert ? (string) ($cert->getPathCertificate() ?? '') : '',
'html_url' => $htmlUrl,
'pdf_url' => $pdfUrl,
];
Expand Down Expand Up @@ -218,6 +281,7 @@ function generateBadgePanel(int $userId, int $courseId, int $sessionId = 0): str
if (!$skill) {
continue;
}

$items .= "
<div class='row'>
<div class='col-md-2 col-xs-4'>
Expand Down Expand Up @@ -264,14 +328,25 @@ function renderFinalItemDocument(int $lpItemOrDocId, string $certificateBlock, s
$lpItemRepo = Container::getLpItemRepository();

$document = null;
try { $document = $docRepo->find($lpItemOrDocId); } catch (\Throwable $e) {}

// First, try to use the id directly as a document iid.
try {
$document = $docRepo->find($lpItemOrDocId);
} catch (\Throwable $e) {
// Silence here, we will try the LP item fallback below.
}

// If not a document iid, try resolving from the LP item path.
if (!$document) {
try {
$lpItem = $lpItemRepo->find($lpItemOrDocId);
if ($lpItem) {
// In our case, lp_item.path stores the document iid as string.
$document = $docRepo->find((int) $lpItem->getPath());
}
} catch (\Throwable $e) {}
} catch (\Throwable $e) {
// As a last resort, fail quietly and return empty content.
}
}

if (!$document) {
Expand All @@ -288,8 +363,12 @@ function renderFinalItemDocument(int $lpItemOrDocId, string $certificateBlock, s
$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); }
if ($hasCert) {
$content = str_replace('((certificate))', $certificateBlock, $content);
}
if ($hasSkill) {
$content = str_replace('((skill))', $badgeBlock, $content);
}

return $content;
}
Loading