@@ -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 ('N° ' ).'</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 ('N° ' ).'</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