Plugins: Add positioning plugin see #3644

pull/3565/head^2
Julio Montoya 5 years ago
parent b1be4b9bb0
commit 9dff93545c
  1. 2
      main/announcements/announcements.php
  2. 60
      main/exercise/TestCategory.php
  3. 186
      main/exercise/exercise.class.php
  4. 1
      main/exercise/exercise_question_reminder.php
  5. 1
      main/exercise/exercise_reminder.php
  6. 5
      main/exercise/exercise_result.php
  7. 2
      main/exercise/exercise_show.php
  8. 10
      main/exercise/exercise_submit.php
  9. 1
      main/exercise/exercise_submit_modal.php
  10. 10
      main/exercise/overview.php
  11. 1
      main/exercise/result.php
  12. 3
      main/forum/index.php
  13. 1
      main/inc/ajax/exercise.ajax.php
  14. 50
      main/inc/lib/api.lib.php
  15. 22
      main/inc/lib/course_home.lib.php
  16. 6
      main/inc/lib/exercise.lib.php
  17. 10
      plugin/positioning/README.md
  18. 1
      plugin/positioning/index.php
  19. 7
      plugin/positioning/install.php
  20. 22
      plugin/positioning/lang/english.php
  21. 5
      plugin/positioning/lang/french.php
  22. 5
      plugin/positioning/lang/spanish.php
  23. 7
      plugin/positioning/plugin.php
  24. 232
      plugin/positioning/src/Positioning.php
  25. 44
      plugin/positioning/src/ajax.php
  26. 116
      plugin/positioning/start.php
  27. 94
      plugin/positioning/start_student.php
  28. 7
      plugin/positioning/uninstall.php
  29. 7
      plugin/positioning/view/start.tpl
  30. 11
      plugin/positioning/view/start_student.tpl

@ -14,7 +14,7 @@
$use_anonymous = true;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_ANNOUNCEMENT;
api_protect_course_script(true);
api_protect_course_group(GroupManager::GROUP_TOOL_ANNOUNCEMENT);

