PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.3.0
Tutor LMS – eLearning and online course solution v3.3.0
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 1 year ago Admin.php 1 year ago Ajax.php 1 year ago Announcements.php 1 year ago Assets.php 1 year ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Course.php 1 year ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 1 year 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 Input.php 1 year ago Instructor.php 1 year ago Instructors_List.php 1 year ago Lesson.php 1 year ago Options_V2.php 1 year ago Permalink.php 2 years ago Post_types.php 1 year ago Private_Course_Access.php 1 year ago Q_And_A.php 1 year ago Question_Answers_List.php 3 years ago Quiz.php 1 year ago QuizBuilder.php 1 year ago Quiz_Attempts_List.php 1 year ago RestAPI.php 2 years ago Reviews.php 3 years 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 3 years ago Taxonomies.php 1 year ago Template.php 1 year ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 1 year ago Tutor.php 1 year ago TutorEDD.php 1 year ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 year ago Upgrader.php 1 year ago User.php 1 year ago Utils.php 1 year ago Video_Stream.php 3 years ago WhatsNew.php 2 years ago Withdraw.php 1 year ago Withdraw_Requests_List.php 1 year ago WooCommerce.php 1 year ago
QuizBuilder.php
405 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 private 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 quiz questions.
112 *
113 * @since 3.0.0
114 *
115 * @param int $quiz_id quiz id.
116 * @param array $questions questions data.
117 *
118 * @return void
119 */
120 public function save_questions( $quiz_id, $questions ) {
121 global $wpdb;
122 $questions_table = $wpdb->prefix . 'tutor_quiz_questions';
123 $answers_table = $wpdb->prefix . 'tutor_quiz_question_answers';
124
125 $question_order = 0;
126 foreach ( $questions as $question ) {
127 $data_status = isset( $question[ self::TRACKING_KEY ] ) ? $question[ self::TRACKING_KEY ] : self::FLAG_NO_CHANGE;
128 $question_type = Input::sanitize( $question['question_type'] );
129 $question_data = $this->prepare_question_data( $quiz_id, $question );
130 $question_answers = isset( $question['question_answers'] ) ? $question['question_answers'] : array();
131
132 // New question.
133 if ( self::FLAG_NEW === $data_status ) {
134 $wpdb->insert( $questions_table, $question_data );
135 $question_id = $wpdb->insert_id;
136 }
137
138 // Update question.
139 if ( self::FLAG_UPDATE === $data_status ) {
140 $question_id = (int) $question['question_id'];
141 $wpdb->update(
142 $questions_table,
143 $question_data,
144 array( 'question_id' => $question_id )
145 );
146 }
147
148 if ( self::FLAG_NO_CHANGE === $data_status ) {
149 $question_id = $question['question_id'];
150 }
151
152 // Save sort order.
153 $question_order++;
154 $wpdb->update(
155 $questions_table,
156 array( 'question_order' => $question_order ),
157 array( 'question_id' => $question_id )
158 );
159
160 // Save question's answers.
161 $answer_order = 0;
162 foreach ( $question_answers as $answer ) {
163 $data_status = isset( $answer[ self::TRACKING_KEY ] ) ? $answer[ self::TRACKING_KEY ] : self::FLAG_NO_CHANGE;
164 $answer_data = $this->prepare_answer_data( $question_id, $question_type, $answer );
165
166 // New answer.
167 if ( self::FLAG_NEW === $data_status ) {
168 $wpdb->insert( $answers_table, $answer_data );
169 $answer_id = $wpdb->insert_id;
170 }
171
172 // Update answer.
173 if ( self::FLAG_UPDATE === $data_status ) {
174 $answer_id = $answer['answer_id'];
175 $wpdb->update(
176 $answers_table,
177 $answer_data,
178 array( 'answer_id' => $answer_id )
179 );
180 }
181
182 if ( self::FLAG_NO_CHANGE === $data_status ) {
183 $answer_id = $answer['answer_id'];
184 }
185
186 // Save sort order.
187 $answer_order++;
188 $wpdb->update(
189 $answers_table,
190 array( 'answer_order' => $answer_order ),
191 array( 'answer_id' => $answer_id )
192 );
193 }
194 }
195 }
196
197 /**
198 * Validate payload.
199 *
200 * @since 3.0.0
201 *
202 * @param array $payload payload.
203 *
204 * @return object consist success, errors.
205 */
206 public function validate_payload( $payload ) {
207 $errors = array();
208 $success = true;
209
210 if ( ! is_array( $payload ) ) {
211 $success = false;
212 $errors['payload'] = __( 'Invalid payload', 'tutor' );
213 }
214
215 $rules = array(
216 'post_title' => 'required',
217 'quiz_option' => 'required|is_array',
218 'questions' => 'required|is_array',
219 );
220
221 $validation = ValidationHelper::validate(
222 $rules,
223 $payload
224 );
225
226 if ( ! $validation->success ) {
227 $success = false;
228 $errors = array_merge( $errors, $validation->errors );
229 }
230
231 foreach ( $payload['questions'] as $question ) {
232 if ( ! isset( $question[ self::TRACKING_KEY ] ) ) {
233 $success = false;
234 $errors[ self::TRACKING_KEY ][] = sprintf( __( '%s is required for each question', 'tutor' ), self::TRACKING_KEY ); //phpcs:ignore
235 break;
236 }
237
238 if ( ! in_array( $question[ self::TRACKING_KEY ], array( self::FLAG_NEW, self::FLAG_UPDATE, self::FLAG_NO_CHANGE ), true ) ) {
239 $success = false;
240 $errors[ self::TRACKING_KEY ][] = sprintf( __( 'Invalid value for %s', 'tutor' ), self::TRACKING_KEY ); //phpcs:ignore
241 break;
242 }
243
244 if ( ! isset( $question['question_settings'] ) || ! is_array( $question['question_settings'] ) ) {
245 $success = false;
246 $errors['question_settings'][] = __( 'Question settings is required with array data', 'tutor' );
247 break;
248 }
249 }
250
251 return (object) array(
252 'success' => $success,
253 'errors' => $errors,
254 );
255 }
256
257 /**
258 * Handle delete questions and answers.
259 *
260 * @since 3.0.0
261 *
262 * @param array $deleted_question_ids question ids.
263 * @param array $deleted_answer_ids answer ids.
264 *
265 * @return void
266 */
267 public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array() ) {
268 global $wpdb;
269 $deleted_question_ids = array_filter( $deleted_question_ids, 'is_numeric' );
270 $deleted_answer_ids = array_filter( $deleted_answer_ids, 'is_numeric' );
271
272 if ( count( $deleted_question_ids ) ) {
273 $id_str = QueryHelper::prepare_in_clause( $deleted_question_ids );
274 //phpcs:ignore -- sanitized $id_str.
275 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE question_id IN (" . $id_str . ')' );
276 do_action( 'tutor_deleted_quiz_question_ids', $deleted_question_ids );
277 }
278
279 if ( count( $deleted_answer_ids ) ) {
280 $id_str = QueryHelper::prepare_in_clause( $deleted_answer_ids );
281 //phpcs:ignore -- sanitized $id_str.
282 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN (" . $id_str . ')' );
283 }
284 }
285
286 /**
287 * Create or update quiz.
288 *
289 * @since 3.0.0
290 *
291 * @param int $topic_id topic id.
292 * @param array $payload payload.
293 *
294 * @return object consist success, errors.
295 */
296 public function save_quiz( $topic_id, $payload ) {
297 $success = true;
298 $data = null;
299 $errors = array();
300
301 $validation = $this->validate_payload( $payload );
302
303 if ( ! $validation->success ) {
304 return (object) array(
305 'success' => false,
306 'errors' => $validation->errors,
307 );
308 }
309
310 $is_update = isset( $payload['ID'] );
311 $quiz_id = $is_update ? $payload['ID'] : null;
312 $questions = isset( $payload['questions'] ) ? $payload['questions'] : array();
313
314 $menu_order = (int) ( isset( $payload['menu_order'] )
315 ? $payload['menu_order']
316 : tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id ) );
317
318 $quiz_data = array(
319 'post_type' => tutor()->quiz_post_type,
320 'post_title' => Input::sanitize( wp_slash( $payload['post_title'] ?? '' ) ),
321 'post_content' => Input::sanitize( wp_slash( $payload['post_content'] ?? '' ) ),
322 'post_status' => 'publish',
323 'post_author' => get_current_user_id(),
324 'post_parent' => $topic_id,
325 'menu_order' => $menu_order,
326 );
327
328 global $wpdb;
329 $wpdb->query( 'START TRANSACTION' );
330
331 try {
332 // Add or update the quiz.
333 if ( $is_update ) {
334 $quiz_data['ID'] = $quiz_id;
335 }
336
337 $quiz_id = wp_insert_post( $quiz_data );
338 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
339
340 // Save quiz settings.
341 $quiz_option = Input::sanitize_array( $payload['quiz_option'] ?? array() ); //phpcs:ignore
342 update_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, $quiz_option );
343 do_action( 'tutor_quiz_settings_updated', $quiz_id );
344
345 // Save quiz questions.
346 if ( count( $questions ) ) {
347 $this->save_questions( $quiz_id, $questions );
348 }
349
350 // Delete questions and answers.
351 $deleted_question_ids = Input::post( 'deleted_question_ids', array(), Input::TYPE_ARRAY );
352 $deleted_answer_ids = Input::post( 'deleted_answer_ids', array(), Input::TYPE_ARRAY );
353 $this->handle_delete( $deleted_question_ids, $deleted_answer_ids );
354
355 $wpdb->query( 'COMMIT' );
356
357 $data = $quiz_id;
358
359 } catch ( \Throwable $th ) {
360 $wpdb->query( 'ROLLBACK' );
361
362 $success = false;
363 $errors['500'][] = $th->getMessage();
364 }
365
366 return (object) array(
367 'success' => $success,
368 'data' => $data,
369 'errors' => $errors,
370 );
371 }
372
373 /**
374 * Create or update quiz from new course builder.
375 *
376 * @since 3.0.0
377 *
378 * @return void json response.
379 */
380 public function ajax_quiz_builder_save() {
381 tutor_utils()->check_nonce();
382
383 $payload = $_POST['payload'] ?? array(); //phpcs:ignore
384 if ( is_string( $payload ) ) {
385 $payload = json_decode( wp_unslash( $payload ), true );
386 }
387
388 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
389 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
390 $course_cls = new Course( false );
391
392 $course_cls->check_access( $course_id );
393
394 $result = $this->save_quiz( $topic_id, wp_slash( $payload ) );
395 if ( $result->success ) {
396 $quiz_id = $result->data;
397 $quiz_details = QuizModel::get_quiz_details( $quiz_id );
398 $this->json_response( __( 'Quiz saved successfully', 'tutor' ), $quiz_details );
399 } else {
400 $this->json_response( __( 'Error', 'tutor' ), $result->errors, HttpHelper::STATUS_BAD_REQUEST );
401 }
402
403 }
404 }
405