PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.7.2
Tutor LMS – eLearning and online course solution v3.7.2
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / QuizBuilder.php
tutor / classes Last commit date
Addons.php 11 months ago Admin.php 11 months ago Ajax.php 1 year ago Announcements.php 1 year ago Assets.php 11 months ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 11 months ago Container.php 11 months ago Course.php 10 months ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 10 months ago Course_Settings_Tabs.php 1 year ago Course_Widget.php 1 year ago Custom_Validation.php 3 years ago Dashboard.php 1 year ago Earnings.php 1 year ago FormHandler.php 2 years ago Frontend.php 1 year ago Gutenberg.php 1 year ago Icon.php 10 months ago Input.php 1 year ago Instructor.php 1 year ago Instructors_List.php 11 months ago Lesson.php 10 months ago Options_V2.php 11 months ago Permalink.php 2 years ago Post_types.php 1 year ago Private_Course_Access.php 1 year ago Q_And_A.php 10 months ago Question_Answers_List.php 11 months ago Quiz.php 10 months ago QuizBuilder.php 11 months ago Quiz_Attempts_List.php 11 months ago RestAPI.php 2 years ago Reviews.php 11 months ago Rewrite_Rules.php 2 years ago Shortcode.php 1 year ago Singleton.php 1 year ago Student.php 1 year ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 11 months ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 1 year ago Tutor.php 10 months ago TutorEDD.php 1 year ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 year ago Upgrader.php 10 months ago User.php 1 year ago Utils.php 10 months ago Video_Stream.php 3 years ago WhatsNew.php 2 years ago Withdraw.php 1 year ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 1 year ago
QuizBuilder.php
436 lines
1 <?php
2 /**
3 * Quiz Builder
4 *
5 * @package Tutor\Classes
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 3.0.0
9 */
10
11 namespace TUTOR;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 use Tutor\Helpers\HttpHelper;
18 use Tutor\Helpers\QueryHelper;
19 use Tutor\Helpers\ValidationHelper;
20 use Tutor\Models\QuizModel;
21 use Tutor\Traits\JsonResponse;
22
23 /**
24 * Class QuizBuilder
25 *
26 * @since 1.0.0
27 */
28 class QuizBuilder {
29 use JsonResponse;
30
31 const TRACKING_KEY = '_data_status';
32 const FLAG_NEW = 'new';
33 const FLAG_UPDATE = 'update';
34 const FLAG_NO_CHANGE = 'no_change';
35
36 /**
37 * Register hooks and dependencies.
38 *
39 * @param boolean $register_hooks register hooks or not.
40 */
41 public function __construct( $register_hooks = true ) {
42 if ( ! $register_hooks ) {
43 return;
44 }
45
46 add_action( 'wp_ajax_tutor_quiz_builder_save', array( $this, 'ajax_quiz_builder_save' ) );
47 }
48
49
50 /**
51 * Prepare question data.
52 *
53 * @since 3.0.0
54 *
55 * @param int $quiz_id quiz id.
56 * @param array $input question data.
57 *
58 * @return array
59 */
60 public function prepare_question_data( $quiz_id, $input ) {
61 $question_title = Input::sanitize( wp_slash( $input['question_title'] ), '' );
62 $question_description = Input::sanitize( wp_slash( $input['question_description'] ) ?? '', '', Input::TYPE_KSES_POST );
63 $question_type = Input::sanitize( $input['question_type'], '' );
64 $question_mark = Input::sanitize( $input['question_mark'], 1, Input::TYPE_INT );
65 $question_settings = Input::sanitize_array( $input['question_settings'] );
66
67 $data = array(
68 'quiz_id' => $quiz_id,
69 'question_title' => $question_title,
70 'question_description' => $question_description,
71 'question_type' => $question_type,
72 'question_mark' => $question_mark,
73 'question_settings' => maybe_serialize( $question_settings ),
74 );
75
76 return apply_filters( 'tutor_quiz_question_data', $data, $input );
77 }
78
79 /**
80 * Prepare answer data.
81 *
82 * @param int $question_id question id.
83 * @param string $question_type question type.
84 * @param array $input answer data.
85 *
86 * @return array
87 */
88 public function prepare_answer_data( $question_id, $question_type, $input ) {
89 $answer_title = Input::sanitize( wp_slash( $input['answer_title'] ) ?? '', '' );
90 $is_correct = Input::sanitize( $input['is_correct'] ?? 0, 0, Input::TYPE_INT );
91 $image_id = Input::sanitize( $input['image_id'] ?? null );
92 $answer_two_gap_match = Input::sanitize( $input['answer_two_gap_match'] ?? '' );
93 $answer_view_format = Input::sanitize( $input['answer_view_format'] ?? '' );
94 $answer_settings = null;
95
96 $answer_data = array(
97 'belongs_question_id' => $question_id,
98 'belongs_question_type' => $question_type,
99 'answer_title' => $answer_title,
100 'is_correct' => $is_correct,
101 'image_id' => $image_id,
102 'answer_two_gap_match' => $answer_two_gap_match,
103 'answer_view_format' => $answer_view_format,
104 'answer_settings' => $answer_settings,
105 );
106
107 return $answer_data;
108 }
109
110 /**
111 * Save question answers.
112 *
113 * @since 3.7.0
114 *
115 * @param int $question_id question id.
116 * @param string $question_type question type.
117 * @param array $question_answers question answers.
118 *
119 * @return void
120 */
121 public function save_question_answers( $question_id, $question_type, $question_answers ) {
122 global $wpdb;
123 $answers_table = $wpdb->prefix . 'tutor_quiz_question_answers';
124
125 $answer_order = 0;
126 foreach ( $question_answers as $answer ) {
127 $data_status = isset( $answer[ self::TRACKING_KEY ] ) ? $answer[ self::TRACKING_KEY ] : self::FLAG_NO_CHANGE;
128 $answer_data = $this->prepare_answer_data( $question_id, $question_type, $answer );
129
130 // New answer.
131 if ( self::FLAG_NEW === $data_status ) {
132 $wpdb->insert( $answers_table, $answer_data );
133 $answer_id = $wpdb->insert_id;
134 }
135
136 // Update answer.
137 if ( self::FLAG_UPDATE === $data_status ) {
138 $answer_id = $answer['answer_id'];
139 $wpdb->update(
140 $answers_table,
141 $answer_data,
142 array( 'answer_id' => $answer_id )
143 );
144 }
145
146 if ( self::FLAG_NO_CHANGE === $data_status ) {
147 $answer_id = $answer['answer_id'];
148 }
149
150 // Save sort order.
151 $answer_order++;
152 $wpdb->update(
153 $answers_table,
154 array( 'answer_order' => $answer_order ),
155 array( 'answer_id' => $answer_id )
156 );
157 }
158 }
159
160 /**
161 * Save quiz questions.
162 *
163 * @since 3.0.0
164 *
165 * @param int $quiz_id quiz id.
166 * @param array $questions questions data.
167 *
168 * @return void
169 */
170 public function save_questions( $quiz_id, $questions ) {
171 global $wpdb;
172 $questions_table = $wpdb->prefix . 'tutor_quiz_questions';
173
174 $question_order = 0;
175 foreach ( $questions as $question ) {
176 $data_status = isset( $question[ self::TRACKING_KEY ] ) ? $question[ self::TRACKING_KEY ] : self::FLAG_NO_CHANGE;
177 $question_order++;
178 if ( isset( $question['is_cb_question'], $question['cb_action'] ) && 'link' === $question['cb_action'] ) {
179 $question['question_order'] = $question_order;
180 do_action( 'tutor_content_bank_question_linked_to_quiz', $quiz_id, (object) $question );
181 continue;
182 }
183
184 $question_type = Input::sanitize( $question['question_type'] );
185 $question_data = $this->prepare_question_data( $quiz_id, $question );
186 $question_answers = isset( $question['question_answers'] ) ? $question['question_answers'] : array();
187
188 // New question.
189 if ( self::FLAG_NEW === $data_status ) {
190 $wpdb->insert( $questions_table, $question_data );
191 $question_id = $wpdb->insert_id;
192
193 if ( isset( $question['is_cb_question'] ) ) {
194 $question['question_order'] = $question_order;
195 $question['new_question_id'] = $question_id;
196 do_action( 'tutor_content_bank_question_added_to_quiz', $quiz_id, (object) $question );
197 }
198 }
199
200 // Update question.
201 if ( self::FLAG_UPDATE === $data_status ) {
202 $question_id = (int) $question['question_id'];
203 $wpdb->update(
204 $questions_table,
205 $question_data,
206 array( 'question_id' => $question_id )
207 );
208 }
209
210 if ( self::FLAG_NO_CHANGE === $data_status ) {
211 $question_id = $question['question_id'];
212 }
213
214 // Save sort order.
215 $wpdb->update(
216 $questions_table,
217 array( 'question_order' => $question_order ),
218 array( 'question_id' => $question_id )
219 );
220
221 // Save question's answers.
222 $this->save_question_answers( $question_id, $question_type, $question_answers );
223 }
224 }
225
226 /**
227 * Validate payload.
228 *
229 * @since 3.0.0
230 *
231 * @param array $payload payload.
232 *
233 * @return object consist success, errors.
234 */
235 public function validate_payload( $payload ) {
236 $errors = array();
237 $success = true;
238
239 if ( ! is_array( $payload ) ) {
240 $success = false;
241 $errors['payload'] = __( 'Invalid payload', 'tutor' );
242 }
243
244 $rules = array(
245 'post_title' => 'required',
246 'quiz_option' => 'required|is_array',
247 'questions' => 'required|is_array',
248 );
249
250 $validation = ValidationHelper::validate(
251 $rules,
252 $payload
253 );
254
255 if ( ! $validation->success ) {
256 $success = false;
257 $errors = array_merge( $errors, $validation->errors );
258 }
259
260 foreach ( $payload['questions'] as $question ) {
261 if ( ! isset( $question[ self::TRACKING_KEY ] ) ) {
262 $success = false;
263 // translators: %s is the tracking key required for each question.
264 $errors[ self::TRACKING_KEY ][] = sprintf( __( '%s is required for each question', 'tutor' ), self::TRACKING_KEY );
265 break;
266 }
267
268 if ( ! in_array( $question[ self::TRACKING_KEY ], array( self::FLAG_NEW, self::FLAG_UPDATE, self::FLAG_NO_CHANGE ), true ) ) {
269 $success = false;
270 // translators: %s is the tracking key for which the value is invalid.
271 $errors[ self::TRACKING_KEY ][] = sprintf( __( 'Invalid value for %s', 'tutor' ), self::TRACKING_KEY );
272 break;
273 }
274
275 if ( ! isset( $question['question_settings'] ) || ! is_array( $question['question_settings'] ) ) {
276 $success = false;
277 $errors['question_settings'][] = __( 'Question settings is required with array data', 'tutor' );
278 break;
279 }
280 }
281
282 return (object) array(
283 'success' => $success,
284 'errors' => $errors,
285 );
286 }
287
288 /**
289 * Handle delete questions and answers.
290 *
291 * @since 3.0.0
292 *
293 * @param array $deleted_question_ids question ids.
294 * @param array $deleted_answer_ids answer ids.
295 *
296 * @return void
297 */
298 public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array() ) {
299 global $wpdb;
300 $deleted_question_ids = array_filter( $deleted_question_ids, 'is_numeric' );
301 $deleted_answer_ids = array_filter( $deleted_answer_ids, 'is_numeric' );
302
303 if ( count( $deleted_question_ids ) ) {
304 $id_str = QueryHelper::prepare_in_clause( $deleted_question_ids );
305 //phpcs:ignore -- sanitized $id_str.
306 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE content_id IS NULL AND question_id IN (" . $id_str . ')' );
307 do_action( 'tutor_deleted_quiz_question_ids', $deleted_question_ids );
308 }
309
310 if ( count( $deleted_answer_ids ) ) {
311 $id_str = QueryHelper::prepare_in_clause( $deleted_answer_ids );
312 //phpcs:ignore -- sanitized $id_str.
313 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN (" . $id_str . ')' );
314 }
315 }
316
317 /**
318 * Create or update quiz.
319 *
320 * @since 3.0.0
321 *
322 * @param int $topic_id topic id.
323 * @param array $payload payload.
324 *
325 * @return object consist success, errors.
326 */
327 public function save_quiz( $topic_id, $payload ) {
328 $success = true;
329 $data = null;
330 $errors = array();
331
332 $validation = $this->validate_payload( $payload );
333
334 if ( ! $validation->success ) {
335 return (object) array(
336 'success' => false,
337 'errors' => $validation->errors,
338 );
339 }
340
341 $is_update = isset( $payload['ID'] );
342 $quiz_id = $is_update ? $payload['ID'] : null;
343 $questions = isset( $payload['questions'] ) ? $payload['questions'] : array();
344
345 $menu_order = (int) ( isset( $payload['menu_order'] )
346 ? $payload['menu_order']
347 : tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id ) );
348
349 $quiz_data = array(
350 'post_type' => tutor()->quiz_post_type,
351 'post_title' => Input::sanitize( wp_slash( $payload['post_title'] ?? '' ) ),
352 'post_content' => Input::sanitize( wp_slash( $payload['post_content'] ?? '' ) ),
353 'post_status' => 'publish',
354 'post_author' => get_current_user_id(),
355 'post_parent' => $topic_id,
356 'menu_order' => $menu_order,
357 );
358
359 global $wpdb;
360 $wpdb->query( 'START TRANSACTION' );
361
362 try {
363 // Add or update the quiz.
364 if ( $is_update ) {
365 $quiz_data['ID'] = $quiz_id;
366 }
367
368 $quiz_id = wp_insert_post( $quiz_data );
369 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
370
371 // Save quiz settings.
372 $quiz_option = Input::sanitize_array( $payload['quiz_option'] ?? array() ); //phpcs:ignore
373 update_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, $quiz_option );
374 do_action( 'tutor_quiz_settings_updated', $quiz_id );
375
376 // Save quiz questions.
377 if ( count( $questions ) ) {
378 $this->save_questions( $quiz_id, $questions );
379 }
380
381 // Delete questions and answers.
382 $deleted_question_ids = Input::post( 'deleted_question_ids', array(), Input::TYPE_ARRAY );
383 $deleted_answer_ids = Input::post( 'deleted_answer_ids', array(), Input::TYPE_ARRAY );
384 $this->handle_delete( $deleted_question_ids, $deleted_answer_ids );
385
386 $wpdb->query( 'COMMIT' );
387
388 $data = $quiz_id;
389
390 } catch ( \Throwable $th ) {
391 $wpdb->query( 'ROLLBACK' );
392
393 $success = false;
394 $errors['500'][] = $th->getMessage();
395 }
396
397 return (object) array(
398 'success' => $success,
399 'data' => $data,
400 'errors' => $errors,
401 );
402 }
403
404 /**
405 * Create or update quiz from new course builder.
406 *
407 * @since 3.0.0
408 *
409 * @return void json response.
410 */
411 public function ajax_quiz_builder_save() {
412 tutor_utils()->check_nonce();
413
414 $payload = $_POST['payload'] ?? array(); //phpcs:ignore
415 if ( is_string( $payload ) ) {
416 $payload = json_decode( wp_unslash( $payload ), true );
417 }
418
419 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
420 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
421 $course_cls = new Course( false );
422
423 $course_cls->check_access( $course_id );
424
425 $result = $this->save_quiz( $topic_id, wp_slash( $payload ) );
426 if ( $result->success ) {
427 $quiz_id = $result->data;
428 $quiz_details = QuizModel::get_quiz_details( $quiz_id );
429 $this->json_response( __( 'Quiz saved successfully', 'tutor' ), $quiz_details );
430 } else {
431 $this->json_response( __( 'Error', 'tutor' ), $result->errors, HttpHelper::STATUS_BAD_REQUEST );
432 }
433
434 }
435 }
436