@ -762,7 +762,6 @@ class TestCategory
return null;
}
$categoryNameList = self::getListOfCategoriesNameForTest($exerciseId);
$table = new HTML_Table(
[
'class' => 'table table-hover table-striped table-bordered',
@ -818,64 +817,7 @@ class TestCategory
// Radar requires more than 3 categories.
if ($countCategories > 2 && RESULT_DISABLE_RADAR === (int) $exercise->results_disabled) {
$categoryNameToJson = json_encode(array_column($categoryNameList, 'title'));
$resultsToJson = json_encode($resultsArray);
$radar = "
<canvas id='categoryRadar' width='400' height='200'></canvas>
<script>
var data = {
labels: $categoryNameToJson,
datasets: [{
fill:true,
label: '".get_lang('Categories')."',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
pointBackgroundColor: 'rgb(255, 99, 132)',
pointBorderColor: '#fff',
pointHoverBackgroundColor:'#fff',
pointHoverBorderColor: 'rgb(255, 99, 132)',
pointRadius: 6,
pointBorderWidth: 3,
pointHoverRadius: 10,
data: $resultsToJson
}]
}
var options = {
scale: {
angleLines: {
display: false
},
ticks: {
beginAtZero: true,
min: 0,
max: 10,
stepSize: 1
},
pointLabels: {
fontSize: 14,
//fontStyle: 'bold'
},
},
elements: {
line: {
tension:0,
borderWidth:3
}
},
legend: {
//position: 'bottom'
display: false
}
};
var ctx = document.getElementById('categoryRadar').getContext('2d');
var myRadarChart = new Chart(ctx, {
type: 'radar',
data: data,
options: options
});
</script>
";
$radar = $exercise->getRadar(array_column($categoryNameList, 'title'), [$resultsArray]);
}
if (!empty($none_category)) {

@ -8666,7 +8666,7 @@ class Exercise
* @param int $courseId
* @param int $sessionId
* @param bool $returnData
* @param int $minCategories
* @param int $minCategoriesInExercise
* @param int $filterByResultDisabled
* @param int $filterByAttempt
*
@ -8679,11 +8679,13 @@ class Exercise
$courseId = 0,
$sessionId = 0,
$returnData = false,
$minCategories = 0,
$minCategoriesInExercise = 0,
$filterByResultDisabled = 0,
$filterByAttempt = 0
$filterByAttempt = 0,
$myActions = null,
$returnTable = false
) {
$allowDelete = Exercise::allowAction('delete');
//$allowDelete = Exercise::allowAction('delete');
$allowClean = self::allowAction('clean_results');
$TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
@ -8754,17 +8756,15 @@ class Exercise
}
$filterByResultDisabledCondition = '';
/*$filterByResultDisabled = (int) $filterByResultDisabled;
$filterByResultDisabled = (int) $filterByResultDisabled;
if (!empty($filterByResultDisabled)) {
$filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
}*/
}
$filterByAttemptCondition = '';
/*$filterByAttempt = (int) $filterByAttempt;
$filterByAttempt = (int) $filterByAttempt;
if (!empty($filterByAttempt)) {
$filterByResultDisabledCondition = ' AND e.result_disabled = '.$filterByResultDisabled;
}*/
/*$minCategories = 0,
$filterByAttempt = 0*/
$filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
}
// Only for administrators
if ($is_allowedToEdit) {
@ -8777,6 +8777,7 @@ class Exercise
$categoryCondition
$keywordCondition
$filterByResultDisabledCondition
$filterByAttemptCondition
";
$sql = "SELECT * FROM $TBL_EXERCISES e
WHERE
@ -8786,6 +8787,7 @@ class Exercise
$categoryCondition
$keywordCondition
$filterByResultDisabledCondition
$filterByAttemptCondition
ORDER BY title
LIMIT $from , $limit";
} else {
@ -9078,10 +9080,17 @@ class Exercise
$results_text = $count_exercise_not_validated == 1 ? get_lang('ResultNotRevised') : get_lang('ResultsNotRevised');
$title .= '<span class="exercise_tooltip" style="display: none;">'.$count_exercise_not_validated.' '.$results_text.' </span>';
}*/
$url = $move.'<a '.$alt_title.' class="'.$class_tip.'" id="tooltip_'.$row['id'].'" href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">
$overviewUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php';
$url = $move.
'<a
'.$alt_title.'
class="'.$class_tip.'"
id="tooltip_'.$row['id'].'"
href="'.$overviewUrl.'?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'"
>
'.Display::return_icon('quiz.png', $row['title']).'
'.$title.' </a>'.PHP_EOL;
'.$title.'
</a>'.PHP_EOL;
if (ExerciseLib::isQuizEmbeddable($row)) {
$embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
@ -9198,6 +9207,7 @@ class Exercise
}
$actions .= $clean;
// Visible / invisible
// Check if this exercise was added in a LP
if ($exercise->exercise_was_added_in_lp == true) {
@ -9342,6 +9352,13 @@ class Exercise
$delete = '';
}
if (!empty($minCategoriesInExercise)) {
$cats = TestCategory::getListOfCategoriesForTest($exercise);
if (!(count($cats) >= $minCategoriesInExercise)) {
continue;
}
}
$actions .= $delete;
// Number of questions
@ -9529,6 +9546,11 @@ class Exercise
$actions .= $additionalActions.PHP_EOL;
}
// Replace with custom actions.
if (!empty($myActions) && is_callable($myActions)) {
$actions = $myActions($row);
}
$currentRow = [
$row['id'],
$currentRow['title'],
@ -9778,6 +9800,10 @@ class Exercise
}
}
if ($returnTable) {
return $table;
}
$content .= $table->return_table();
}
@ -10908,4 +10934,136 @@ class Exercise
get_lang('ShowResultsToStudents')
);
}
public function getRadarsFromUsers($userList, $exercises, $courseId, $sessionId)
{
$dataSet = [];
$labels = [];
/** @var Exercise $exercise */
foreach ($exercises as $exercise) {
if (empty($labels)) {
$categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
$labels = array_column($categoryNameList, 'title');
}
foreach ($userList as $userId) {
$results = Event::getExerciseResultsByUser(
$userId,
$exercise->iId,
$courseId,
$sessionId
);
if ($results) {
$firstAttempt = current($results);
$exeId = $firstAttempt['exe_id'];
ob_start();
$stats = ExerciseLib::displayQuestionListByAttempt(
$exercise,
$exeId,
false
);
ob_end_clean();
$categoryList = $stats['category_list'];
$resultsArray = [];
foreach ($categoryList as $category_id => $category_item) {
$resultsArray[] = round($category_item['score'] / $category_item['total'] * 10);
}
$dataSet[] = $resultsArray;
}
}
}
return $this->getRadar($labels, $dataSet);
}
public function getRadar($labels, $dataSet)
{
if (empty($labels) || empty($dataSet)) {
return '';
}
$labels = json_encode($labels);
// Default preset, after that colors are generated randomly. @todo improve colors. Use a js lib?
$colorList = [
'rgb(255, 99, 132, 1.0)',
'rgb(0,0,200,1.0)', // red
'rgb(255, 159, 64, 1.0)', // orange
'rgb(255, 205, 86, 1.0)', //yellow
'rgb(75, 192, 192, 1.0)', // green
'rgb(54, 162, 235, 1.0)', // blue
'rgb(153, 102, 255, 1.0)', // purple
//'rgb(201, 203, 207)' grey
];
$dataSetToJson = [];
$counter = 0;
foreach ($dataSet as $resultsArray) {
$color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0,255).', '.rand(0,255).', '.rand(0,255).', 1.0)';
$background = str_replace('1.0', '0.2', $color);
$dataSetToJson[] = [
'fill' => true,
//'label' => '".get_lang('Categories')."',
'backgroundColor' => $background,
'borderColor' => $color,
'pointBackgroundColor' => $color,
'pointBorderColor' => '#fff',
'pointHoverBackgroundColor' => '#fff',
'pointHoverBorderColor' => $color,
'pointRadius' => 6,
'pointBorderWidth' => 3,
'pointHoverRadius' => 10,
'data' => $resultsArray,
];
$counter++;
}
$resultsToJson = json_encode($dataSetToJson);
return "
<canvas id='categoryRadar' width='400' height='200'></canvas>
<script>
var data = {
labels: $labels,
datasets: $resultsToJson
}
var options = {
scale: {
angleLines: {
display: false
},
ticks: {
beginAtZero: true,
min: 0,
max: 10,
stepSize: 1
},
pointLabels: {
fontSize: 14,
//fontStyle: 'bold'
},
},
elements: {
line: {
tension: 0,
borderWidth: 3
}
},
legend: {
//position: 'bottom'
display: false
}
};
var ctx = document.getElementById('categoryRadar').getContext('2d');
var myRadarChart = new Chart(ctx, {
type: 'radar',
data: data,
options: options
});
</script>
";
}
}

