PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.4.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.4.01
2.11.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 All 78 releases
fluent-community / Modules / Course / Http / Controllers / CourseAdminController.php

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

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