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

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