From 4ea03be3320c61fb5740e4ea5f4c2830e5988648 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Wed, 29 Jan 2025 16:35:24 -0500 Subject: [PATCH] Gradebook: Add min_score validation and highlight unmet scores in gradebook - refs #6049 --- .../main/gradebook/lib/be/category.class.php | 62 ++++++++++++++----- .../lib/gradebook_data_generator.class.php | 12 ++++ src/CoreBundle/Entity/GradebookEvaluation.php | 15 +++++ src/CoreBundle/Entity/GradebookLink.php | 15 +++++ src/CoreBundle/Framework/Container.php | 6 ++ .../Schema/V200/Version20250120103800.php | 37 +++++++++++ .../Repository/GradebookResultRepository.php | 19 ++++++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 src/CoreBundle/Migrations/Schema/V200/Version20250120103800.php create mode 100644 src/CoreBundle/Repository/GradebookResultRepository.php diff --git a/public/main/gradebook/lib/be/category.class.php b/public/main/gradebook/lib/be/category.class.php index 347e1e17618..f68d0d95675 100644 --- a/public/main/gradebook/lib/be/category.class.php +++ b/public/main/gradebook/lib/be/category.class.php @@ -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; @@ -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); @@ -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 [ @@ -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; } @@ -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)) { @@ -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; } /** diff --git a/public/main/gradebook/lib/gradebook_data_generator.class.php b/public/main/gradebook/lib/gradebook_data_generator.class.php index 27455457f0e..8fb4e07877c 100644 --- a/public/main/gradebook/lib/gradebook_data_generator.class.php +++ b/public/main/gradebook/lib/gradebook_data_generator.class.php @@ -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 @@ -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 = "$display"; + } + return [ 'display' => $display, 'score' => $score, diff --git a/src/CoreBundle/Entity/GradebookEvaluation.php b/src/CoreBundle/Entity/GradebookEvaluation.php index 70901cc13ff..ed527218cb9 100644 --- a/src/CoreBundle/Entity/GradebookEvaluation.php +++ b/src/CoreBundle/Entity/GradebookEvaluation.php @@ -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; @@ -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; + } } diff --git a/src/CoreBundle/Entity/GradebookLink.php b/src/CoreBundle/Entity/GradebookLink.php index 4097ae393ec..85269831829 100644 --- a/src/CoreBundle/Entity/GradebookLink.php +++ b/src/CoreBundle/Entity/GradebookLink.php @@ -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; @@ -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; + } } diff --git a/src/CoreBundle/Framework/Container.php b/src/CoreBundle/Framework/Container.php index a15e42fe2e0..a05df4ee36c 100644 --- a/src/CoreBundle/Framework/Container.php +++ b/src/CoreBundle/Framework/Container.php @@ -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; @@ -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); diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20250120103800.php b/src/CoreBundle/Migrations/Schema/V200/Version20250120103800.php new file mode 100644 index 00000000000..32e3a161eb1 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20250120103800.php @@ -0,0 +1,37 @@ +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'); + } +} diff --git a/src/CoreBundle/Repository/GradebookResultRepository.php b/src/CoreBundle/Repository/GradebookResultRepository.php new file mode 100644 index 00000000000..5b2a3fcace0 --- /dev/null +++ b/src/CoreBundle/Repository/GradebookResultRepository.php @@ -0,0 +1,19 @@ +