Skip to content

Commit b1f0e79

Browse files
Merge pull request #7388 from christianbeeznest/GH-7378
Exercise: Fix multiple answer True/False radios visibility - refs #7378
2 parents b7ab17b + de6eec7 commit b1f0e79

2 files changed

Lines changed: 129 additions & 100 deletions

File tree

public/main/exercise/exercise.class.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3712,7 +3712,7 @@ function ($answerIdKey) use ($objAnswerTmp) {
37123712
$questionScore += $true_score;
37133713
} else {
37143714
if (isset($quiz_question_options[$studentChoice])
3715-
&& in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3715+
&& in_array($quiz_question_options[$studentChoice]['title'], ["Don't know", 'DoubtScore'])
37163716
) {
37173717
$questionScore += $doubt_score;
37183718
} else {
@@ -6474,7 +6474,7 @@ public function send_mail_notification_for_exam(
64746474
if (!empty($sessionInfo)) {
64756475
$sessionData = '<tr>'
64766476
.'<td>'.get_lang('Session name').'</td>'
6477-
.'<td>'.$sessionInfo['name'].'</td>'
6477+
.'<td>'.$sessionInfo['title'].'</td>'
64786478
.'</tr>';
64796479
}
64806480
}
@@ -10883,7 +10883,7 @@ private function sendNotificationForOpenQuestions(
1088310883
if (!empty($sessionInfo)) {
1088410884
$sessionData = '<tr>'
1088510885
.'<td><em>'.get_lang('Session name').'</em></td>'
10886-
.'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10886+
.'<td>&nbsp;<b>'.$sessionInfo['title'].'</b></td>'
1088710887
.'</tr>';
1088810888
}
1088910889
}

public/main/exercise/multiple_answer_true_false.class.php

Lines changed: 126 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function __construct()
2929

3030
public function createAnswersForm($form)
3131
{
32-
$nb_answers = isset($_POST['nb_answers']) ? $_POST['nb_answers'] : 4;
32+
$nb_answers = (int) ($_POST['nb_answers'] ?? 4);
3333
// The previous default value was 2. See task #1759.
3434
$nb_answers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
3535

@@ -38,22 +38,24 @@ public function createAnswersForm($form)
3838
$renderer = &$form->defaultRenderer();
3939
$defaults = [];
4040

41-
$html = '<table class="table table-striped table-hover">';
42-
$html .= '<thead>';
41+
$html = '';
42+
$html .= '<div class="overflow-x-auto">';
43+
$html .= '<table class="min-w-full text-sm border border-gray-200 rounded-lg overflow-hidden table table-striped table-hover">';
44+
$html .= '<thead class="bg-gray-20">';
4345
$html .= '<tr>';
44-
$html .= '<th>'.get_lang('').'</th>';
45-
$html .= '<th>'.get_lang('True').'</th>';
46-
$html .= '<th>'.get_lang('False').'</th>';
47-
$html .= '<th>'.get_lang('Answer').'</th>';
46+
$html .= '<th class="px-3 py-2 text-left font-semibold text-gray-700">'.get_lang('').'</th>';
47+
$html .= '<th class="px-3 py-2 text-center font-semibold text-gray-700">'.get_lang('True').'</th>';
48+
$html .= '<th class="px-3 py-2 text-center font-semibold text-gray-700">'.get_lang('False').'</th>';
49+
$html .= '<th class="px-3 py-2 text-left font-semibold text-gray-700">'.get_lang('Answer').'</th>';
4850

49-
// show column comment when feedback is enable
51+
// Show column comment when feedback is enabled
5052
if (EXERCISE_FEEDBACK_TYPE_EXAM != $obj_ex->getFeedbackType()) {
51-
$html .= '<th>'.get_lang('Comment').'</th>';
53+
$html .= '<th class="px-3 py-2 text-left font-semibold text-gray-700">'.get_lang('Comment').'</th>';
5254
}
5355

5456
$html .= '</tr>';
5557
$html .= '</thead>';
56-
$html .= '<tbody>';
58+
$html .= '<tbody class="divide-y divide-gray-200 bg-white">';
5759

5860
$form->addHeader(get_lang('Answers'));
5961
$form->addHtml($html);
@@ -75,26 +77,43 @@ public function createAnswersForm($form)
7577
echo Display::return_message(get_lang('You have to create at least one answer'));
7678
}
7779

78-
// Can be more options
80+
// Can be more options in the future
7981
$optionData = Question::readQuestionOption($this->id, $course_id);
8082

83+
// Safe input attrs (force visibility even if global CSS sets appearance:none)
84+
$choiceInputAttrs = [
85+
'class' => 'h-4 w-4 cursor-pointer',
86+
'style' => 'appearance:auto;-webkit-appearance:auto;',
87+
];
88+
8189
for ($i = 1; $i <= $nb_answers; $i++) {
8290
$form->addHtml('<tr>');
8391

8492
$renderer->setElementTemplate(
85-
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
86-
'correct['.$i.']'
93+
'<td class="px-3 py-2 align-top text-gray-700">'.
94+
'<!-- BEGIN error --><div class="text-xs text-red-600 mb-1">{error}</div><!-- END error -->'.
95+
'{element}</td>',
96+
'counter['.$i.']'
8797
);
98+
99+
// This will generate 2 <td> cells for the 2 radios, matching the True/False columns.
88100
$renderer->setElementTemplate(
89-
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
90-
'counter['.$i.']'
101+
'<td class="px-3 py-2 align-top text-center">'.
102+
'<!-- BEGIN error --><div class="text-xs text-red-600 mb-1">{error}</div><!-- END error -->'.
103+
'{element}</td>',
104+
'correct['.$i.']'
91105
);
106+
92107
$renderer->setElementTemplate(
93-
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
108+
'<td class="px-3 py-2 align-top">'.
109+
'<!-- BEGIN error --><div class="text-xs text-red-600 mb-1">{error}</div><!-- END error -->'.
110+
'{element}</td>',
94111
'answer['.$i.']'
95112
);
96113
$renderer->setElementTemplate(
97-
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
114+
'<td class="px-3 py-2 align-top">'.
115+
'<!-- BEGIN error --><div class="text-xs text-red-600 mb-1">{error}</div><!-- END error -->'.
116+
'{element}</td>',
98117
'comment['.$i.']'
99118
);
100119

@@ -110,28 +129,32 @@ public function createAnswersForm($form)
110129
if (is_object($answer)) {
111130
$defaults['answer['.$i.']'] = $answer->answer[$i] ?? null;
112131
$defaults['comment['.$i.']'] = $answer->comment[$i] ?? null;
113-
$correct = $answer->correct[$i] ?? false;
132+
$correct = $answer->correct[$i] ?? '';
114133
$defaults['correct['.$i.']'] = $correct;
115134

116-
$j = 1;
135+
// Render True/False radios based on existing option IDs (iid)
136+
// Only take the first 2 options (True/False). The 3rd option is "Don't know" score, not a correct flag.
117137
if (!empty($optionData)) {
118-
foreach ($optionData as $id => $data) {
119-
$rdoCorrect = $form->addElement('radio', 'correct['.$i.']', null, null, $id);
120-
121-
if (isset($_POST['correct']) && isset($_POST['correct'][$i]) && $j == $_POST['correct'][$i]) {
122-
$rdoCorrect->setValue(Security::remove_XSS($_POST['correct'][$i]));
123-
} else {
124-
$rdoCorrect->setValue($data['iid']);
138+
$count = 0;
139+
foreach ($optionData as $data) {
140+
$value = (int) ($data['iid'] ?? 0);
141+
if ($value > 0) {
142+
$form->addElement('radio', 'correct['.$i.']', null, null, $value, $choiceInputAttrs);
143+
$count++;
125144
}
126-
$j++;
127-
if (3 == $j) {
145+
if ($count >= 2) {
128146
break;
129147
}
130148
}
149+
} else {
150+
// Fallback
151+
$form->addElement('radio', 'correct['.$i.']', null, null, 1, $choiceInputAttrs);
152+
$form->addElement('radio', 'correct['.$i.']', null, null, 2, $choiceInputAttrs);
131153
}
132154
} else {
133-
$form->addElement('radio', 'correct['.$i.']', null, null, 1);
134-
$form->addElement('radio', 'correct['.$i.']', null, null, 2);
155+
// New question: positions 1/2 are mapped to the real option iids on save
156+
$form->addElement('radio', 'correct['.$i.']', null, null, 1, $choiceInputAttrs);
157+
$form->addElement('radio', 'correct['.$i.']', null, null, 2, $choiceInputAttrs);
135158

136159
$defaults['answer['.$i.']'] = '';
137160
$defaults['comment['.$i.']'] = '';
@@ -146,10 +169,11 @@ public function createAnswersForm($form)
146169
['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100']
147170
);
148171

149-
if (isset($_POST['answer']) && isset($_POST['answer'][$i])) {
172+
if (isset($_POST['answer'][$i])) {
150173
$form->getElement("answer[$i]")->setValue(Security::remove_XSS($_POST['answer'][$i]));
151174
}
152-
// show comment when feedback is enable
175+
176+
// Show comment when feedback is enabled
153177
if (EXERCISE_FEEDBACK_TYPE_EXAM != $obj_ex->getFeedbackType()) {
154178
$form->addHtmlEditor(
155179
'comment['.$i.']',
@@ -163,72 +187,70 @@ public function createAnswersForm($form)
163187
]
164188
);
165189
$form->applyFilter("comment[$i]", 'attr_on_filter');
166-
if (isset($_POST['comment']) && isset($_POST['comment'][$i])) {
190+
if (isset($_POST['comment'][$i])) {
167191
$form->getElement("comment[$i]")->setValue(Security::remove_XSS($_POST['comment'][$i]));
168192
}
169193
}
170194

171195
$form->addHtml('</tr>');
172196
}
173197

174-
$form->addHtml('</tbody></table>');
175-
176-
$correctInputTemplate = '<div class="form-group">';
177-
$correctInputTemplate .= '<label class="col-sm-2 control-label">';
178-
$correctInputTemplate .= '<span class="form_required">*</span>'.get_lang('Score');
179-
$correctInputTemplate .= '</label>';
180-
$correctInputTemplate .= '<div class="col-sm-8">';
181-
$correctInputTemplate .= '<table>';
182-
$correctInputTemplate .= '<tr>';
183-
$correctInputTemplate .= '<td>';
184-
$correctInputTemplate .= get_lang('Correct').'{element}';
185-
$correctInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
186-
$correctInputTemplate .= '</td>';
187-
188-
$wrongInputTemplate = '<td>';
189-
$wrongInputTemplate .= get_lang('Wrong').'{element}';
190-
$wrongInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
191-
$wrongInputTemplate .= '</td>';
192-
193-
$doubtScoreInputTemplate = '<td>'.get_lang('Don\'t know').'<br>{element}';
194-
$doubtScoreInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
195-
$doubtScoreInputTemplate .= '</td>';
196-
$doubtScoreInputTemplate .= '</tr>';
197-
$doubtScoreInputTemplate .= '</table>';
198-
$doubtScoreInputTemplate .= '</div>';
199-
$doubtScoreInputTemplate .= '</div>';
200-
201-
$renderer->setElementTemplate($correctInputTemplate, 'option[1]');
202-
$renderer->setElementTemplate($wrongInputTemplate, 'option[2]');
203-
$renderer->setElementTemplate($doubtScoreInputTemplate, 'option[3]');
198+
$form->addHtml('</tbody></table></div>');
199+
200+
// Score block
201+
$scoreWrapperStart = '<div class="mt-6 border border-gray-200 rounded-lg bg-white p-4 mb-4">';
202+
$scoreWrapperStart .= '<div class="text-sm font-semibold text-gray-700 mb-3">';
203+
$scoreWrapperStart .= '<span class="text-red-600">*</span> '.get_lang('Score');
204+
$scoreWrapperStart .= '</div>';
205+
$scoreWrapperStart .= '<div class="flex flex-wrap gap-6 items-end">';
206+
207+
$scoreWrapperEnd = '</div></div>';
208+
209+
$scoreInputTemplate = function (string $label) {
210+
$tpl = '<div class="min-w-[160px]">';
211+
$tpl .= '<div class="text-xs text-gray-600 mb-1">'.$label.'</div>';
212+
$tpl .= '{element}';
213+
$tpl .= '<!-- BEGIN error --><div class="text-xs text-red-600 mt-1">{error}</div><!-- END error -->';
214+
$tpl .= '</div>';
215+
216+
return $tpl;
217+
};
218+
219+
$renderer->setElementTemplate($scoreWrapperStart.$scoreInputTemplate(get_lang('Correct')), 'option[1]');
220+
$renderer->setElementTemplate($scoreInputTemplate(get_lang('Wrong')), 'option[2]');
221+
$renderer->setElementTemplate($scoreInputTemplate(get_lang('Don\'t know')).$scoreWrapperEnd, 'option[3]');
222+
223+
$scoreInputAttrs = [
224+
'class' => 'w-24 rounded-md border border-gray-300 px-2 py-1 text-sm',
225+
];
204226

205227
// 3 scores
206-
$txtOption1 = $form->addElement('text', 'option[1]', get_lang('Correct'), ['class' => 'span1', 'value' => '1']);
207-
$txtOption2 = $form->addElement('text', 'option[2]', get_lang('Wrong'), ['class' => 'span1', 'value' => '-0.5']);
208-
$txtOption3 = $form->addElement('text', 'option[3]', get_lang('Don\'t know'), ['class' => 'span1', 'value' => '0']);
228+
$txtOption1 = $form->addElement('text', 'option[1]', get_lang('Correct'), array_merge($scoreInputAttrs, ['value' => '1']));
229+
$txtOption2 = $form->addElement('text', 'option[2]', get_lang('Wrong'), array_merge($scoreInputAttrs, ['value' => '-0.5']));
230+
$txtOption3 = $form->addElement('text', 'option[3]', get_lang('Don\'t know'), array_merge($scoreInputAttrs, ['value' => '0']));
209231

210232
$form->addRule('option[1]', get_lang('Required field'), 'required');
211233
$form->addRule('option[2]', get_lang('Required field'), 'required');
212234
$form->addRule('option[3]', get_lang('Required field'), 'required');
213235

214236
$form->addElement('hidden', 'options_count', 3);
215237

216-
// Extra values True, false, Dont known
238+
// Extra values True, false, Dont know
217239
if (!empty($this->extra)) {
218240
$scores = explode(':', $this->extra);
219-
220241
if (!empty($scores)) {
221-
$txtOption1->setValue($scores[0]);
222-
$txtOption2->setValue($scores[1]);
223-
$txtOption3->setValue($scores[2]);
242+
$txtOption1->setValue($scores[0] ?? '1');
243+
$txtOption2->setValue($scores[1] ?? '-0.5');
244+
$txtOption3->setValue($scores[2] ?? '0');
224245
}
225246
}
226247

227248
global $text;
228249
if (true == $obj_ex->edit_exercise_in_lp ||
229250
(empty($this->exerciseList) && empty($obj_ex->id))
230251
) {
231-
// setting the save button here and not in the question class.php
252+
// Setting the save button here and not in the question class.php
253+
$buttonGroup = [];
232254
$buttonGroup[] = $form->addButtonDelete(get_lang('Remove answer option'), 'lessAnswers', true);
233255
$buttonGroup[] = $form->addButtonCreate(get_lang('Add answer option'), 'moreAnswers', true);
234256
$buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
@@ -246,58 +268,65 @@ public function processAnswersCreation($form, $exercise)
246268
{
247269
$questionWeighting = 0;
248270
$objAnswer = new Answer($this->id);
249-
$nb_answers = $form->getSubmitValue('nb_answers');
271+
$nb_answers = (int) $form->getSubmitValue('nb_answers');
250272
$course_id = api_get_course_int_id();
251273
$repo = Container::getQuestionRepository();
274+
252275
/** @var CQuizQuestion $question */
253276
$question = $repo->find($this->id);
254-
$options = $question->getOptions();
255-
$em = Database::getManager();
277+
$optionsCollection = $question->getOptions();
278+
$isFirstCreation = $optionsCollection->isEmpty();
256279

257-
if (!$options->isEmpty()) {
258-
foreach ($options as $optionData) {
259-
$optionData->setTitle($optionData>getTitle());
260-
}
261-
} else {
280+
// Ensure default options exist (True, False, DoubtScore)
281+
if ($isFirstCreation) {
262282
for ($i = 1; $i <= 3; $i++) {
263283
Question::saveQuestionOption($question, $this->options[$i], $i);
264284
}
265285
}
266286

267-
/* Getting quiz_question_options (true, false, doubt) because
268-
it's possible that there are more options in the future */
287+
/*
288+
* Getting quiz_question_options (true, false, doubt) because
289+
* it's possible that there are more options in the future.
290+
*/
269291
$new_options = Question::readQuestionOption($this->id, $course_id);
270292
$sortedByPosition = [];
271293
foreach ($new_options as $item) {
272-
$sortedByPosition[$item['position']] = $item;
294+
$sortedByPosition[(int) $item['position']] = $item;
273295
}
274296

275-
/* Saving quiz_question.extra values that has the correct scores of
276-
the true, false, doubt options registered in this format
277-
XX:YY:ZZZ where XX is a float score value.*/
297+
/*
298+
* Saving quiz_question.extra values that has the correct scores of
299+
* the true, false, doubt options registered in this format:
300+
* XX:YY:ZZZ
301+
*/
278302
$extra_values = [];
279303
for ($i = 1; $i <= 3; $i++) {
280-
$score = trim($form->getSubmitValue('option['.$i.']'));
304+
$score = trim((string) $form->getSubmitValue('option['.$i.']'));
281305
$extra_values[] = $score;
282306
}
283307
$this->setExtra(implode(':', $extra_values));
284308

285309
for ($i = 1; $i <= $nb_answers; $i++) {
286-
$answer = trim($form->getSubmitValue('answer['.$i.']'));
287-
$comment = trim($form->getSubmitValue('comment['.$i.']'));
288-
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
289-
if (empty($options)) {
290-
//If this is the first time that the question is created when
291-
// change the default values from the form 1 and 2 by the correct "option id" registered
292-
$goodAnswer = isset($sortedByPosition[$goodAnswer]) ? $sortedByPosition[$goodAnswer]['iid'] : '';
310+
$answer = trim((string) $form->getSubmitValue('answer['.$i.']'));
311+
$comment = trim((string) $form->getSubmitValue('comment['.$i.']'));
312+
$goodAnswer = trim((string) $form->getSubmitValue('correct['.$i.']'));
313+
314+
if ($isFirstCreation) {
315+
// First creation: map submitted position (1/2) to the real option iid
316+
$pos = (int) $goodAnswer;
317+
$goodAnswer = isset($sortedByPosition[$pos]) ? (string) $sortedByPosition[$pos]['iid'] : '';
293318
}
294-
$questionWeighting += $extra_values[0]; //By default 0 has the correct answers
319+
320+
// Total question weighting = nb_answers * "Correct" score (option[1])
321+
$questionWeighting += (float) ($extra_values[0] ?? 0);
322+
295323
$objAnswer->createAnswer($answer, $goodAnswer, $comment, '', $i);
296324
}
297325

298-
// saves the answers into the database
326+
// Saves the answers into the database
299327
$objAnswer->save();
300-
// sets the total weighting of the question
328+
329+
// Sets the total weighting of the question
301330
$this->updateWeighting($questionWeighting);
302331
$this->save($exercise);
303332
}

0 commit comments

Comments
 (0)