@ -5,6 +5,7 @@
use ChamiloSession as Session;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
if (false === api_get_configuration_value('block_category_questions')) {
api_not_allowed(true);

@ -11,6 +11,7 @@ use ChamiloSession as Session;
* @author Julio Montoya switchable fill in blank option added
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$this_section = SECTION_COURSES;

@ -19,7 +19,7 @@ use ChamiloSession as Session;
*/
$debug = false;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$this_section = SECTION_COURSES;
api_protect_course_script(true);
@ -60,6 +60,9 @@ $interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
if (RESULT_DISABLE_RADAR === (int) $objExercise->results_disabled) {
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
}
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';

@ -15,7 +15,7 @@ use ChamiloSession as Session;
* @todo small letters for table variables
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$origin = api_get_origin();
$currentUserId = api_get_user_id();
$printHeaders = 'learnpath' === $origin;

@ -35,6 +35,7 @@ $debug = false;
// Notice for unauthorized people.
api_protect_course_script(true);
$origin = api_get_origin();
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
$courseId = api_get_course_int_id();
@ -188,10 +189,17 @@ $exerciseInSession = Session::read('objExercise');
// 3. $objExercise is not set, then return to the exercise list.
if (!is_object($objExercise)) {
header('Location: exercise.php');
header('Location: exercise.php?'.api_get_cidreq());
exit;
}
if ('true' === api_get_plugin_setting('positioning', 'tool_enable')) {
$plugin = Positioning::create();
if ($plugin->blockFinalExercise(api_get_user_id(), $objExercise->iId, api_get_course_int_id(), $sessionId)) {
api_not_allowed(true);
}
}
// if the user has submitted the form.
$exercise_title = $objExercise->selectTitle();
$exercise_sound = $objExercise->selectSound();

@ -8,6 +8,7 @@ use ChamiloSession as Session;
* @author Julio Montoya <gugli100@gmail.com>
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
api_protect_course_script();

@ -8,11 +8,8 @@
* @author Julio Montoya <gugli100@gmail.com>
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
Exercise::cleanSessionVariables();
$this_section = SECTION_COURSES;
$js = '<script>'.api_get_language_translate_html().'</script>';
@ -31,6 +28,13 @@ if (!$result) {
api_not_allowed(true);
}
if ('true' === api_get_plugin_setting('positioning', 'tool_enable')) {
$plugin = Positioning::create();
if ($plugin->blockFinalExercise(api_get_user_id(), $exercise_id, api_get_course_int_id(), $sessionId)) {
api_not_allowed(true);
}
}
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
$learnpathItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : null;

@ -10,6 +10,7 @@ use ChamiloSession as Session;
* @author Julio Montoya - Simple exercise result page
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$id = isset($_REQUEST['id']) ? (int) $_GET['id'] : 0; // exe id
$show_headers = isset($_REQUEST['show_headers']) ? (int) $_REQUEST['show_headers'] : null;

@ -24,10 +24,9 @@ use ChamiloSession as Session;
* @copyright Patrick Cool
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_FORUM;
api_protect_course_script(true);
$current_course_tool = TOOL_FORUM;
$htmlHeadXtra[] = '<script>
$(function() {
$(\'.hide-me\').slideUp();

@ -6,6 +6,7 @@ use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
use ChamiloSession as Session;
require_once __DIR__.'/../global.inc.php';
$current_course_tool = TOOL_QUIZ;
$debug = false;
// Check if the user has access to the contextual course/session
api_protect_course_script(true);

@ -1188,19 +1188,19 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
default:
case COURSE_VISIBILITY_CLOSED:
// Completely closed: the course is only accessible to the teachers. - 0
if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
if ($isAllowedInCourse && api_get_user_id() && !api_is_anonymous()) {
$is_visible = true;
}
break;
case COURSE_VISIBILITY_REGISTERED:
// Private - access authorized to course members only - 1
if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
if ($isAllowedInCourse && api_get_user_id() && !api_is_anonymous()) {
$is_visible = true;
}
break;
case COURSE_VISIBILITY_OPEN_PLATFORM:
// Open - access allowed for users registered on the platform - 2
if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
if ($isAllowedInCourse && api_get_user_id() && !api_is_anonymous()) {
$is_visible = true;
}
break;
@ -1216,10 +1216,9 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
break;
}
//If password is set and user is not registered to the course then the course is not visible
if ($isAllowedInCourse == false &&
isset($course_info['registration_code']) &&
!empty($course_info['registration_code'])
// If password is set and user is not registered to the course then the course is not visible.
if (false == $isAllowedInCourse &&
isset($course_info['registration_code']) && !empty($course_info['registration_code'])
) {
$is_visible = false;
}
@ -1251,6 +1250,43 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
return false;
}
if ($is_visible && 'true' === api_get_plugin_setting('positioning', 'tool_enable')) {
$plugin = Positioning::create();
$block = $plugin->get('block_course_if_initial_exercise_not_attempted');
if ('true' === $block) {
$currentPath = $_SERVER['PHP_SELF'];
// Allowed only this course paths.
$paths = [
'/plugin/positioning/start.php',
'/plugin/positioning/start_student.php',
'/main/course_home/course_home.php',
'/main/exercise/overview.php'
];
if (!in_array($currentPath, $paths, true)) {
// Check if entering an exercise.
global $current_course_tool;
if ('quiz' !== $current_course_tool) {
$initialData = $plugin->getInitialExercise($course_info['real_id'], $session_id);
if ($initialData && isset($initialData['exercise_id'])) {
$results = Event::getExerciseResultsByUser(
api_get_user_id(),
$initialData['exercise_id'],
$course_info['real_id'],
$session_id
);
if (empty($results)) {
api_not_allowed($print_headers);
return false;
}
}
}
}
}
}
apiBlockInactiveUser();
return true;

@ -536,6 +536,26 @@ class CourseHome
)';
}
if ('true' === api_get_plugin_setting('positioning', 'tool_enable')) {
$plugin = Positioning::create();
$block = $plugin->get('block_course_if_initial_exercise_not_attempted');
if ('true' === $block) {
$initialData = $plugin->getInitialExercise($course_id, $sessionId);
if ($initialData && isset($initialData['exercise_id'])) {
$results = Event::getExerciseResultsByUser(
$userId,
$initialData['exercise_id'],
$course_id,
$sessionId
);
if (empty($results)) {
$conditions .= ' AND t.name = "positioning"';
}
}
}
}
// Add order if there are LPs
$sql = "SELECT t.* FROM $course_tool_table t
LEFT JOIN $lpTable l
@ -544,12 +564,14 @@ class CourseHome
ON (t.c_id = lc.c_id AND l.category_id = lc.iid)
$conditions AND
t.c_id = $course_id $condition_session
ORDER BY
CASE WHEN l.category_id IS NULL THEN 0 ELSE 1 END,
CASE WHEN l.display_order IS NULL THEN 0 ELSE 1 END,
lc.position,
l.display_order,
t.id";
$orderBy = '';
break;
case TOOL_AUTHORING:

@ -380,7 +380,7 @@ class ExerciseLib
$header1 = Display::tag('th', '&nbsp;');
$cpt1 = 0;
foreach ($objQuestionTmp->options as $item) {
$colorBorder1 = ($cpt1 == (count($objQuestionTmp->options) - 1))
$colorBorder1 = $cpt1 == (count($objQuestionTmp->options) - 1)
? '' : 'border-right: solid #FFFFFF 1px;';
if ($item === 'True' || $item === 'False') {
$header1 .= Display::tag(
@ -4912,7 +4912,9 @@ EOT;
echo $chartMultiAnswer;
}
if (!empty($category_list) && ($show_results || $show_only_score)) {
if (!empty($category_list) &&
($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
) {
// Adding total
$category_list['total'] = [
'score' => $total_score,

@ -0,0 +1,10 @@
Positioning
===
This plugin adds a positioning test tool in every course.
Positioning tests should be used before and after any course. One initial test should be taken before anything else is used in the course, and a final test (usually a copy of the same test) should be used at the end. A radar-chart will show the differences between the initial test and the final test for each learner.
For a test to be used as initial or final test, it has to match 3 criteria:
- use at least 3 question categories
- use the "radar" type result page
- have only one available attempt

@ -0,0 +1,7 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';
Positioning::create()->install();

@ -0,0 +1,22 @@
<?php
$strings['plugin_title'] = 'Positioning';
$strings['plugin_comment'] = 'Adds positioning tests to the course homepage';
$strings['tool_enable'] = 'Enable plugin';
$strings['block_course_if_initial_exercise_not_attempted'] = 'Block other course tools';
$strings['block_course_if_initial_exercise_not_attempted_help'] = 'When this option is enabled, if an initial positioning test has been configured, the learner will not be able to use the rest of the course on the course homepage until he/she has completed the initial test.';
$strings['average_percentage_to_unlock_final_exercise'] = 'End test unlock threshold';
$strings['average_percentage_to_unlock_final_exercise_help'] = 'Learners *must* have an average progress of at least this percentage (e.g. \'75\' for a 75% progress required) in the combined learning paths of this course.';
$strings['PositioningIntroduction'] = 'Please select one initial test and one final test below. Only tests that match the 3 following criteria will be available for selection: use at least 3 question categories, use the radar report mode *and* have only one possible attempt allowed. Once these are selected, the student will only be able to pass the final test if he/she has completed the average of all learning paths in this course to %S or more.';
$strings['SelectAsInitialTest'] = 'Select as initial test';
$strings['UnselectAsInitialTest'] = 'Unselect as initial test';
$strings['SelectAsFinalTest'] = 'Select as final test';
$strings['UnselectAsFinalTest'] = 'Unselect as final test';
$strings['InviteToTakePositioningTest'] = 'Please take this positioning test before you start. This will help us measure the quality of this course.';
$strings['InitialTest'] = 'Initial test';
$strings['YouMustCompleteAThresholdToTakeFinalTest'] = 'You must complete at least %s of progress on average for all learning paths to unlock the final test.';
$strings['FinalTest'] = 'Final test';
$strings['Positioning'] = 'Positioning';

@ -0,0 +1,5 @@
<?php
$strings['plugin_title'] = 'Positionnement';
$strings['plugin_comment'] = '';
$strings['tool_enable'] = 'Activer le plugin';

@ -0,0 +1,5 @@
<?php
$strings['plugin_title'] = 'Posicionamiento';
$strings['plugin_comment'] = '';
$strings['tool_enable'] = 'Activar plugin';

@ -0,0 +1,7 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin_info = Positioning::create()->get_info();

@ -0,0 +1,232 @@
<?php
/* For licensing terms, see /license.txt */
class Positioning extends Plugin
{
public $isCoursePlugin = true;
public $table;
/**
* Class constructor.
*/
protected function __construct()
{
parent::__construct(
'1.0',
'Julio Montoya',
[
'tool_enable' => 'boolean',
'block_course_if_initial_exercise_not_attempted' => 'boolean',
'average_percentage_to_unlock_final_exercise' => 'text',
]
);
$this->table = Database::get_main_table('plugin_positioning_exercise');
}
public static function create()
{
static $result = null;
return $result ? $result : $result = new self();
}
public function install()
{
$table = $this->table;
$sql = 'CREATE TABLE IF NOT EXISTS '.$table.' (
id INT unsigned NOT NULL auto_increment PRIMARY KEY,
exercise_id INT unsigned NOT NULL,
c_id INT unsigned NOT NULL,
session_id INT unsigned DEFAULT NULL,
is_initial TINYINT(1) NOT NULL,
is_final TINYINT(1) NOT NULL
)';
Database::query($sql);
// Installing course settings
$this->install_course_fields_in_all_courses();
}
public function uninstall()
{
$table = $this->table;
Database::query("DROP TABLE IF EXISTS $table");
}
public function isInitialExercise($exerciseId, $courseId, $sessionId)
{
$data = $this->getPositionData($exerciseId, $courseId, $sessionId);
if ($data && isset($data['is_initial']) && 1 === (int) $data['is_initial']) {
return true;
}
return false;
}
public function getPositionData($exerciseId, $courseId, $sessionId)
{
$table = $this->table;
$courseId = (int) $courseId;
$sessionId = (int) $sessionId;
$sql = "SELECT * FROM $table
WHERE
exercise_id = $exerciseId AND
c_id = $courseId AND
session_id = $sessionId
";
$result = Database::query($sql);
if (Database::num_rows($result) > 0) {
return Database::fetch_array($result, 'ASSOC');
}
return false;
}
public function isFinalExercise($exerciseId, $courseId, $sessionId)
{
$data = $this->getPositionData($exerciseId, $courseId, $sessionId);
if ($data && isset($data['is_final']) && 1 === (int) $data['is_final']) {
return true;
}
}
public function setInitialExercise($exerciseId, $courseId, $sessionId)
{
$this->setOption('is_initial', $exerciseId, $courseId, $sessionId);
}
private function setOption($field, $exerciseId, $courseId, $sessionId)
{
if (!in_array($field, ['is_initial', 'is_final'], true)) {
return false;
}
$data = $this->getPositionData($exerciseId, $courseId, $sessionId);
$disableField = $field === 'is_initial' ? 'is_final' : 'is_initial';
if ($data && isset($data['id'])) {
$id = $data['id'];
$sql = "UPDATE $this->table SET
$field = 1,
$disableField = 0
WHERE id = $id";
Database::query($sql);
$sql = "DELETE FROM $this->table
WHERE $field = 1 AND c_id = $courseId AND session_id = $sessionId AND id <> $id";
Database::query($sql);
} else {
$params = [
'exercise_id' => $exerciseId,
'c_id' => $courseId,
'session_id' => $sessionId,
$field => 1,
$disableField => 0,
];
$id = Database::insert($this->table, $params);
$sql = "DELETE FROM $this->table
WHERE $field = 1 AND c_id = $courseId AND session_id = $sessionId AND id <> $id";
Database::query($sql);
}
}
public function setFinalExercise($exerciseId, $courseId, $sessionId)
{
$this->setOption('is_final', $exerciseId, $courseId, $sessionId);
}
public function blockFinalExercise($userId, $exerciseId, $courseId, $sessionId)
{
$initialData = $this->getInitialExercise($courseId, $sessionId);
if (empty($initialData)) {
return false;
}
if ($initialData && isset($initialData['exercise_id'])) {
// If this is final exercise?
$finalData = $this->getFinalExercise($courseId, $sessionId);
if (!empty($finalData) && $finalData['exercise_id'] && $exerciseId == $finalData['exercise_id']) {
$initialResults = Event::getExerciseResultsByUser(
$userId,
$initialData['exercise_id'],
$courseId,
$sessionId
);
if (empty($initialResults)) {
return true;
}
$averageToUnlock = (int) $this->get('average_percentage_to_unlock_final_exercise');
if (empty($averageToUnlock)) {
return false;
}
// Check average
$courseInfo = api_get_course_info_by_id($courseId);
$userAverage = (int) Tracking::getAverageStudentScore($userId, $courseInfo['code'], [], $sessionId);
if ($userAverage >= $averageToUnlock) {
return false;
}
return true;
} else {
return false;
}
}
return true;
}
public function getInitialExercise($courseId, $sessionId)
{
return $this->getCourseExercise($courseId, $sessionId, true, false);
}
private function getCourseExercise($courseId, $sessionId, $isInitial, $isFinal)
{
$table = $this->table;
$courseId = (int) $courseId;
$sessionId = (int) $sessionId;
$sql = "SELECT * FROM $table
WHERE
c_id = $courseId AND
session_id = $sessionId
";
if ($isInitial) {
$sql .= ' AND is_initial = 1 ';
} else {
$sql .= ' AND is_initial = 0 ';
}
if ($isFinal) {
$sql .= ' AND is_final = 1 ';
} else {
$sql .= ' AND is_final = 0 ';
}
$result = Database::query($sql);
if (Database::num_rows($result) > 0) {
return Database::fetch_array($result, 'ASSOC');
}
return false;
}
public function getFinalExercise($courseId, $sessionId)
{
return $this->getCourseExercise($courseId, $sessionId, false, true);
}
}

@ -0,0 +1,44 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_admin_script();
$plugin = Positioning::create();
$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null;
switch ($action) {
case 'delete-file':
$path = isset($_REQUEST['path']) ? $_REQUEST['path'] : null;
if (empty($path)) {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]);
exit;
}
if (unlink($path)) {
Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success');
echo json_encode(["status" => "true"]);
} else {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorDeleteFile')]);
}
break;
case 'delete-files-list':
$list = isset($_REQUEST['list']) ? $_REQUEST['list'] : [];
if (empty($list)) {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]);
exit;
}
foreach ($list as $value) {
if (empty($value)) {
continue;
}
unlink($value);
}
Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success');
echo json_encode(["status" => "true"]);
break;
}

@ -0,0 +1,116 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
// Students are redirected to the start_student.php
api_location(api_get_path(WEB_PLUGIN_PATH).'positioning/start_student.php?'.api_get_cidreq());
}
$plugin = Positioning::create();
if (!$plugin->isEnabled()) {
api_not_allowed(true);
}
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
$formToString = '';
$currentUrl = api_get_self().'?'.api_get_cidreq();
$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
switch ($action) {
case 'set_initial':
Display::addFlash(Display::return_message(get_lang('Updated')));
$plugin->setInitialExercise($id, $courseId, $sessionId);
api_location($currentUrl);
break;
case 'set_final':
Display::addFlash(Display::return_message(get_lang('Updated')));
$plugin->setFinalExercise($id, $courseId, $sessionId);
api_location($currentUrl);
break;
}
$nameTools = $plugin->get_lang('Positioning');
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
$template = new Template($nameTools);
$url = $currentUrl.'&';
$actions = function ($row) use ($plugin, $url, $courseId, $sessionId) {
$classInitial = 'btn btn-default';
if ($plugin->isInitialExercise($row['iid'], $courseId, $sessionId)) {
$classInitial = 'btn btn-primary disabled';
}
$classFinal = 'btn btn-default';
if ($plugin->isFinalExercise($row['iid'], $courseId, $sessionId)) {
$classFinal = 'btn btn-primary disabled';
}
$actions = Display::url(
$plugin->get_lang('SelectAsInitialTest'),
$url.'&action=set_initial&id='.$row['iid'],
['class' => $classInitial]
);
$actions .= Display::url(
$plugin->get_lang('SelectAsFinalTest'),
$url.'&action=set_final&id='.$row['iid'],
['class' => $classFinal]
);
return $actions;
};
$table = Exercise::exerciseGrid(
0,
null,
null,
null,
null,
false,
3,
RESULT_DISABLE_RADAR,
1,
$actions,
true
);
$table->headers = [];
$table->set_header(0, get_lang('ExerciseName'), false);
$table->set_header(1, get_lang('QuantityQuestions'), false);
$table->set_header(2, get_lang('Actions'), false);
$exerciseList = [];
foreach ($table->table_data as &$data) {
$data = [
$data[1],
$data[2],
$data[3],
];
}
$initialData = $plugin->getInitialExercise($courseId, $sessionId);
$users = CourseManager::get_user_list_from_course_code(api_get_course_id(), $sessionId);
$radars = '';
$initialExerciseTitle = '';
if (!empty($users) && $initialData && $initialData['exercise_id']) {
$users = array_column($users, 'user_id');
$exerciseId = $initialData['exercise_id'];
$initialExercise = new Exercise();
$initialExercise->read($exerciseId);
$radars = $initialExercise->getRadarsFromUsers($users, [$initialExercise], $courseId, $sessionId);
$initialExerciseTitle = $initialExercise->get_formated_title();
}
$table->set_form_actions([]);
$exercises = $table->return_table();
$template->assign('grid', $exercises);
$template->assign('radars', $radars);
$template->assign('initial_exercise', $initialExerciseTitle);
$template->assign('content', $template->fetch('positioning/view/start.tpl'));
$template->display_one_col_template();

@ -0,0 +1,94 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_course_script(true);
$plugin = Positioning::create();
if (!$plugin->isEnabled()) {
api_not_allowed(true);
}
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
$formToString = '';
$currentUrl = api_get_self().'?'.api_get_cidreq();
$courseId = api_get_course_int_id();
$courseCode = api_get_course_id();
$sessionId = api_get_session_id();
$currentUserId = api_get_user_id();
$initialData = $plugin->getInitialExercise($courseId, $sessionId);
$finalData = $plugin->getFinalExercise($courseId, $sessionId);
$initialExerciseTitle = '';
$radar = '';
$initialResults = null;
$exercisesToRadar = [];
if ($initialData) {
$exerciseId = $initialData['exercise_id'];
$initialExercise = new Exercise();
$initialExercise->read($exerciseId);
$initialResults = Event::getExerciseResultsByUser(
$currentUserId,
$initialData['exercise_id'],
$courseId,
$sessionId
);
$initialExerciseTitle = $initialExercise->get_formated_title();
if (empty($initialResults)) {
$url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exerciseId;
$initialExerciseTitle = Display::url($initialExercise->get_formated_title(), $url);
} else {
$exercisesToRadar[] = $initialExercise;
}
}
$studentAverage = (int) Tracking::getAverageStudentScore($currentUserId, $courseCode, [], $sessionId);
$averageToUnlock = (int) $plugin->get('average_percentage_to_unlock_final_exercise');
$finalExerciseTitle = '';
if ($finalData) {
$exerciseId = $finalData['exercise_id'];
$finalExercise = new Exercise();
$finalExercise->read($exerciseId);
$finalResults = Event::getExerciseResultsByUser(
api_get_user_id(),
$initialData['exercise_id'],
$courseId,
$sessionId
);
$finalExerciseTitle = $finalExercise->get_formated_title();
if (!empty($initialResults)) {
if ($studentAverage >= $averageToUnlock) {
$url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exerciseId;
if (empty($finalResults)) {
$finalExerciseTitle = Display::url($finalExercise->get_formated_title(), $url);
}
}
$exercisesToRadar[] = $finalExercise;
}
}
$radars = $initialExercise->getRadarsFromUsers([$currentUserId], $exercisesToRadar, $courseId, $sessionId);
$nameTools = $plugin->get_lang('Positioning');
$template = new Template($nameTools);
$template->assign('initial_exercise', $initialExerciseTitle);
$template->assign('final_exercise', $finalExerciseTitle);
$template->assign(
'average_percentage_to_unlock_final_exercise',
$averageToUnlock
);
$template->assign('average', $studentAverage);
$template->assign('radars', $radars);
$template->assign('content', $template->fetch('positioning/view/start_student.tpl'));
$template->display_one_col_template();

@ -0,0 +1,7 @@
<?php
/* For license terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';
Positioning::create()->uninstall();

@ -0,0 +1,7 @@
{{ grid }}
{% if radars %}
<h4>{{ "InitialTest"| get_plugin_lang('Positioning') }}: {{ initial_exercise }}</h4>
{{ radars }}
{% endif %}

@ -0,0 +1,11 @@
<h3>{{ "InviteToTakePositioningTest"| get_plugin_lang('Positioning') }}</h3>
<p>{{ "InitialTest"| get_plugin_lang('Positioning') }}: {{ initial_exercise }}</p>
<h3>{{ "YouMustCompleteAThresholdToTakeFinalTest"| get_plugin_lang('Positioning') | format(average_percentage_to_unlock_final_exercise) }}</h3>
<p>{{ "Average"| get_lang }}: {{ average }}</p>
<p>{{ "FinalTest"| get_plugin_lang('Positioning') }}: {{ final_exercise }}</p>
{{ radars }}
Loading…
Cancel
Save