PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.5
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.5
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / Modules / Course / Http / Controllers / CourseAdminController.php

CourseAdminController.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.5, at Modules/Course/Http/Controllers/CourseAdminController.php

1,265 lines 43.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Modules\Course\Http\Controllers;
4
5 use FluentCommunity\App\App;
6 use FluentCommunity\App\Functions\Utility;
7 use FluentCommunity\App\Http\Controllers\Controller;
8 use FluentCommunity\App\Models\BaseSpace;
9 use FluentCommunity\App\Models\Comment;
10 use FluentCommunity\App\Models\Reaction;
11 use FluentCommunity\App\Models\User;
12 use FluentCommunity\App\Models\XProfile;
13 use FluentCommunity\App\Services\CustomSanitizer;
14 use FluentCommunity\App\Services\Helper;
15 use FluentCommunity\App\Services\LockscreenService;
16 use FluentCommunity\App\Services\ProfileHelper;
17 use FluentCommunity\App\Services\FeedsHelper;
18 use FluentCommunity\Framework\Http\Request\Request;
19 use FluentCommunity\Framework\Support\Arr;
20 use FluentCommunity\App\Models\SpaceUserPivot;
21 use FluentCommunity\Modules\Course\Model\Course;
22 use FluentCommunity\Modules\Course\Model\CourseLesson;
23 use FluentCommunity\Modules\Course\Model\CourseTopic;
24 use FluentCommunity\Modules\Course\Services\CourseHelper;
25
26 class CourseAdminController extends Controller
27 {
28 public function getCourses(Request $request)
29 {
30 $user = $this->getUser();
31
32 $status = $request->getSafe('status');
33 $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'latest');
34 $topicSlug = $request->getSafe('topic_slug');
35
36 $query = Course::searchBy($request->getSafe('search'))
37 ->byAdminAccess($user->ID)
38 ->byPostTopic($topicSlug);
39
40 if ($status && in_array($status, [ 'published', 'draft' ])) {
41 $query->where('status', $status);
42 }
43
44 if ($sortBy === 'alphabetical') {
45 $query->orderBy('title', 'ASC');
46 } else {
47 $query->orderBy('created_at', 'DESC');
48 }
49
50 $courses = $query->with([ 'owner' ])->paginate();
51
52 foreach ($courses as $course) {
53 $course->students_count = $course->students()->count();
54 if (!$course->cover_photo) {
55 $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
56 }
57 $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
58 $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
59 }
60
61 $data = [
62 'courses' => $courses,
63 'course_categories' => $request->get('with_categories') ? CourseHelper::getCourseCategories() : [],
64 ];
65
66 return apply_filters('fluent_community/admin_courses_api_response', $data, $request->all());
67 }
68
69 public function createCourse(Request $request)
70 {
71 $this->validate($request->all(), [
72 'title' => 'required',
73 'description' => 'required',
74 'privacy' => 'required|in:public,private,secret',
75 'course_type' => 'required|in:self_paced,structured,scheduled',
76 ]);
77
78 $parentId = $request->get('parent_id');
79 if ($parentId) {
80 $serial = BaseSpace::where('parent_id', $parentId)->max('serial') + 1;
81 } else {
82 $serial = BaseSpace::max('serial') + 1;
83 }
84
85 $courseData = [
86 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
87 'title' => $request->getSafe('title', 'sanitize_text_field'),
88 'privacy' => $request->get('privacy'),
89 'description' => wp_kses_post($request->get('description')),
90 'status' => $request->get('status', 'draft'),
91 'settings' => [
92 'course_type' => $request->get('course_type'),
93 'emoji' => CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', '')),
94 'shape_svg' => CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', '')),
95 'disable_comments' => $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no',
96 'hide_members_count' => $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no',
97 'course_layout' => $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic',
98 'course_details' => CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details'))),
99 'hide_instructor_view' => $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no',
100 'show_instructor_students_count' => $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no',
101 'sequential_lesson_order' => $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no',
102 ],
103 'serial' => $serial,
104 ];
105
106 $lockScreenType = $request->get('settings.custom_lock_screen');
107 if (!in_array($lockScreenType, [ 'yes', 'no', 'redirect' ]) || $request->get('privacy') != 'private') {
108 $lockScreenType = 'no';
109 }
110
111 $courseData['settings']['custom_lock_screen'] = $lockScreenType;
112
113 if ($lockScreenType === 'redirect') {
114 $redirectUrl = $request->get('settings.onboard_redirect_url');
115 if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
116 return $this->sendError([
117 'message' => __('Course Redirect URL is not valid', 'fluent-community'),
118 ]);
119 }
120 $courseData['settings']['onboard_redirect_url'] = sanitize_url($redirectUrl);
121 }
122
123 if ($request->get('privacy') == 'public' && $request->get('course_type') == 'self_paced') {
124 $courseData['settings']['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
125 }
126
127 $slug = $request->get('slug');
128
129 $slug = $slug ? $slug : $courseData['title'];
130
131 $slug = preg_replace('/[^a-zA-Z0-9-_]/', '', $slug);
132
133 $slug = sanitize_title($slug, '');
134
135 if ($slug) {
136 $slug = Utility::slugify($slug);
137 $exist = Course::where('slug', $slug)
138 ->exists();
139
140 if ($exist) {
141 $slug = $slug . '-' . time();
142 }
143
144 $courseData['slug'] = $slug;
145 }
146
147 do_action('fluent_community/course/before_create', $courseData);
148
149 $course = Course::create($courseData);
150
151 $imageTypes = [ 'cover_photo', 'logo' ];
152
153 $metaData = [];
154 foreach ($imageTypes as $type) {
155 if (!empty($request->get($type))) {
156 $media = Helper::getMediaFromUrl($request->get($type));
157 if (!$media || $media->is_active) {
158 continue;
159 }
160 $metaData[$type] = $media->public_url;
161 $media->update([
162 'is_active' => true,
163 'user_id' => get_current_user_id(),
164 'sub_object_id' => $course->id,
165 'object_source' => 'space_' . $type,
166 ]);
167 }
168 }
169
170 if ($metaData) {
171 $course->fill($metaData);
172 $course->save();
173 }
174
175 if ($request->get('category_ids', [])) {
176 $topicsConfig = Helper::getTopicsConfig();
177 $categoryIds = (array) $request->get('category_ids', []);
178 $categoryIds = array_slice($categoryIds, 0, $topicsConfig['max_topics_per_space']);
179 $course->syncCategories($categoryIds);
180 }
181
182 do_action('fluent_community/course/created', $course);
183
184 return [
185 'message' => __('Course has been created successfully', 'fluent-community'),
186 'course' => $course,
187 ];
188 }
189
190 public function findCourse(Request $request, $courseId)
191 {
192 /** @var Course $course */
193 $course = Course::where('id', $courseId)
194 ->with([ 'owner' ])
195 ->firstOrFail();
196
197 $course->students_count = $course->students()->count();
198 $course->course_type = $course->settings['course_type'];
199 $course->lockscreen = $course->getLockscreen();
200
201 $course->category_ids = $course->categories->pluck('id')->toArray();
202
203 if ($course->students_count) {
204 $course->completed_students = $course->getCompletedStrundesCount();
205 $course->overAllProgress = CourseHelper::overallCourseProgressAverage($course);
206 }
207
208 unset($course->categories);
209
210 $course = apply_filters('fluent_community/course_info', $course, $request->all());
211
212 return [
213 'course' => $course,
214 ];
215 }
216
217 public function updateCourse(Request $request, $courseId)
218 {
219 $this->validate($request->all(), [
220 'title' => 'required',
221 'description' => 'required',
222 'privacy' => 'required|in:public,private,secret',
223 'status' => 'required|in:draft,published,archived',
224 'course_type' => 'required|in:self_paced,structured,scheduled',
225 'created_by' => 'exists:users,ID',
226 ]);
227
228 $course = Course::findOrFail($courseId);
229
230 $courseData = [
231 'title' => $request->getSafe('title', 'sanitize_text_field'),
232 'privacy' => $request->get('privacy'),
233 'description' => wp_kses_post($request->get('description')),
234 'status' => $request->get('status'),
235 'cover_photo' => $request->getSafe('cover_photo', 'sanitize_url'),
236 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
237 ];
238
239 $slug = $request->get('slug');
240 if ($slug && $course->slug != $slug) {
241 $slug = Utility::slugify($slug);
242
243 $exist = App::getInstance('db')->table('fcom_spaces')->where('slug', $slug)
244 ->where('id', '!=', $course->id)
245 ->exists();
246
247 if ($exist || !$slug) {
248 return $this->sendError([
249 'message' => __('Slug is already taken. Please use a different slug', 'fluent-community'),
250 ]);
251 }
252
253 $courseData['slug'] = $slug;
254 }
255
256 if ($request->get('created_by') && Helper::isSiteAdmin()) {
257 $courseData['created_by'] = (int)$request->get('created_by');
258 }
259
260 $imageTypes = [ 'cover_photo', 'logo' ];
261
262 foreach ($imageTypes as $type) {
263 if (!empty($request->get($type))) {
264 $media = Helper::getMediaFromUrl($request->get($type));
265 if (!$media || $media->is_active) {
266 continue;
267 }
268 $courseData[$type] = $media->public_url;
269 $media->update([
270 'is_active' => true,
271 'user_id' => get_current_user_id(),
272 'sub_object_id' => $course->id,
273 'object_source' => 'space_' . $type,
274 ]);
275 } else {
276 $courseData[$type] = null;
277 }
278 }
279
280 $existingSettings = $course->settings;
281 $existingSettings['course_type'] = $request->get('course_type');
282
283 $lockScreenType = $request->get('settings.custom_lock_screen');
284 if (!in_array($lockScreenType, [ 'yes', 'no', 'redirect' ]) || $request->get('privacy') != 'private') {
285 $lockScreenType = 'no';
286 }
287 if ($lockScreenType == 'redirect') {
288 $redirectUrl = $request->get('settings.onboard_redirect_url');
289 if (!$redirectUrl || !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
290 return $this->sendError([
291 'message' => __('Course Redirect URL is not valid', 'fluent-community'),
292 ]);
293 }
294 $existingSettings['onboard_redirect_url'] = sanitize_url($redirectUrl);
295 }
296
297 $existingSettings['custom_lock_screen'] = $lockScreenType;
298 $existingSettings['emoji'] = CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', ''));
299 $existingSettings['shape_svg'] = CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', ''));
300 $existingSettings['disable_comments'] = $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no';
301 $existingSettings['hide_members_count'] = $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no';
302 $existingSettings['hide_instructor_view'] = $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no';
303 $existingSettings['show_instructor_students_count'] = $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no';
304 $existingSettings['show_paywalls'] = $request->get('settings.show_paywalls') === 'yes' ? 'yes' : 'no';
305 $existingSettings['show_welcome_banner'] = $request->get('settings.show_welcome_banner') === 'yes' ? 'yes' : 'no';
306 $existingSettings['course_layout'] = $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic';
307 $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details')));
308 $existingSettings['sequential_lesson_order'] = $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no';
309
310 if ($request->get('privacy') == 'public' && $existingSettings['course_type'] == 'self_paced') {
311 $existingSettings['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
312 } else {
313 unset($existingSettings['public_lesson_view']);
314 }
315
316 $courseData['settings'] = $existingSettings;
317
318 $previousStatus = $course->status;
319
320 $prevCourse = clone $course;
321
322 $course->fill($courseData);
323 $dirtyFields = $course->getDirty();
324
325 if ($dirtyFields) {
326 $course->save();
327 do_action('fluent_community/course/updated', $course, $dirtyFields, $prevCourse);
328 if ($previousStatus != 'published' && $course->status == 'published') {
329 do_action('fluent_community/course/published', $course);
330 }
331 }
332
333 if ($request->get('category_ids', [])) {
334 $topicsConfig = Helper::getTopicsConfig();
335 $categoryIds = (array) $request->get('category_ids', []);
336 $categoryIds = array_slice($categoryIds, 0, $topicsConfig['max_topics_per_space']);
337 $course->syncCategories($categoryIds);
338 }
339
340 $metaSettings = $request->get('meta_settings', []);
341 if($metaSettings) {
342 foreach ($metaSettings as $metaProvider => $metaData) {
343 do_action('fluent_community/course/update_meta_settings_'.$metaProvider, $metaData, $course);
344 }
345 }
346
347 return [
348 'message' => __('Course has been updated successfully.', 'fluent-community'),
349 'course' => $course,
350 ];
351 }
352
353 public function duplicateCourse(Request $request, $courseId)
354 {
355 $original = Course::findOrFail($courseId);
356
357 $courseData = $original->toArray();
358 $courseData['title'] = $original->title . ' (Copy)';
359 $courseData['slug'] = Utility::slugify($original->slug . '-' . time());
360 $courseData['status'] = 'draft';
361 $courseData['created_by'] = get_current_user_id();
362
363 do_action('fluent_community/course/before_create', $courseData);
364
365 /** @var Course $newCourse */
366 $newCourse = $original->replicate();
367 $newCourse->title = $courseData['title'];
368 $newCourse->slug = $courseData['slug'];
369 $newCourse->status = $courseData['status'];
370 $newCourse->created_by = $courseData['created_by'];
371 $newCourse->save();
372
373 if ($original->categories) {
374 $newCourse->syncCategories($original->categories->pluck('id')->toArray());
375 }
376
377 $topics = CourseTopic::with('lessons')
378 ->where('space_id', $original->id)
379 ->get();
380
381 foreach ($topics as $topic) {
382 /** @var CourseTopic $topic */
383 $newTopic = $topic->replicate();
384 $newTopic->space_id = $newCourse->id;
385 $newTopic->save();
386
387 foreach ($topic->lessons as $lesson) {
388 /** @var CourseLesson $lesson */
389 $newLesson = $lesson->replicate();
390 $newLesson->space_id = $newCourse->id;
391 $newLesson->parent_id = $newTopic->id;
392 $newLesson->save();
393 CourseHelper::copyLessonDocuments($lesson, $newLesson);
394 }
395 }
396
397 do_action('fluent_community/course/created', $newCourse);
398
399 return [
400 'message' => __('Course duplicated successfully.', 'fluent-community'),
401 'course' => $newCourse,
402 ];
403 }
404
405 public function deleteCourse(Request $request, $courseId)
406 {
407 $course = Course::findOrFail($courseId);
408
409 do_action('fluent_community/course/before_delete', $course);
410
411 // Let's remove the reactions
412 Reaction::query()->whereHas('feed', function ($q) use ($course) {
413 $q->where('space_id', $course->id);
414 })->delete();
415
416 Comment::whereHas('post', function ($q) use ($course) {
417 $q->where('space_id', $course->id);
418 })->delete();
419
420 $courseTopics = CourseTopic::with('lessons')
421 ->where('space_id', $course->id)
422 ->get();
423
424 foreach ($courseTopics as $courseTopic) {
425 do_action('fluent_community/section/before_deleted', $courseTopic);
426
427 foreach ($courseTopic->lessons as $courseLesson) {
428 do_action('fluent_community/lesson/before_deleted', $courseLesson);
429 $courseLesson->delete();
430 }
431
432 $courseTopic->delete();
433 }
434
435 // Let's delete the student enrollments
436 SpaceUserPivot::where('space_id', $course->id)
437 ->delete();
438
439 $courseId = $course->id;
440 $course->delete();
441
442 do_action('fluent_community/course/deleted', $courseId);
443
444 return [
445 'message' => __('Course has been deleted successfully along with all the associated data', 'fluent-community'),
446 ];
447 }
448
449 public function getCourseComments(Request $request, $courseId)
450 {
451 $course = Course::findOrFail($courseId);
452
453 $comments = Comment::byContentModerationAccessStatus($this->getUser(), $course)
454 ->whereHas('post', function ($q) use ($courseId) {
455 return $q->where('space_id', $courseId);
456 })
457 ->orderBy('id', 'DESC')
458 ->with([
459 'post' => function ($q) {
460 return $q->select([ 'id', 'title', 'slug' ]);
461 },
462 'xprofile' => function ($q) {
463 $q->select(ProfileHelper::getXProfilePublicFields());
464 },
465 ])
466 ->paginate();
467
468 foreach ($comments as $comment) {
469 if ($comment->user) {
470 $comment->user->makeHidden([ 'user_email' ]);
471 }
472 $likedIds = FeedsHelper::getLikedIdsByUserFeedId($comment->post_id, get_current_user_id());
473 if ($likedIds && in_array($comment->id, $likedIds)) {
474 $comment->liked = 1;
475 }
476 }
477
478 $data = [
479 'comments' => $comments,
480 ];
481
482 return apply_filters('fluent_community/admin_course_comments_api_response', $data, $request->all());
483 }
484
485 public function getCourseStudents(Request $request, $courseId)
486 {
487 Course::findOrFail($courseId);
488
489 $search = $request->getSafe('search');
490
491 $students = XProfile::whereHas('space_pivot', function ($q) use ($courseId) {
492 return $q->where('space_id', $courseId)
493 ->where('role', 'student');
494 })
495 ->searchBy($search)
496 ->whereHas('user')
497 ->with([
498 'space_pivot' => function ($q) use ($courseId) {
499 return $q->where('space_id', $courseId);
500 },
501 ])
502 ->select(ProfileHelper::getXProfilePublicFields())
503 ->paginate();
504
505 $studentUserIds = $students->pluck('user_id')->toArray();
506
507 $progressMap = CourseHelper::getBulkCourseProgress($courseId, $studentUserIds);
508
509 foreach ($students as $student) {
510 $student->progress = $progressMap[$student->user_id] ?? 0;
511 }
512
513 $data = [
514 'students' => $students,
515 ];
516
517 return apply_filters('fluent_community/admin_course_students_api_response', $data, $request->all());
518 }
519
520 public function addStudent(Request $request, $courseId)
521 {
522 $course = Course::findOrFail($courseId);
523
524 $this->validate($request->all(), [
525 'user_id' => 'required|exists:users,ID',
526 ]);
527
528 $userId = (int)$request->get('user_id');
529 $targetUser = User::findOrFail($userId);
530 $xprofile = $targetUser->syncXProfile();
531
532 if ($xprofile && $xprofile->status != 'active') {
533 return $this->sendError([
534 'message' => __('Selected user is not active', 'fluent-community'),
535 ]);
536 }
537
538 $enrolled = CourseHelper::enrollCourse($course, $userId, 'by_admin');
539
540 if (!$enrolled) {
541 return $this->sendError([
542 'message' => __('User is already added to this course.', 'fluent-community'),
543 ]);
544 }
545
546 return [
547 'message' => __('User has been added to this course', 'fluent-community'),
548 ];
549 }
550
551 public function removeStudent(Request $request, $courseId, $studentId)
552 {
553 $course = Course::findOrFail($courseId);
554
555 $student = SpaceUserPivot::bySpace($course->id)
556 ->byUser($studentId)
557 ->first();
558
559 if (!$student) {
560 return $this->sendError([
561 'message' => __('Selected user is not a student of this course', 'fluent-community'),
562 ]);
563 }
564
565 Helper::removeFromSpace($course, $studentId, 'by_admin');
566
567 return [
568 'message' => __('Student has been removed from this course', 'fluent-community'),
569 ];
570 }
571
572 public function resetStudentProgress(Request $request, $courseId, $studentId)
573 {
574 Course::findOrFail($courseId);
575
576 $pivot = SpaceUserPivot::where('space_id', $courseId)
577 ->where('user_id', $studentId)
578 ->where('role', 'student')
579 ->first();
580
581 if (!$pivot) {
582 return $this->sendError([
583 'message' => __('This student is not enrolled in this course.', 'fluent-community'),
584 ]);
585 }
586
587 CourseHelper::resetCourseProgress($courseId, (int) $studentId);
588
589 return [
590 'message' => __("Student's progress has been reset.", 'fluent-community'),
591 ];
592 }
593
594 public function getSections(Request $request, $courseId)
595 {
596 $course = Course::findOrFail($courseId);
597 /** @var Course $course */
598
599 $sectionsQuery = CourseTopic::where('space_id', $courseId)
600 ->orderBy('priority', 'ASC')
601 ->orderBy('id', 'ASC');
602
603 if (in_array('only_published', $request->get('conditions', []))) {
604 $sectionsQuery->where('status', 'published')
605 ->with([
606 'lessons' => function ($q) {
607 $q->where('status', 'published');
608 },
609 ]);
610 }
611
612 if (empty($request->get('conditions', []))) {
613 $sectionsQuery->with([ 'lessons' ]);
614 }
615
616 $sections = $sectionsQuery->get();
617
618 $data = [
619 'sections' => $sections,
620 ];
621
622 if ($request->get('with_lock_screen')) {
623 $data['lockscreen'] = LockscreenService::getLockscreenSettings($course);
624 }
625
626 return apply_filters('fluent_community/admin_course_sections_api_response', $data, $request->all());
627 }
628
629 public function getSection(Request $request, $courseId, $topicId)
630 {
631 $topic = CourseTopic::where('space_id', $courseId)
632 ->whereHas('course', function ($query) use ($courseId) {
633 $query->where('id', $courseId);
634 })
635 ->where('id', $topicId)
636 ->with([ 'lessons' ])
637 ->firstOrFail();
638
639 $data = [
640 'topic' => $topic,
641 ];
642
643 return apply_filters('fluent_community/admin_course_section_api_response', $data, $request->all());
644 }
645
646 public function resetSectionIndexes(Request $request, $courseId)
647 {
648 $indexes = $request->get('indexes', []);
649
650 $sections = CourseTopic::where('space_id', $courseId)
651 ->whereIn('id', array_keys($indexes))
652 ->get();
653
654 foreach ($sections as $section) {
655 if (!isset($indexes[$section->id])) { continue;
656 }
657 $section->priority = $indexes[$section->id];
658 $section->save();
659 }
660
661 return [
662 'sections' => $sections,
663 'message' => __('Section indexes have been updated successfully.', 'fluent-community'),
664 ];
665 }
666
667 public function resetLessonIndexes(Request $request, $courseId, $sectionId)
668 {
669 $indexes = $request->get('indexes', []);
670
671 $lessons = CourseLesson::where('space_id', $courseId)
672 ->where('parent_id', $sectionId)
673 ->whereIn('id', array_keys($indexes))
674 ->get();
675
676 foreach ($lessons as $lesson) {
677 if (!isset($indexes[$lesson->id])) { continue;
678 }
679 $lesson->priority = $indexes[$lesson->id];
680 $lesson->save();
681 }
682
683 return [
684 'lessons' => $lessons,
685 'message' => __('Lesson indexes have been updated successfully.', 'fluent-community'),
686 ];
687 }
688
689 public function moveLesson(Request $request, $courseId)
690 {
691 $lessonId = $request->getSafe('lesson_id', 'intval');
692 $sectionId = $request->getSafe('section_id', 'intval');
693
694 Course::findOrFail($courseId);
695 CourseTopic::findOrFail($sectionId);
696
697 $lesson = CourseLesson::findOrFail($lessonId);
698
699 $lesson->update([
700 'parent_id' => $sectionId,
701 ]);
702
703 return [
704 'message' => __('Lesson has been moved successfully', 'fluent-community'),
705 ];
706 }
707
708 public function createSection(Request $request, $courseId)
709 {
710 $this->validate($request->all(), [
711 'title' => 'required',
712 ]);
713
714 $sectionData = [
715 'title' => $request->getSafe('title'),
716 'space_id' => $courseId,
717 'status' => 'published',
718 ];
719
720 Course::findOrFail($courseId);
721
722 $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $courseId)->max('priority');
723
724 $sectionData['priority'] = (int) $latestPriority + 1;
725
726 $section = CourseTopic::create($sectionData);
727
728 $section->load('lessons');
729
730 return [
731 'message' => __('Section has been created successfully.', 'fluent-community'),
732 'section' => $section,
733 ];
734 }
735
736 public function updateSection(Request $request, $courseId, $tipicId)
737 {
738 $this->validate($request->all(), [
739 'title' => 'required',
740 'status' => 'required|in:draft,published,archived',
741 ]);
742
743 Course::findOrFail($courseId);
744
745 $topic = CourseTopic::where('space_id', $courseId)
746 ->where('id', $tipicId)
747 ->firstOrFail();
748
749
750 $topicData = [
751 'title' => $request->getSafe('title'),
752 'status' => $request->get('status'),
753 ];
754
755 $topic->update($topicData);
756
757 return [
758 'message' => __('Topic has been updated successfully.', 'fluent-community'),
759 'topic' => $topic,
760 ];
761 }
762
763 public function patchSection(Request $request, $courseId, $tipicId)
764 {
765 $course = Course::findOrFail($courseId);
766
767 $topic = CourseTopic::where('space_id', $courseId)
768 ->where('id', $tipicId)
769 ->firstOrFail();
770
771 $acceptedFields = [ 'title', 'status' ];
772
773 if ($course->getCourseType() == 'scheduled') {
774 $acceptedFields[] = 'scheduled_at';
775 } elseif ($course->getCourseType() == 'structured') {
776 $acceptedFields[] = 'reactions_count';
777 }
778
779 $topicData = $request->only($acceptedFields);
780
781 if (!empty($topicData['scheduled_at'])) {
782 $topic->reactions_count = 0;
783 } elseif (isset($topicData['reactions_count'])) {
784 $topic->scheduled_at = null;
785 $topic->reactions_count = $topicData['reactions_count'];
786 }
787
788 $topicData = apply_filters('fluent_community/section/update_data', $topicData, $course, $topic, $request->all());
789
790 if (is_wp_error($topicData)) {
791 return $this->sendError($topicData->get_error_messages());
792 }
793
794 $topic->fill($topicData);
795
796 $scheduledAtDirty = $topic->isDirty('scheduled_at');
797 $reactionsCountDirty = $topic->isDirty('reactions_count');
798
799 $topic->save();
800
801 if ($scheduledAtDirty) {
802 do_action('fluent_community/section/scheduled_at_updated', $course, $topic);
803 }
804 if ($reactionsCountDirty) {
805 do_action('fluent_community/section/reactions_count_updated', $course, $topic);
806 }
807
808 return [
809 'message' => __('Topic has been updated successfully.', 'fluent-community'),
810 'topic' => $topic,
811 ];
812 }
813
814 public function copySection(Request $request, $toCourseId)
815 {
816 $sectionId = $request->getSafe('section_id', 'intval');
817 $fromCourseId = $request->getSafe('from_course_id', 'intval');
818
819 $fromCourse = Course::findOrFail($fromCourseId);
820
821 if (!$fromCourse->isCourseAdmin()) {
822 return $this->sendError([
823 'message' => __('You do not have permission to access this course', 'fluent-community'),
824 ]);
825 }
826
827 /** @var CourseTopic $originalSection */
828 $originalSection = CourseTopic::where('id', $sectionId)
829 ->where('space_id', $fromCourseId)
830 ->firstOrFail();
831
832 $toCourse = Course::findOrFail($toCourseId);
833
834 $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $toCourse->id)->max('priority');
835
836 /** @var CourseTopic $newSection */
837 $newSection = $originalSection->replicate();
838 $newSection->space_id = $toCourse->id;
839 $newSection->priority = (int) $latestPriority + 1;
840 $newSection->save();
841
842 $originalLessons = CourseLesson::where('parent_id', $originalSection->id)->get();
843 foreach ($originalLessons as $lesson) {
844 /** @var CourseLesson $lesson */
845 $newLesson = $lesson->replicate();
846 $newLesson->space_id = $toCourse->id;
847 $newLesson->parent_id = $newSection->id;
848 $newLesson->save();
849 CourseHelper::copyLessonDocuments($lesson, $newLesson);
850 }
851
852 $newSection->load('lessons');
853
854 return [
855 'message' => __('Section has been copied to the selected course', 'fluent-community'),
856 'section' => $newSection,
857 ];
858 }
859
860 public function deleteSection(Request $request, $courseId, $sectionId)
861 {
862 $topic = CourseTopic::where([
863 'id' => $sectionId,
864 'space_id' => $courseId,
865 ])->firstOrFail();
866
867 do_action('fluent_community/section/before_deleted', $topic);
868
869 $topic->delete();
870
871 $lessons = CourseLesson::where([
872 'parent_id' => $sectionId,
873 'space_id' => $courseId,
874 ])->get();
875
876 foreach ($lessons as $lesson) {
877 do_action('fluent_community/lesson/before_deleted', $lesson);
878 $lesson->delete();
879 }
880
881 return [
882 'message' => __('Section has been deleted successfully.', 'fluent-community'),
883 ];
884 }
885
886 public function getLessons(Request $request, $courseId)
887 {
888 Course::findOrFail($courseId);
889
890 $lessons = CourseLesson::where('space_id', $courseId)
891 ->orderBy('priority', 'ASC')
892 ->orderBy('id', 'ASC');
893
894 $topicId = (int)$request->get('topic_id');
895
896 if ($topicId) {
897 $lessons = $lessons->where('parent_id', $topicId);
898 }
899
900 $lessons = $lessons->get();
901
902 $data = [
903 'lessons' => $lessons,
904 ];
905
906 return apply_filters('fluent_community/admin_course_lessons_api_response', $data, $request->all());
907 }
908
909 public function getLesson(Request $request, $courseId, $lessonId)
910 {
911 $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
912 $query->where('id', $courseId);
913 })
914 ->where('id', $lessonId)
915 ->with([ 'topic', 'course' ])
916 ->firstOrFail();
917
918 $data = [
919 'lesson' => $lesson,
920 ];
921
922 return apply_filters('fluent_community/admin_course_lesson_api_response', $data, $request->all());
923 }
924
925 public function createLesson(Request $request, $courseId)
926 {
927 $this->validate($request->all(), [
928 'title' => 'required',
929 'section_id' => 'required',
930 ]);
931
932 $sectionId = (int)$request->get('section_id');
933
934 $topic = CourseTopic::whereHas('course', function ($query) use ($courseId) {
935 $query->where('id', $courseId);
936 })
937 ->where('id', $sectionId)
938 ->firstOrFail();
939
940 $lessonData = [
941 'title' => $request->getSafe('title'),
942 'parent_id' => $topic->id,
943 'space_id' => $courseId,
944 'status' => 'draft',
945 ];
946
947 $latestPriority = CourseLesson::where('type', 'course_lesson')
948 ->where('parent_id', $sectionId)
949 ->where('space_id', $courseId)
950 ->max('priority');
951
952 $lessonData['priority'] = (int) $latestPriority + 1;
953
954 $lessonData = apply_filters('fluent_community/lesson/create_data', $lessonData, $request);
955
956 $lesson = CourseLesson::create($lessonData);
957
958 $lesson = CourseLesson::findOrFail($lesson->id);
959
960 return [
961 'message' => __('Lesson has been created successfully.', 'fluent-community'),
962 'lesson' => $lesson,
963 ];
964 }
965
966 public function updateLesson(Request $request, $courseId, $lessionId)
967 {
968 Course::findOrFail($courseId);
969
970 $lessonData = $request->get('lesson');
971
972 $this->validate($lessonData, [
973 'title' => 'required',
974 'parent_id' => 'required',
975 'status' => 'required|in:draft,published,archived',
976 ]);
977
978 CourseTopic::whereHas('course', function ($query) use ($courseId) {
979 $query->where('id', $courseId);
980 })
981 ->where('id', $lessonData['parent_id'])
982 ->firstOrFail();
983
984 /** @var CourseLesson $lesson */
985 $lesson = CourseLesson::where('id', $lessionId)
986 ->where('space_id', $courseId)
987 ->firstOrFail();
988
989 $previousStatus = $lesson->status;
990
991 $updatedMeta = CourseHelper::sanitizeLessonMeta(Arr::get($lessonData, 'meta', []), $lesson);
992 $updatedMeta['document_ids'] = Arr::get($lesson->meta, 'document_ids', []);
993
994 if ($mediaId = Arr::get($updatedMeta, 'featured_image_id')) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
995 if (!$lesson->isQuizType()) {
996 $media = wp_get_attachment_image_url($mediaId);
997 $lesson->featured_image = $media ? $media : null;
998 } else {
999 $media = Helper::getMediaFromUrl(sanitize_url($mediaId));
1000 if ($media && !$media->is_active) {
1001 Helper::removeMediaByUrl($lesson->featured_image, $lesson->id);
1002 $lesson->featured_image = $media->public_url;
1003 $media->update([
1004 'is_active' => true,
1005 'user_id' => get_current_user_id(),
1006 'sub_object_id' => $lesson->id,
1007 'object_source' => 'quiz_thumbnail_' . $lesson->id,
1008 ]);
1009 }
1010 }
1011 } else {
1012 Helper::removeMediaByUrl($lesson->featured_image, $lesson->id);
1013 $lesson->featured_image = null;
1014 }
1015
1016 $updateData = array_filter([
1017 'title' => sanitize_text_field(Arr::get($lessonData, 'title')),
1018 'status' => Arr::get($lessonData, 'status'),
1019 'meta' => wp_parse_args($updatedMeta, $lesson->meta),
1020 ]);
1021
1022 // message bypasses array_filter so an emptied lesson body still saves
1023 if (array_key_exists('message', $lessonData)) {
1024 $updateData['message'] = CourseHelper::santizeLessonBody((string) Arr::get($lessonData, 'message'));
1025 }
1026
1027 $updateData = apply_filters('fluent_community/lesson/update_data', $updateData, $lesson);
1028
1029 $lesson->fill($updateData);
1030 $dirtyFields = $lesson->getDirty();
1031
1032 if ($dirtyFields) {
1033 $lesson->save();
1034 $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1035 do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1036 }
1037
1038 do_action('fluent_community/lesson/additional_media_updated', $request->all(), $lesson, $updateData);
1039
1040 return [
1041 'message' => __('Lesson has been updated successfully.', 'fluent-community'),
1042 'lesson' => $lesson,
1043 ];
1044 }
1045
1046 public function patchLesson(Request $request, $courseId, $lessionId)
1047 {
1048 $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
1049 $query->where('id', $courseId);
1050 })
1051 ->where('id', $lessionId)
1052 ->firstOrFail();
1053
1054 $acceptedFields = [ 'title', 'status', 'slug' ];
1055
1056 // empty title/slug/status must not overwrite, but a literal "0" is a valid value
1057 $lessonData = array_filter($request->only($acceptedFields), function ($value) {
1058 return $value !== null && $value !== '';
1059 });
1060
1061 if (Arr::get($lessonData, 'status') === 'published' && $lesson->status !== 'published') {
1062 if (empty($lesson->scheduled_at)) {
1063 $lessonData['scheduled_at'] = current_time('mysql');
1064 }
1065 }
1066
1067 // message bypasses the empty-value filter above so an emptied lesson body still saves
1068 if ($request->exists('message')) {
1069 $lessonData['message'] = CourseHelper::santizeLessonBody((string) $request->get('message'));
1070 }
1071
1072 if (!empty($lessonData)) {
1073 $lesson->fill($lessonData);
1074 if ($lesson->isDirty()) {
1075 $lesson->save();
1076 }
1077 }
1078
1079 return [
1080 'message' => __('Lesson has been updated successfully.', 'fluent-community'),
1081 'lesson' => $lesson,
1082 ];
1083 }
1084
1085 public function deleteLesson(Request $request, $courseId, $lessionId)
1086 {
1087 $lesson = CourseLesson::whereHas('course', function ($query) use ($courseId) {
1088 $query->where('id', $courseId);
1089 })
1090 ->where('id', $lessionId)
1091 ->firstOrFail();
1092
1093 do_action('fluent_community/lesson/before_deleted', $lesson);
1094
1095 $lesson->delete();
1096
1097 return [
1098 'message' => __('Lesson has been deleted successfully.', 'fluent-community'),
1099 ];
1100 }
1101
1102 public function duplicateLesson(Request $request, $courseId, $lessonId)
1103 {
1104 Course::findOrFail($courseId);
1105
1106 $lesson = CourseLesson::where('id', $lessonId)
1107 ->where('space_id', $courseId)
1108 ->firstOrFail();
1109
1110 $siblings = CourseLesson::where('parent_id', $lesson->parent_id)
1111 ->where('space_id', $courseId)
1112 ->orderBy('priority', 'ASC')
1113 ->orderBy('id', 'ASC')
1114 ->get();
1115
1116 $sourceIndex = $siblings->search(function ($sibling) use ($lesson) {
1117 return (int)$sibling->id === (int)$lesson->id;
1118 });
1119
1120 if ($sourceIndex === false) {
1121 $sourceIndex = $siblings->count() - 1;
1122 }
1123
1124 $existingTitles = $siblings->pluck('title')->toArray();
1125 $duplicateTitle = $lesson->title . ' (Copy)';
1126 if (in_array($duplicateTitle, $existingTitles, true)) {
1127 $counter = 2;
1128 while (in_array($lesson->title . ' (Copy ' . $counter . ')', $existingTitles, true)) {
1129 ++$counter;
1130 }
1131 $duplicateTitle = $lesson->title . ' (Copy ' . $counter . ')';
1132 }
1133
1134 /** @var CourseLesson $newLesson */
1135 $newLesson = $lesson->replicate();
1136 $newLesson->title = $duplicateTitle;
1137 $newLesson->slug = null;
1138 $newLesson->priority = $sourceIndex + 1;
1139 $newLesson->save();
1140
1141 $orderedIds = $siblings->pluck('id')->toArray();
1142 array_splice($orderedIds, $sourceIndex + 1, 0, [ $newLesson->id ]);
1143
1144 foreach ($orderedIds as $index => $siblingId) {
1145 CourseLesson::where('id', $siblingId)->update([ 'priority' => $index ]);
1146 }
1147
1148 $newLesson = CourseLesson::findOrFail($newLesson->id);
1149
1150 CourseHelper::copyLessonDocuments($lesson, $newLesson);
1151
1152 do_action('fluent_community/lesson/duplicated', $newLesson, $lesson);
1153
1154 return [
1155 'message' => __('Lesson has been duplicated successfully.', 'fluent-community'),
1156 'lesson' => $newLesson,
1157 ];
1158 }
1159
1160 public function getOtherUsers(Request $request, $courseId)
1161 {
1162 $selects = [
1163 'ID',
1164 'display_name',
1165 ];
1166
1167 if (current_user_can('list_users')) {
1168 $selects[] = 'user_email';
1169 }
1170
1171 $userQuery = User::select([ 'ID' ])
1172 ->whereDoesntHave('space_pivot', function ($q) use ($courseId) {
1173 $q->where('space_id', $courseId);
1174 })
1175 ->limit(100)
1176 ->searchBy($request->getSafe('search'));
1177
1178 if (is_multisite()) {
1179 global $wpdb;
1180 $blogId = get_current_blog_id();
1181 $blogPrefix = $wpdb->get_blog_prefix($blogId);
1182 $userQuery->whereHas('usermeta', function ($q) use ($blogPrefix) {
1183 $q->where('meta_key', $blogPrefix . 'capabilities');
1184 });
1185 }
1186
1187 $userIds = $userQuery->get()
1188 ->pluck('ID')
1189 ->toArray();
1190
1191 $users = User::select($selects)
1192 ->whereIn('ID', $userIds)
1193 ->paginate(100);
1194
1195 $data = [
1196 'users' => $users,
1197 ];
1198
1199 return apply_filters('fluent_community/admin_course_non_members_api_response', $data, $request->all());
1200 }
1201
1202 public function updateLinks(Request $request, $id)
1203 {
1204 $course = Course::findOrFail($id);
1205 $links = $request->get('links', []);
1206
1207 $links = array_map(function ($link) {
1208 return CustomSanitizer::santizeLinkItem($link);
1209 }, $links);
1210
1211 $settings = $course->settings;
1212 $settings['links'] = $links;
1213 $course->settings = $settings;
1214 $course->save();
1215
1216 return [
1217 'message' => __('Links have been updated for the course', 'fluent-community'),
1218 'links' => $links,
1219 ];
1220 }
1221
1222 public function getMetaSettings(Request $request, $id)
1223 {
1224 $course = Course::findOrFail($id);
1225 $metaSettings = apply_filters('fluent_community/course/meta_fields', [], $course, $request->all());
1226
1227 if (!$metaSettings) {
1228 return [
1229 'meta_settings' => null,
1230 ];
1231 }
1232
1233 return [
1234 'meta_settings' => $metaSettings,
1235 ];
1236 }
1237
1238 public function getOtherInstructors(Request $request, $courseId)
1239 {
1240 $search = $request->getSafe('search');
1241
1242 Course::findOrFail($courseId);
1243
1244 $selects = [
1245 'ID',
1246 'display_name',
1247 ];
1248
1249 if (current_user_can('list_users')) {
1250 $selects[] = 'user_email';
1251 }
1252
1253 $instructors = User::select($selects)
1254 ->limit(100)
1255 ->searchBy($search)
1256 ->get();
1257
1258 $data = [
1259 'instructors' => $instructors,
1260 ];
1261
1262 return apply_filters('fluent_community/admin_course_other_instructors_api_response', $data, $request->all());
1263 }
1264 }
1265