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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
3 changes: 3 additions & 0 deletions include/main_lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ function load_js($file, $init='') {
$file = 'jquery.ui.touch-punch.min.js';
} elseif ($file == 'drag-and-drop-shapes') {
$file = 'drag-and-drop-shapes.js';
} elseif ($file == 'codemirror') {
$head_content .= css_link('codemirror/lib/codemirror.css');
$file = 'codemirror/lib/codemirror.js';
}

$head_content .= js_link($file);
Expand Down
6 changes: 6 additions & 0 deletions js/build/build.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
}
}

// Copy CodeMirror 5
removeDir('js/codemirror');
mkdir('js/codemirror');
recurse_copy('node_modules/codemirror/lib', 'js/codemirror/lib');
recurse_copy('node_modules/codemirror/mode', 'js/codemirror/mode');

function get_base_path() {
$path = dirname(dirname(dirname(__FILE__)));
if (DIRECTORY_SEPARATOR !== '/') {
Expand Down
7 changes: 4 additions & 3 deletions js/tinymce/plugins/latexhelper/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ body {
.symbol-panel { display: none; }
.symbol-panel.active {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 8px;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
}

/* Wide Grid for Chemistry */
Expand All @@ -136,7 +136,8 @@ body {
}

.symbol-btn {
height: 60px;
height: 70px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
Expand Down
4 changes: 2 additions & 2 deletions js/tinymce/plugins/latexhelper/latex_codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ window.latexSymbols = {
{ symbol: '\\Gamma', code: '\\Gamma', name: 'Gamma (capital)' },
{ symbol: '\\Delta', code: '\\Delta', name: 'Delta (capital)' },
{ symbol: '\\mathrm{E}', code: '\\mathrm{E}', name: 'Epsilon (capital)'},
{ symbol: '\\Z', code: 'Z', name: 'Zeta (capital)'},
{ symbol: '\\mathrm{Z}', code: '\\mathrm{Z}', name: 'Zeta (capital)'},
{ symbol: '\\mathrm{H}', code: '\\mathrm{H}', name: 'Eta (capital)'},
{ symbol: '\\Theta', code: '\\Theta', name: 'Theta (capital)' },
{ symbol: '\\mathrm{I}', code: '\\mathrm{I}', name: 'Iota (capital)'},
Expand Down Expand Up @@ -85,7 +85,7 @@ window.latexSymbols = {
{ symbol: '\\oiiint', code: '\\oiiint', name: 'Volume integral' }
],
'Relations': [
{ symbol: '\\=', code: '\\=', name: 'Equals' },
{ symbol: '=', code: '=', name: 'Equals' },
{ symbol: '\\neq', code: '\\neq', name: 'Not equals' },
{ symbol: '\\approx', code: '\\approx', name: 'Approximately' },
{ symbol: '\\equiv', code: '\\equiv', name: 'Equivalent' },
Expand Down
2 changes: 2 additions & 0 deletions lang/el/messages.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,8 @@
$langEndMessageInfo = "εμφανίζεται μετά την ολοκλήρωση της άσκησης";
$langExercisePreventCopy = 'Αποτροπή αντιγραφής κειμένου';
$langExercisePreventCopyExplanation = 'Αποτροπή αντιγραφής και επικόλλησης κειμένου από την οθόνη κατά την εκτέλεση της άσκησης';
$langCodeExercise = 'Άσκηση κώδικα';
$langCodeExerciseLang = 'Γλώσσα προγραμματισμού';
$langStricterExamRestriction = "Αυστηρός περιορισμός";
$langExerciseWillBeCanceledInStrictMode = "Η εξέταση ακυρώνεται σε παράλληλες ενέργειες χρηστών όπως ανακατεύθυνση σε νέα σελίδα ή άνοιγμα νέου παραθύρου";
$langExerciseNoCalcGradeMethod = "Κανονικός";
Expand Down
2 changes: 2 additions & 0 deletions lang/en/messages.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -5558,6 +5558,8 @@
$langNoticeCourseDeleted = "If the $langsCourse was deleted by you, you can ignore this email. If the deletion was done by mistake and you wish to restore your $langsCourse, please contact the platform administrators.";
$langExercisePreventCopy = 'Prevent Text Copying';
$langExercisePreventCopyExplanation = 'Prevent copying and pasting text during execution of the exercise';
$langCodeExercise = 'Code Exercise';
$langCodeExerciseLang = 'Programming Language';
$langDoubleLoginLock = 'You have logged into the platform from another device
with the same account. Double logins are disabled, so your current login has
been disconnected.';
Expand Down
17 changes: 16 additions & 1 deletion modules/exercise/FreeTextAnswer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,25 @@ public function AnswerQuestion($question_number, $exerciseResult = [], $options
$text = $exerciseResult[$questionId];
}

$html_content .= "
// Check if this is a code exercise
$questionOptions = Database::get()->querySingle("SELECT options FROM exercise_question WHERE id = ?d", $questionId)->options;
$questionOpts = json_decode($questionOptions ?? '', true);
$isCodeExercise = ($questionOpts['code_exercise'] ?? false) === true;
$codeLanguage = $questionOpts['code_language'] ?? 'javascript';

if ($isCodeExercise) {
// Render textarea for CodeMirror
$html_content .= "
<div class='col-12' id='freetext_{$questionId}'>
<textarea name='choice[$questionId]' id='code_editor_{$questionId}' class='code-exercise-editor form-control' rows='14' data-language='" . q($codeLanguage) . "'>" . q($text) . "</textarea>
</div>";
} else {
// Render rich text editor (TinyMCE)
$html_content .= "
<div class='col-12' id='freetext_{$questionId}'>
" . rich_text_editor("choice[$questionId]", 14, 90, $text, options: $options) . "
</div>";
}

return $html_content;
}
Expand Down
48 changes: 48 additions & 0 deletions modules/exercise/code_exercise_languages.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* Single source of truth for code exercise languages.
* Used by statement_admin.inc.php (dropdown) and exercise_submit.php (load mode scripts).
* Programming language names are the same in all locales.
*
* Author: Marios Giannopoulos
*/

if (!isset($CODE_EXERCISE_LANGUAGES)) {

$CODE_EXERCISE_LANGUAGES = [
'javascript' => [
'mode' => 'javascript/javascript.js',
'name' => 'JavaScript',
],
'python' => [
'mode' => 'python/python.js',
'name' => 'Python',
],
'php' => [
'mode' => 'php/php.js',
'name' => 'PHP',
'extra' => ['clike/clike.js'],
],
'text/x-c++src' => [
'mode' => 'clike/clike.js',
'name' => 'C++',
],
'text/x-java' => [
'mode' => 'clike/clike.js',
'name' => 'Java',
],
'sql' => [
'mode' => 'sql/sql.js',
'name' => 'SQL',
],
'text/html' => [
'mode' => 'htmlmixed/htmlmixed.js',
'name' => 'HTML',
],
'css' => [
'mode' => 'css/css.js',
'name' => 'CSS',
],
];
}
83 changes: 83 additions & 0 deletions modules/exercise/exercise_submit.php
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,89 @@ function default_settings() {
}
}

// Check if any FREE_TEXT question is a code exercise
$hasCodeExercise = false;
$codeEditors = [];
foreach ($questionList as $q_id) {
$t_question = $questions[$q_id] ?? null;
if ($t_question && $t_question->selectType() == FREE_TEXT) {
$qOptions = $t_question->selectOptions();
$qOpts = json_decode($qOptions ?? '', true);
if (($qOpts['code_exercise'] ?? false) === true) {
$hasCodeExercise = true;
$codeEditors[] = [
'id' => $q_id,
'language' => $qOpts['code_language'] ?? 'javascript'
];
}
}
}

// Load CodeMirror if needed (only the mode(s) for the selected language(s))
if ($hasCodeExercise) {
require_once __DIR__ . '/code_exercise_languages.inc.php';
load_js('codemirror');
$head_content .= '<link rel="stylesheet" href="' . $urlAppend . 'js/codemirror/lib/codemirror.css">';
$head_content .= '
<style>
.code-exercise-header-bar{
height: 30px;
background: #F7F7F7;
border:1px solid #ddd;
border-bottom:none;
border-radius:4px 4px 0 0;
}
</style>';
$languagesToLoad = array_unique(array_column($codeEditors, 'language'));
$loadedModes = [];
foreach ($languagesToLoad as $lang) {
$opts = $CODE_EXERCISE_LANGUAGES[$lang] ?? null;
if ($opts) {
if (!in_array($opts['mode'], $loadedModes)) {
$head_content .= js_link('codemirror/mode/' . $opts['mode']);
$loadedModes[] = $opts['mode'];
}
if (!empty($opts['extra'])) {
foreach ($opts['extra'] as $extra) {
if (!in_array($extra, $loadedModes)) {
$head_content .= js_link('codemirror/mode/' . $extra);
$loadedModes[] = $extra;
}
}
}
}
}
// Add initialization script
$head_content .= "
<script>
$(document).ready(function() {
document.querySelectorAll('.code-exercise-editor').forEach(function(textarea) {
var questionId = textarea.id.replace('code_editor_', '');
var language = textarea.getAttribute('data-language') || 'javascript';
var editor = CodeMirror.fromTextArea(textarea, {
lineNumbers: true,
mode: language
});
var wrapper = textarea.nextElementSibling;
if (wrapper && wrapper.classList.contains('CodeMirror')) {
var headerBar = document.createElement('div');
headerBar.className = 'code-exercise-header-bar';
wrapper.parentNode.insertBefore(headerBar, wrapper);
}
editor.on('change', function() {
if (editor.getValue().trim() !== '') {
var qPanel = $('#qPanel' + questionId);
var qCheck = qPanel.find('span').first();
var qButton = $('#' + qCheck.attr('id').replace('qCheck', 'q_num'));
qCheck.addClass('fa fa-check');
qButton.removeClass('btn-default').addClass('btn-info');
}
});
});
});
</script>";
}

if ($questionList) {
// Display a notification that informs the user that the exercise will be canceled.
// if ($is_exam && $stricterExamMode && $exerciseType == SINGLE_PAGE_TYPE) {
Expand Down
80 changes: 80 additions & 0 deletions modules/exercise/statement_admin.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ function hideGrade(){
function showGrade(){
$('input[name=questionGrade]').prop('disabled', false).closest('div.form-group').removeClass('hide');
}
function hideCodeExercise(){
$('#codeExerciseWrapper').addClass('hide');
$('input[name=code_exercise]').prop('disabled', true);
$('select[name=code_language]').prop('disabled', true);
}
function showCodeExercise(){
$('#codeExerciseWrapper').removeClass('hide');
$('input[name=code_exercise]').prop('disabled', false);
$('select[name=code_language]').prop('disabled', false);
}
function toggleCodeLanguage(){
if ($('input[name=code_exercise]').is(':checked')) {
$('#codeLanguageWrapper').removeClass('hide');
$('select[name=code_language]').prop('disabled', false);
} else {
$('#codeLanguageWrapper').addClass('hide');
$('select[name=code_language]').prop('disabled', true);
}
}
function showFillInChoices(){
$('#fillInBlanksOptions').removeClass('hide');
}
Expand All @@ -94,16 +113,26 @@ function updateFillInBlanksAnswerTypeValue(){
if (selectedOption.selected && selectedOption.value == 7) {
$('.fill_in_blank_strict').removeClass('d-none').addClass('d-block');
hideGrade();
hideCodeExercise();
} else {
$('.fill_in_blank_strict').removeClass('d-block').addClass('d-none');
if (selectedOption.selected && (selectedOption.value == 6 || selectedOption.value == 13)) {
showGrade();
if (selectedOption.value == 6) {
showCodeExercise();
} else {
hideCodeExercise();
}
} else {
hideGrade();
hideCodeExercise();
}
}
});

// Handle code exercise checkbox toggle
$('input[name=code_exercise]').on('change', toggleCodeLanguage);

$('.deletePicture').on('click', function () {
var eid = $(this).attr('data-exercise-id');
var qid = $(this).attr('data-question-id');
Expand Down Expand Up @@ -177,6 +206,22 @@ function updateFillInBlanksAnswerTypeValue(){
if (isset($_POST['questionGrade'])) {
$objQuestion->updateWeighting(str_replace(',', '.', $_POST['questionGrade']));
}

// Save code exercise options (only for Free text questions)
if ($answerType == FREE_TEXT) {
$codeOptions = [];
if (isset($_POST['code_exercise']) && $_POST['code_exercise'] == '1') {
$codeOptions['code_exercise'] = true;
$codeOptions['code_language'] = $_POST['code_language'] ?? 'javascript';
} else {
$codeOptions['code_exercise'] = false;
}
$objQuestion->updateOptions(json_encode($codeOptions));
} else {
// Clear code exercise options for non-free-text questions
$objQuestion->updateOptions(null);
}

if (isset($_GET['exerciseId'])) {
$exerciseId = intval($_GET['exerciseId']);
$objQuestion->save($exerciseId);
Expand Down Expand Up @@ -256,6 +301,12 @@ function updateFillInBlanksAnswerTypeValue(){
$difficulty = $objQuestion->selectDifficulty();
$category = $objQuestion->selectCategory();
$questionWeight = $objQuestion->selectWeighting();

// Load code exercise options
$questionOptions = $objQuestion->selectOptions();
$codeExerciseOptions = json_decode($questionOptions ?? '', true);
$codeExerciseEnabled = ($codeExerciseOptions['code_exercise'] ?? false) === true;
$codeLanguage = $codeExerciseOptions['code_language'] ?? 'javascript';
}
}
if (isset($_GET['newQuestion']) || isset($_GET['modifyQuestion'])) {
Expand Down Expand Up @@ -369,6 +420,35 @@ function updateFillInBlanksAnswerTypeValue(){
</div>
</div>";

// Code exercise checkbox and language dropdown (only for FREE_TEXT type 6)
require_once __DIR__ . '/code_exercise_languages.inc.php';
$codeExerciseChecked = ($codeExerciseEnabled ?? false) ? 'checked' : '';
$codeLanguageHide = ($codeExerciseEnabled ?? false) ? '' : 'hide';
$codeLanguageDisabled = ($codeExerciseEnabled ?? false) ? '' : 'disabled';
$codeLanguageOptions = [];
foreach ($CODE_EXERCISE_LANGUAGES as $value => $opts) {
$codeLanguageOptions[$value] = $opts['name'];
}
$codeLanguageSelect = selection($codeLanguageOptions, 'code_language', $codeLanguage ?? 'javascript', '');

$tool_content .= "<div id='codeExerciseWrapper' class='row form-group ".(($answerType != FREE_TEXT) ? "hide": "")." mt-4'>
<div class='col-12'>
<div class='checkbox'>
<label class='label-container' aria-label='$langSettingSelect'>
<input type='checkbox' name='code_exercise' value='1' id='codeExerciseCheck' $codeExerciseChecked ".(($answerType != FREE_TEXT) ? "disabled" : "").">
<span class='checkmark'></span>$langCodeExercise
</label>
</div>
</div>
</div>";

$tool_content .= "<div id='codeLanguageWrapper' class='row form-group $codeLanguageHide mt-4'>
<label for='code_language' class='col-12 control-label-notes mb-1'>$langCodeExerciseLang</label>
<div class='col-12'>
$codeLanguageSelect
</div>
</div>";

if (!$okPicture) {
$tool_content .= "
<div class='row form-group mt-4'>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"h5p-standalone": "^3.8.0",
"mathjax": "4",
"recordrtc": "^5.6.2",
"video.js": "^8.19"
"video.js": "^8.19",
"codemirror": "^5.65.20"
},
"scripts": {
"postinstall": "php ./js/build/build.php"
Expand Down