Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor-pro/tutorai/CourseCreatorController.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + /**
3 + * Handle ChatGPT course creation.
4 + *
5 + * @package TutorPro\TutorAI
6 + * @author Themeum <support@themeum.com>
7 + * @link https://themeum.com
8 + * @since 3.0.0
9 + */
10 +
11 + namespace TutorPro\TutorAI;
12 +
13 + use Throwable;
14 + use TUTOR\Course;
15 + use Tutor\Helpers\HttpHelper;
16 + use Tutor\Helpers\ValidationHelper;
17 + use TUTOR\Input;
18 + use Tutor\Models\CourseModel;
19 + use TUTOR\Quiz;
20 + use TUTOR\QuizBuilder;
21 + use Tutor\Traits\JsonResponse;
22 +
23 + /**
24 + * AiCourseCreator Class.
25 + *
26 + * @since 3.0.0
27 + */
28 + class CourseCreatorController {
29 + use JsonResponse;
30 +
31 + /**
32 + * Topic menu order.
33 + *
34 + * @since 3.0.0
35 + *
36 + * @var integer
37 + */
38 + private $topic_order = 0;
39 +
40 + /**
41 + * Course content menu order.
42 + *
43 + * @since 3.0.0
44 + *
45 + * @var integer
46 + */
47 + private $content_order = 0;
48 +
49 + /**
50 + * Register hooks.
51 + *
52 + * @since 3.0.0
53 + *
54 + * @return void
55 + */
56 + public function __construct() {
57 + /**
58 + * Handle AJAX request for creating course contents generated by AI.
59 + *
60 + * @since 3.0.0
61 + */
62 + add_action( 'wp_ajax_tutor_pro_ai_course_create', array( $this, 'ajax_ai_course_create' ) );
63 +
64 + /**
65 + * Add a filter hook for course details response.
66 + *
67 + * @since 3.0.0
68 + */
69 + add_filter( 'tutor_course_details_response', array( $this, 'extend_course_details_response' ) );
70 + }
71 +
72 + /**
73 + * Validate payload.
74 + *
75 + * @since 3.0.0
76 + *
77 + * @param array $payload payload.
78 + *
79 + * @return object consist success, errors.
80 + */
81 + public function validate_payload( $payload ) {
82 + $errors = array();
83 + $success = true;
84 +
85 + if ( ! is_array( $payload ) ) {
86 + $success = false;
87 + $errors['payload'] = __( 'Invalid payload', 'tutor-pro' );
88 + }
89 +
90 + $rules = array(
91 + 'title' => 'required',
92 + 'description' => 'if_input|string',
93 + 'topics' => 'required|is_array',
94 + );
95 +
96 + $validation = ValidationHelper::validate(
97 + $rules,
98 + $payload
99 + );
100 +
101 + if ( ! $validation->success ) {
102 + $success = false;
103 + $errors = array_merge( $errors, $validation->errors );
104 + }
105 +
106 + return (object) array(
107 + 'success' => $success,
108 + 'errors' => $errors,
109 + );
110 + }
111 +
112 + /**
113 + * Add topic.
114 + *
115 + * @since 3.0.0
116 + *
117 + * @param int $course_id course id.
118 + * @param string $title title.
119 + * @param string $description description (optional).
120 + *
121 + * @return int|false
122 + */
123 + private function add_topic( $course_id, $title, $description = '' ) {
124 + $data = array(
125 + 'post_title' => $title,
126 + 'post_content' => $description,
127 + 'post_type' => tutor()->topics_post_type,
128 + 'post_parent' => $course_id,
129 + 'post_status' => 'publish',
130 + 'menu_order' => $this->topic_order,
131 + );
132 +
133 + $topic_id = wp_insert_post( $data );
134 +
135 + if ( ! $topic_id ) {
136 + return false;
137 + }
138 +
139 + return $topic_id;
140 + }
141 +
142 + /**
143 + * Add lesson.
144 + *
145 + * @since 3.0.0
146 + *
147 + * @param int $topic_id topic id.
148 + * @param string $title title.
149 + * @param string $description description (optional).
150 + *
151 + * @return void
152 + */
153 + private function add_lesson( $topic_id, $title, $description = '' ) {
154 + $data = array(
155 + 'post_title' => $title,
156 + 'post_content' => $description,
157 + 'post_type' => tutor()->lesson_post_type,
158 + 'post_parent' => $topic_id,
159 + 'post_status' => 'publish',
160 + 'menu_order' => $this->content_order,
161 + );
162 +
163 + wp_insert_post( $data );
164 + }
165 +
166 + /**
167 + * Check assignment addon active.
168 + *
169 + * @since 3.0.0
170 + *
171 + * @return boolean
172 + */
173 + private function is_assignment_addon_active() {
174 + $basename = plugin_basename( TUTOR_ASSIGNMENTS_FILE );
175 + return tutor_utils()->is_addon_enabled( $basename );
176 + }
177 +
178 + /**
179 + * Add assignment.
180 + *
181 + * @since 3.0.0
182 + *
183 + * @param int $topic_id topic id.
184 + * @param string $title title.
185 + * @param string $description description (optional).
186 + *
187 + * @return int|false
188 + */
189 + private function add_assignment( $topic_id, $title, $description = '' ) {
190 + $default_options = array(
191 + 'time_duration' => array(
192 + 'time' => 'weeks',
193 + 'value' => 1,
194 + ),
195 + 'total_mark' => 10,
196 + 'pass_mark' => 5,
197 + 'upload_files_limit' => 1,
198 + 'upload_file_size_limit' => 2,
199 + );
200 +
201 + $data = array(
202 + 'post_title' => $title,
203 + 'post_content' => $description,
204 + 'post_type' => tutor()->assignment_post_type,
205 + 'post_parent' => $topic_id,
206 + 'post_status' => 'publish',
207 + 'menu_order' => $this->content_order,
208 + );
209 +
210 + $assignment_id = wp_insert_post( $data );
211 + if ( $assignment_id ) {
212 + update_post_meta( $assignment_id, 'assignment_option', $default_options );
213 + }
214 +
215 + return $assignment_id ? $assignment_id : false;
216 + }
217 +
218 + /**
219 + * Prepare question.
220 + *
221 + * @since 3.0.0
222 + *
223 + * @param array $question question.
224 + *
225 + * @return array
226 + */
227 + private function prepare_question( $question ) {
228 + $type = $question['type'] ?? '';
229 +
230 + $arr[ QuizBuilder::TRACKING_KEY ] = QuizBuilder::FLAG_NEW;
231 + $arr['question_title'] = $question['title'] ?? '';
232 + $arr['question_description'] = $question['description'] ?? '';
233 + $arr['question_type'] = $type;
234 + $arr['question_mark'] = 1;
235 + $arr['question_settings'] = Quiz::get_default_question_settings( $type );
236 +
237 + $options = $question['options'] ?? array();
238 + foreach ( $options as $option ) {
239 + $arr ['question_answers'][] = array(
240 + QuizBuilder::TRACKING_KEY => QuizBuilder::FLAG_NEW,
241 + 'answer_title' => $option['name'] ?? '',
242 + 'is_correct' => $option['is_correct'] ?? 0,
243 + );
244 + }
245 +
246 + return $arr;
247 + }
248 +
249 + /**
250 + * Add assignment.
251 + *
252 + * @since 3.0.0
253 + *
254 + * @param int $topic_id topic id.
255 + * @param string $title title.
256 + * @param string $description description.
257 + * @param array $questions questions.
258 + *
259 + * @return void
260 + */
261 + private function add_quiz( $topic_id, $title, $description = '', $questions = array() ) {
262 + $allowed_types = array( 'true_false', 'multiple_choice', 'open_ended' );
263 + $prepared_questions = array();
264 +
265 + foreach ( $questions as $question ) {
266 + $type = $question['type'] ?? '';
267 + if ( ! in_array( $type, $allowed_types, true ) ) {
268 + continue;
269 + }
270 +
271 + $prepared_questions[] = $this->prepare_question( $question );
272 + }
273 +
274 + $payload = array(
275 + 'post_title' => $title,
276 + 'post_content' => $description,
277 + 'menu_order' => $this->content_order,
278 + 'quiz_option' => Quiz::get_default_quiz_settings(),
279 + 'questions' => $prepared_questions,
280 + );
281 +
282 + ( new QuizBuilder( false ) )->save_quiz( $topic_id, $payload );
283 + }
284 +
285 + /**
286 + * Add course contents.
287 + *
288 + * @since 3.0.0
289 + *
290 + * @param int $course_id course id.
291 + * @param array $payload payload.
292 + *
293 + * @return int course id.
294 + *
295 + * @throws Throwable If fail.
296 + */
297 + private function add_course_contents( $course_id, $payload ) {
298 + global $wpdb;
299 +
300 + try {
301 + $wpdb->query( 'START TRANSACTION' );
302 +
303 + $base_slug = sanitize_title( $payload['title'] );
304 + $unique_slug = wp_unique_post_slug( $base_slug, $course_id, get_post_status( $course_id ), tutor()->course_post_type, 0 );
305 +
306 + $course_data = array(
307 + 'post_title' => $payload['title'],
308 + 'post_content' => $payload['description'] ?? '',
309 + 'post_name' => $unique_slug,
310 + );
311 +
312 + $wpdb->update( $wpdb->posts, $course_data, array( 'ID' => $course_id ) );
313 +
314 + // Upload featured image add attached with course.
315 + if ( ! empty( $payload['featured_image'] ) ) {
316 + $featured_image = tutor_utils()->upload_base64_image( $payload['featured_image'] );
317 + update_post_meta( $course_id, '_thumbnail_id', $featured_image->id );
318 + }
319 +
320 + // Backup course authors.
321 + $course_authors = CourseModel::get_course_instructor_ids( $course_id );
322 +
323 + // Delete all existing topics and it's content.
324 + ( new CourseModel() )->delete_course_data( $course_id );
325 +
326 + // Re-assign course authors.
327 + foreach ( $course_authors as $author_id ) {
328 + add_user_meta( $author_id, '_tutor_instructor_course_id', $course_id );
329 + }
330 +
331 + // Create course contents.
332 + $topics = $payload['topics'] ?? array();
333 + foreach ( $topics as $topic ) {
334 + $topic_id = $this->add_topic( $course_id, $topic['title'] ?? '' );
335 + $this->topic_order++;
336 +
337 + $topic_contents = $topic['contents'] ?? array();
338 + $allowed_content_type = array( 'lesson', 'assignment', 'quiz' );
339 +
340 + foreach ( $topic_contents as $content ) {
341 + $content_type = $content['type'] ?? '';
342 + if ( ! in_array( $content_type, $allowed_content_type, true ) ) {
343 + continue;
344 + }
345 +
346 + $title = $content['title'] ?? '';
347 + $description = $content['description'] ?? '';
348 +
349 + /**
350 + * Add lesson
351 + */
352 + if ( 'lesson' === $content_type ) {
353 + $this->add_lesson( $topic_id, $title, $description );
354 + }
355 +
356 + /**
357 + * Add assignment
358 + */
359 + if ( 'assignment' === $content_type && $this->is_assignment_addon_active() ) {
360 + $this->add_assignment( $topic_id, $title, $description );
361 + }
362 +
363 + /**
364 + * Add quiz
365 + */
366 + if ( 'quiz' === $content_type ) {
367 + $questions = $content['questions'] ?? array();
368 + if ( count( $questions ) > 0 ) {
369 + $this->add_quiz( $topic_id, $title, $description, $questions );
370 + }
371 + }
372 +
373 + $this->content_order++;
374 + }
375 + }
376 +
377 + $wpdb->query( 'COMMIT' );
378 + } catch ( Throwable $error ) {
379 + $wpdb->query( 'ROLLBACK' );
380 + throw $error;
381 + }
382 +
383 + return $course_id;
384 + }
385 +
386 + /**
387 + * Extend course details response
388 + *
389 + * @since 3.0.0
390 + *
391 + * @param array $data response data.
392 + *
393 + * @return array
394 + */
395 + public function extend_course_details_response( array $data ) {
396 + $course_id = $data['ID'] ?? 0;
397 + if ( ! $course_id ) {
398 + return $data;
399 + }
400 +
401 + $data['total_enrolled_student'] = tutor_utils()->get_total_enrolments( '', '', $course_id );
402 +
403 + return $data;
404 + }
405 +
406 + /**
407 + * Course create with AI
408 + *
409 + * @since 3.0.0
410 + *
411 + * @return void
412 + */
413 + public function ajax_ai_course_create() {
414 + tutor_utils()->check_nonce();
415 +
416 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
417 + $course_cls = new Course( false );
418 + $course_cls->check_access( $course_id );
419 +
420 + $payload = $_POST['payload'] ?? array(); //phpcs:ignore
421 + if ( is_string( $payload ) ) {
422 + $payload = json_decode( wp_unslash( $payload ), true );
423 + }
424 +
425 + $validation = $this->validate_payload( $payload );
426 +
427 + if ( ! $validation->success ) {
428 + $this->json_response(
429 + tutor_utils()->error_message( 'validation_error' ),
430 + $validation->errors,
431 + HttpHelper::STATUS_UNPROCESSABLE_ENTITY
432 + );
433 + }
434 +
435 + $total_enrollment = tutor_utils()->get_total_enrolments( '', '', $course_id );
436 + if ( $total_enrollment > 0 ) {
437 + $this->json_response(
438 + __( "Re-creation isn't allowed due to enrolled students.", 'tutor-pro' ),
439 + null,
440 + HttpHelper::STATUS_BAD_REQUEST
441 + );
442 + }
443 +
444 + try {
445 + $this->add_course_contents( $course_id, $payload );
446 + $this->json_response(
447 + __( 'Successfully added', 'tutor-pro' ),
448 + $total_enrollment
449 + );
450 + } catch ( \Throwable $th ) {
451 + $this->json_response(
452 + $th->getMessage(),
453 + null,
454 + HttpHelper::STATUS_INTERNAL_SERVER_ERROR
455 + );
456 + }
457 + }
458 + }
459 +