PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.13
Tutor LMS – eLearning and online course solution v3.9.13
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 2 months ago Ajax.php 9 months ago Announcements.php 1 year ago Assets.php 2 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 2 months ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 5 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 9 months ago FormHandler.php 2 years ago Frontend.php 1 year ago Gutenberg.php 1 year ago Icon.php 8 months ago Input.php 1 year ago Instructor.php 2 months ago Instructors_List.php 2 months ago Lesson.php 2 weeks ago Options_V2.php 7 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 2 weeks ago QuizBuilder.php 2 weeks ago Quiz_Attempts_List.php 9 months ago RestAPI.php 2 years ago Reviews.php 9 months ago Rewrite_Rules.php 2 years ago Shortcode.php 9 months ago Singleton.php 1 year ago Student.php 2 months ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 9 months ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 3 weeks ago Tutor.php 2 months ago TutorEDD.php 1 year ago Tutor_Base.php 2 years ago Tutor_Setup.php 8 months ago Upgrader.php 9 months ago User.php 4 months ago Utils.php 3 weeks ago Video_Stream.php 3 years ago WhatsNew.php 9 months ago Withdraw.php 1 year ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 7 months ago
QuizBuilder.php
448 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 if ( isset( $payload['ID'] ) && is_numeric( $payload['ID'] ) ) {
261 if ( ! current_user_can( 'edit_post', $payload['ID'] ) ) {
262 $success = false;
263 $errors['permission'][] = __( 'You do not have permission to edit this quiz', 'tutor' );
264 } else {
265 $quiz = get_post( $payload['ID'] );
266 if ( ! $quiz || tutor()->quiz_post_type !== $quiz->post_type ) {
267 $success = false;
268 $errors['ID'][] = __( 'Invalid quiz id provided', 'tutor' );
269 }
270 }
271 }
272
273 foreach ( $payload['questions'] as $question ) {
274 if ( ! isset( $question[ self::TRACKING_KEY ] ) ) {
275 $success = false;
276 // translators: %s is the tracking key required for each question.
277 $errors[ self::TRACKING_KEY ][] = sprintf( __( '%s is required for each question', 'tutor' ), self::TRACKING_KEY );
278 break;
279 }
280
281 if ( ! in_array( $question[ self::TRACKING_KEY ], array( self::FLAG_NEW, self::FLAG_UPDATE, self::FLAG_NO_CHANGE ), true ) ) {
282 $success = false;
283 // translators: %s is the tracking key for which the value is invalid.
284 $errors[ self::TRACKING_KEY ][] = sprintf( __( 'Invalid value for %s', 'tutor' ), self::TRACKING_KEY );
285 break;
286 }
287
288 if ( ! isset( $question['question_settings'] ) || ! is_array( $question['question_settings'] ) ) {
289 $success = false;
290 $errors['question_settings'][] = __( 'Question settings is required with array data', 'tutor' );
291 break;
292 }
293 }
294
295 return (object) array(
296 'success' => $success,
297 'errors' => $errors,
298 );
299 }
300
301 /**
302 * Handle delete questions and answers.
303 *
304 * @since 3.0.0
305 *
306 * @param array $deleted_question_ids question ids.
307 * @param array $deleted_answer_ids answer ids.
308 *
309 * @return void
310 */
311 public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array() ) {
312 global $wpdb;
313 $deleted_question_ids = array_filter( $deleted_question_ids, 'is_numeric' );
314 $deleted_answer_ids = array_filter( $deleted_answer_ids, 'is_numeric' );
315
316 if ( count( $deleted_question_ids ) ) {
317 $in_clause = QueryHelper::prepare_in_clause( $deleted_question_ids );
318 //phpcs:ignore -- sanitized $in_clause.
319 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE content_id IS NULL AND question_id IN ({$in_clause})" ) );
320 do_action( 'tutor_deleted_quiz_question_ids', $deleted_question_ids );
321 }
322
323 if ( count( $deleted_answer_ids ) ) {
324 $in_clause = QueryHelper::prepare_in_clause( $deleted_answer_ids );
325 //phpcs:ignore -- sanitized $in_clause.
326 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN ({$in_clause})" ) );
327 }
328 }
329
330 /**
331 * Create or update quiz.
332 *
333 * @since 3.0.0
334 *
335 * @param int $topic_id topic id.
336 * @param array $payload payload.
337 *
338 * @return object consist success, errors.
339 */
340 public function save_quiz( $topic_id, $payload ) {
341 $success = true;
342 $data = null;
343 $errors = array();
344
345 $validation = $this->validate_payload( $payload );
346
347 if ( ! $validation->success ) {
348 return (object) array(
349 'success' => false,
350 'errors' => $validation->errors,
351 );
352 }
353
354 $is_update = isset( $payload['ID'] );
355 $quiz_id = $is_update ? $payload['ID'] : null;
356 $questions = isset( $payload['questions'] ) ? $payload['questions'] : array();
357
358 $menu_order = (int) ( isset( $payload['menu_order'] )
359 ? $payload['menu_order']
360 : tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id ) );
361
362 $quiz_data = array(
363 'post_type' => tutor()->quiz_post_type,
364 'post_title' => Input::sanitize( wp_slash( $payload['post_title'] ?? '' ) ),
365 'post_content' => Input::sanitize( wp_slash( $payload['post_content'] ?? '' ) ),
366 'post_status' => 'publish',
367 'post_author' => get_current_user_id(),
368 'post_parent' => $topic_id,
369 'menu_order' => $menu_order,
370 );
371
372 global $wpdb;
373 $wpdb->query( 'START TRANSACTION' );
374
375 try {
376 // Add or update the quiz.
377 if ( $is_update ) {
378 $quiz_data['ID'] = $quiz_id;
379 }
380
381 $quiz_id = wp_insert_post( $quiz_data );
382 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
383
384 // Save quiz settings.
385 $quiz_option = Input::sanitize_array( $payload['quiz_option'] ?? array() ); //phpcs:ignore
386 update_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, $quiz_option );
387 do_action( 'tutor_quiz_settings_updated', $quiz_id );
388
389 // Save quiz questions.
390 if ( count( $questions ) ) {
391 $this->save_questions( $quiz_id, $questions );
392 }
393
394 // Delete questions and answers.
395 $deleted_question_ids = Input::post( 'deleted_question_ids', array(), Input::TYPE_ARRAY );
396 $deleted_answer_ids = Input::post( 'deleted_answer_ids', array(), Input::TYPE_ARRAY );
397 $this->handle_delete( $deleted_question_ids, $deleted_answer_ids );
398
399 $wpdb->query( 'COMMIT' );
400
401 $data = $quiz_id;
402
403 } catch ( \Throwable $th ) {
404 $wpdb->query( 'ROLLBACK' );
405
406 $success = false;
407 $errors['500'][] = $th->getMessage();
408 }
409
410 return (object) array(
411 'success' => $success,
412 'data' => $data,
413 'errors' => $errors,
414 );
415 }
416
417 /**
418 * Create or update quiz from new course builder.
419 *
420 * @since 3.0.0
421 *
422 * @return void json response.
423 */
424 public function ajax_quiz_builder_save() {
425 tutor_utils()->check_nonce();
426
427 $payload = $_POST['payload'] ?? array(); //phpcs:ignore
428 if ( is_string( $payload ) ) {
429 $payload = json_decode( wp_unslash( $payload ), true );
430 }
431
432 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
433 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
434 $course_cls = new Course( false );
435
436 $course_cls->check_access( $course_id );
437
438 $result = $this->save_quiz( $topic_id, wp_slash( $payload ) );
439 if ( $result->success ) {
440 $quiz_id = $result->data;
441 $quiz_details = QuizModel::get_quiz_details( $quiz_id );
442 $this->json_response( __( 'Quiz saved successfully', 'tutor' ), $quiz_details );
443 } else {
444 $this->json_response( __( 'Error', 'tutor' ), $result->errors, HttpHelper::STATUS_BAD_REQUEST );
445 }
446 }
447 }
448