Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradebook: Add min_score validation and highlight unmet scores in gradebook - refs #6049 #6058

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
62 changes: 48 additions & 14 deletions public/main/gradebook/lib/be/category.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\GradebookCategory;
use Chamilo\CoreBundle\Framework\Container;
use ChamiloSession as Session;
use Chamilo\CoreBundle\Component\Utils\ActionIcon;

Expand Down Expand Up @@ -2002,26 +2003,21 @@ public function lockAllItems($locked)

/**
* Generates a certificate for this user if everything matches.
*
* @param int $user_id
* @param bool $sendNotification
* @param bool $skipGenerationIfExists
*
* @return array
*/
public static function generateUserCertificate(
GradebookCategory $category,
$user_id,
$sendNotification = false,
$skipGenerationIfExists = false
int $user_id,
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();
$userFinishedCourse = self::userFinishedCourse($user_id, $category, true);
if (!$userFinishedCourse) {
return false;

// check if all min_score requirements are met
if (!self::userMeetsMinimumScores($user_id, $category)) {
return false; // Do not generate certificate if the user does not meet all min_score criteria
}

$skillToolEnabled = SkillModel::hasAccessToUserSkill(api_get_user_id(), $user_id);
Expand All @@ -2034,7 +2030,7 @@ public static function generateUserCertificate(
$userHasSkills = !empty($userSkills);
}

// Block certification links depending on gradebook configuration (generate certifications)
// If certificate generation is disabled, return only badge link (if available)
if (empty($category->getGenerateCertificates())) {
if ($userHasSkills) {
return [
Expand All @@ -2050,6 +2046,7 @@ public static function generateUserCertificate(
}
$my_certificate = GradebookUtils::get_certificate_by_user_id($categoryId, $user_id);

// If certificate already exists and we should skip regeneration, return false
if ($skipGenerationIfExists && !empty($my_certificate)) {
return false;
}
Expand Down Expand Up @@ -2089,7 +2086,7 @@ public static function generateUserCertificate(

$fileWasGenerated = $certificate_obj->isHtmlFileGenerated();

// Fix when using custom certificate BT#15937
// Fix when using a custom certificate plugin
if ('true' === api_get_plugin_setting('customcertificate', 'enable_plugin_customcertificate')) {
$infoCertificate = CustomCertificatePlugin::getCertificateData($my_certificate['id'], $user_id);
if (!empty($infoCertificate)) {
Expand Down Expand Up @@ -2135,6 +2132,43 @@ public static function generateUserCertificate(

return $html;
}

return false;
}

/**
* Checks whether the user has met the minimum score (`min_score`) in all required evaluations.
*/
public static function userMeetsMinimumScores(int $userId, GradebookCategory $category): bool
{
$evaluations = $category->getEvaluations();

foreach ($evaluations as $evaluation) {
$minScore = $evaluation->getMinScore();
if ($minScore !== null) {
$userScore = self::getUserScoreForEvaluation($userId, $evaluation->getId());
if ($userScore === null || $userScore < $minScore) {
return false; // If at least one evaluation is below `min_score`, return false
}
}
}

return true;
}

/**
* Retrieves the score of a user for a specific evaluation using the GradebookResult repository.
*/
public static function getUserScoreForEvaluation(int $userId, int $evaluationId): ?float
{
$gradebookResultRepo = Container::getGradebookResultRepository();

$gradebookResult = $gradebookResultRepo->findOneBy([
'user' => $userId,
'evaluation' => $evaluationId,
]);

return $gradebookResult ? $gradebookResult->getScore() : null;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions public/main/gradebook/lib/gradebook_data_generator.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,13 @@ public function build_result_column(
$scoreDisplay = ScoreDisplay::instance();
$score = $item->calc_score($userId);
$model = ExerciseLib::getCourseScoreModel();

// Get min_score from entity (only if available)
$minScore = null;
if (isset($item->entity) && method_exists($item->entity, 'getMinScore')) {
$minScore = $item->entity->getMinScore();
}

if (!empty($score)) {
switch ($item->get_item_type()) {
// category
Expand Down Expand Up @@ -799,6 +806,11 @@ public function build_result_column(
);
}

// If minScore exists and user score is lower, mark in red
if (!is_null($minScore) && $score[0] < $minScore) {
$display = "<span class='text-danger font-bold'>$display</span>";
}

return [
'display' => $display,
'score' => $score,
Expand Down
15 changes: 15 additions & 0 deletions src/CoreBundle/Entity/GradebookEvaluation.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class GradebookEvaluation
#[ORM\Column(name: 'user_score_list', type: 'array', nullable: true)]
protected ?array $userScoreList = null;

#[ORM\Column(name: 'min_score', type: 'float', precision: 6, scale: 2, nullable: true)]
protected ?float $minScore = null;

public function __construct()
{
$this->locked = 0;
Expand Down Expand Up @@ -308,4 +311,16 @@ public function setCategory(GradebookCategory $category): self

return $this;
}

public function getMinScore(): ?float
{
return $this->minScore;
}

public function setMinScore(?float $minScore): self
{
$this->minScore = $minScore;

return $this;
}
}
15 changes: 15 additions & 0 deletions src/CoreBundle/Entity/GradebookLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class GradebookLink
#[ORM\Column(name: 'user_score_list', type: 'array', nullable: true)]
protected ?array $userScoreList = null;

#[ORM\Column(name: 'min_score', type: 'float', precision: 6, scale: 2, nullable: true)]
protected ?float $minScore = null;

public function __construct()
{
$this->locked = 0;
Expand Down Expand Up @@ -260,4 +263,16 @@ public function setCategory(GradebookCategory $category): self

return $this;
}

public function getMinScore(): ?float
{
return $this->minScore;
}

public function setMinScore(?float $minScore): self
{
$this->minScore = $minScore;

return $this;
}
}
6 changes: 6 additions & 0 deletions src/CoreBundle/Framework/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Chamilo\CoreBundle\Repository\ExtraFieldRepository;
use Chamilo\CoreBundle\Repository\GradeBookCategoryRepository;
use Chamilo\CoreBundle\Repository\GradebookCertificateRepository;
use Chamilo\CoreBundle\Repository\GradebookResultRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\MessageRepository;
Expand Down Expand Up @@ -363,6 +364,11 @@ public static function getGradeBookCertificateRepository(): GradebookCertificate
return self::$container->get(GradebookCertificateRepository::class);
}

public static function getGradebookResultRepository(): GradebookResultRepository
{
return self::$container->get(GradebookResultRepository::class);
}

public static function getGroupRepository(): CGroupRepository
{
return self::$container->get(CGroupRepository::class);
Expand Down
37 changes: 37 additions & 0 deletions src/CoreBundle/Migrations/Schema/V200/Version20250120103800.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/* For licensing terms, see /license.txt */

namespace Chamilo\CoreBundle\Migrations\Schema\V200;

use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;

final class Version20250120103800 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Add min_score column to gradebook_evaluation and gradebook_link tables';
}

public function up(Schema $schema): void
{
$this->addSql('
ALTER TABLE gradebook_evaluation
ADD COLUMN min_score FLOAT DEFAULT NULL
');

$this->addSql('
ALTER TABLE gradebook_link
ADD COLUMN min_score FLOAT DEFAULT NULL
');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE gradebook_evaluation DROP COLUMN min_score');
$this->addSql('ALTER TABLE gradebook_link DROP COLUMN min_score');
}
}
19 changes: 19 additions & 0 deletions src/CoreBundle/Repository/GradebookResultRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

/* For licensing terms, see /license.txt */

namespace Chamilo\CoreBundle\Repository;

use Chamilo\CoreBundle\Entity\GradebookResult;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class GradebookResultRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, GradebookResult::class);
}
}
Loading