PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.6.2
Tutor LMS – eLearning and online course solution v3.6.2
3.9.15 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 1 year 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
407 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 // translators: %s is the tracking key required for each question.
235 $errors[ self::TRACKING_KEY ][] = sprintf( __( '%s is required for each question', 'tutor' ), self::TRACKING_KEY );
236 break;
237 }
238
239 if ( ! in_array( $question[ self::TRACKING_KEY ], array( self::FLAG_NEW, self::FLAG_UPDATE, self::FLAG_NO_CHANGE ), true ) ) {
240 $success = false;
241 // translators: %s is the tracking key for which the value is invalid.
242 $errors[ self::TRACKING_KEY ][] = sprintf( __( 'Invalid value for %s', 'tutor' ), self::TRACKING_KEY );
243 break;
244 }
245
246 if ( ! isset( $question['question_settings'] ) || ! is_array( $question['question_settings'] ) ) {
247 $success = false;
248 $errors['question_settings'][] = __( 'Question settings is required with array data', 'tutor' );
249 break;
250 }
251 }
252
253 return (object) array(
254 'success' => $success,
255 'errors' => $errors,
256 );
257 }
258
259 /**
260 * Handle delete questions and answers.
261 *
262 * @since 3.0.0
263 *
264 * @param array $deleted_question_ids question ids.
265 * @param array $deleted_answer_ids answer ids.
266 *
267 * @return void
268 */
269 public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array() ) {
270 global $wpdb;
271 $deleted_question_ids = array_filter( $deleted_question_ids, 'is_numeric' );
272 $deleted_answer_ids = array_filter( $deleted_answer_ids, 'is_numeric' );
273
274 if ( count( $deleted_question_ids ) ) {
275 $id_str = QueryHelper::prepare_in_clause( $deleted_question_ids );
276 //phpcs:ignore -- sanitized $id_str.
277 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE question_id IN (" . $id_str . ')' );
278 do_action( 'tutor_deleted_quiz_question_ids', $deleted_question_ids );
279 }
280
281 if ( count( $deleted_answer_ids ) ) {
282 $id_str = QueryHelper::prepare_in_clause( $deleted_answer_ids );
283 //phpcs:ignore -- sanitized $id_str.
284 $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN (" . $id_str . ')' );
285 }
286 }
287
288 /**
289 * Create or update quiz.
290 *
291 * @since 3.0.0
292 *
293 * @param int $topic_id topic id.
294 * @param array $payload payload.
295 *
296 * @return object consist success, errors.
297 */
298 public function save_quiz( $topic_id, $payload ) {
299 $success = true;
300 $data = null;
301 $errors = array();
302
303 $validation = $this->validate_payload( $payload );
304
305 if ( ! $validation->success ) {
306 return (object) array(
307 'success' => false,
308 'errors' => $validation->errors,
309 );
310 }
311
312 $is_update = isset( $payload['ID'] );
313 $quiz_id = $is_update ? $payload['ID'] : null;
314 $questions = isset( $payload['questions'] ) ? $payload['questions'] : array();
315
316 $menu_order = (int) ( isset( $payload['menu_order'] )
317 ? $payload['menu_order']
318 : tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id ) );
319
320 $quiz_data = array(
321 'post_type' => tutor()->quiz_post_type,
322 'post_title' => Input::sanitize( wp_slash( $payload['post_title'] ?? '' ) ),
323 'post_content' => Input::sanitize( wp_slash( $payload['post_content'] ?? '' ) ),
324 'post_status' => 'publish',
325 'post_author' => get_current_user_id(),
326 'post_parent' => $topic_id,
327 'menu_order' => $menu_order,
328 );
329
330 global $wpdb;
331 $wpdb->query( 'START TRANSACTION' );
332
333 try {
334 // Add or update the quiz.
335 if ( $is_update ) {
336 $quiz_data['ID'] = $quiz_id;
337 }
338
339 $quiz_id = wp_insert_post( $quiz_data );
340 do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
341
342 // Save quiz settings.
343 $quiz_option = Input::sanitize_array( $payload['quiz_option'] ?? array() ); //phpcs:ignore
344 update_post_meta( $quiz_id, Quiz::META_QUIZ_OPTION, $quiz_option );
345 do_action( 'tutor_quiz_settings_updated', $quiz_id );
346
347 // Save quiz questions.
348 if ( count( $questions ) ) {
349 $this->save_questions( $quiz_id, $questions );
350 }
351
352 // Delete questions and answers.
353 $deleted_question_ids = Input::post( 'deleted_question_ids', array(), Input::TYPE_ARRAY );
354 $deleted_answer_ids = Input::post( 'deleted_answer_ids', array(), Input::TYPE_ARRAY );
355 $this->handle_delete( $deleted_question_ids, $deleted_answer_ids );
356
357 $wpdb->query( 'COMMIT' );
358
359 $data = $quiz_id;
360
361 } catch ( \Throwable $th ) {
362 $wpdb->query( 'ROLLBACK' );
363
364 $success = false;
365 $errors['500'][] = $th->getMessage();
366 }
367
368 return (object) array(
369 'success' => $success,
370 'data' => $data,
371 'errors' => $errors,
372 );
373 }
374
375 /**
376 * Create or update quiz from new course builder.
377 *
378 * @since 3.0.0
379 *
380 * @return void json response.
381 */
382 public function ajax_quiz_builder_save() {
383 tutor_utils()->check_nonce();
384
385 $payload = $_POST['payload'] ?? array(); //phpcs:ignore
386 if ( is_string( $payload ) ) {
387 $payload = json_decode( wp_unslash( $payload ), true );
388 }
389
390 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
391 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
392 $course_cls = new Course( false );
393
394 $course_cls->check_access( $course_id );
395
396 $result = $this->save_quiz( $topic_id, wp_slash( $payload ) );
397 if ( $result->success ) {
398 $quiz_id = $result->data;
399 $quiz_details = QuizModel::get_quiz_details( $quiz_id );
400 $this->json_response( __( 'Quiz saved successfully', 'tutor' ), $quiz_details );
401 } else {
402 $this->json_response( __( 'Error', 'tutor' ), $result->errors, HttpHelper::STATUS_BAD_REQUEST );
403 }
404
405 }
406 }
407