PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.0
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.0, at Modules/Course/Http/Controllers/CourseAdminController.php

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