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

770 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Modules\Course\Http\Controllers;
4
5 use FluentCommunity\App\Http\Controllers\Controller;
6 use FluentCommunity\App\Models\BaseSpace;
7 use FluentCommunity\App\Models\Comment;
8 use FluentCommunity\App\Models\Feed;
9 use FluentCommunity\App\Models\Reaction;
10 use FluentCommunity\App\Models\User;
11 use FluentCommunity\App\Models\XProfile;
12 use FluentCommunity\App\Services\CustomSanitizer;
13 use FluentCommunity\App\Services\Helper;
14 use FluentCommunity\App\Services\LockscreenService;
15 use FluentCommunity\App\Services\ProfileHelper;
16 use FluentCommunity\App\Services\FeedsHelper;
17 use FluentCommunity\Framework\Http\Request\Request;
18 use FluentCommunity\Framework\Support\Arr;
19 use FluentCommunity\App\Models\SpaceUserPivot;
20 use FluentCommunity\Modules\Course\Model\Course;
21 use FluentCommunity\Modules\Course\Model\CourseLesson;
22 use FluentCommunity\Modules\Course\Model\CourseTopic;
23 use FluentCommunity\Modules\Course\Services\CourseHelper;
24
25 class CourseAdminController extends Controller
26 {
27 public function getCourses(Request $request)
28 {
29 $courses = Course::searchBy($request->getSafe('search'))
30 ->orderBy('id', 'DESC')
31 ->with(['owner'])
32 ->paginate();
33
34 foreach ($courses as $course) {
35 $course->students_count = $course->students()->count();
36 if (!$course->cover_photo) {
37 $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
38 }
39 $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
40 $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
41 }
42
43 return [
44 'courses' => $courses
45 ];
46 }
47
48 public function createCourse(Request $request)
49 {
50 $this->validate($request->all(), [
51 'title' => 'required',
52 'description' => 'required',
53 'privacy' => 'required|in:public,private,secret',
54 'course_type' => 'required|in:self_paced,structured,scheduled',
55 'parent_id' => 'nullable|exists:fcom_spaces,id'
56 ]);
57
58 $parentId = $request->get('parent_id');
59
60 $courseData = [
61 'parent_id' => $request->get('parent_id'),
62 'title' => $request->getSafe('title', 'sanitize_text_field'),
63 'privacy' => $request->get('privacy'),
64 'description' => wp_kses_post($request->get('description')),
65 'status' => 'draft',
66 'settings' => [
67 'course_type' => $request->get('course_type'),
68 'emoji' => CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', '')),
69 'shape_svg' => CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', '')),
70 ],
71 'serial' => BaseSpace::where('parent_id', $parentId)->max('serial') + 1
72 ];
73
74 $slug = $request->get('slug');
75
76 if (!$slug) {
77 $slug = sanitize_title($courseData['title']);
78 }
79
80 if ($slug) {
81 $slug = sanitize_title($slug);
82 $exist = Course::where('slug', $slug)
83 ->exists();
84
85 if ($exist) {
86 $slug = $slug . '-' . time();
87 }
88
89 $courseData['slug'] = $slug;
90 }
91
92 do_action('fluent_community/course/before_create', $courseData);
93
94 $course = Course::create($courseData);
95
96 $imageTypes = ['cover_photo', 'logo'];
97
98 $metaData = [];
99 foreach ($imageTypes as $type) {
100 if (!empty($request->get($type))) {
101 $media = Helper::getMediaFromUrl($request->get($type));
102 if (!$media || $media->is_active) {
103 continue;
104 }
105 $metaData[$type] = $media->public_url;
106 $media->update([
107 'is_active' => true,
108 'user_id' => get_current_user_id(),
109 'sub_object_id' => $course->id,
110 'object_source' => 'space_' . $type
111 ]);
112 }
113 }
114
115 if ($metaData) {
116 $course->fill($metaData);
117 $course->save();
118 }
119
120 do_action('fluent_community/course/created', $course);
121
122 return [
123 'course' => $course
124 ];
125 }
126
127 public function findCourse(Request $request, $courseId)
128 {
129 $course = Course::where('id', $courseId)
130 ->with(['owner'])
131 ->firstOrFail();
132
133 $course->students_count = $course->students()->count();
134 $course->course_type = $course->settings['course_type'];
135 $course->lockscreen = $course->getLockscreen();
136
137 $course->category_ids = $course->categories->pluck('id')->toArray();
138
139 if ($course->students_count) {
140 $course->completed_students = $course->getCompletedStrundesCount();
141 $course->overAllProgress = CourseHelper::overallCourseProgressAverage($course);
142 }
143
144 unset($course->categories);
145
146 return [
147 'course' => $course
148 ];
149 }
150
151 public function updateCourse(Request $request, $courseId)
152 {
153 $this->validate($request->all(), [
154 'title' => 'required',
155 'description' => 'required',
156 'privacy' => 'required|in:public,private,secret',
157 'status' => 'required|in:draft,published,archived',
158 'course_type' => 'required|in:self_paced,structured,scheduled',
159 'parent_id' => 'required|exists:fcom_spaces,id'
160 ]);
161
162 $course = Course::findOrFail($courseId);
163
164 $courseData = [
165 'title' => $request->getSafe('title', 'sanitize_text_field'),
166 'privacy' => $request->get('privacy'),
167 'description' => wp_kses_post($request->get('description')),
168 'status' => $request->get('status'),
169 'cover_photo' => $request->getSafe('cover_photo', 'sanitize_url'),
170 'parent_id' => $request->get('parent_id'),
171 ];
172
173 $imageTypes = ['cover_photo', 'logo'];
174
175 foreach ($imageTypes as $type) {
176 if (!empty($request->get($type))) {
177 $media = Helper::getMediaFromUrl($request->get($type));
178 if (!$media || $media->is_active) {
179 continue;
180 }
181 $courseData[$type] = $media->public_url;
182 $media->update([
183 'is_active' => true,
184 'user_id' => get_current_user_id(),
185 'sub_object_id' => $course->id,
186 'object_source' => 'space_' . $type
187 ]);
188 }
189 }
190
191 $existingSettings = $course->settings;
192 $existingSettings['course_type'] = $request->get('course_type');
193 $existingSettings['custom_lock_screen'] = $request->get('settings.custom_lock_screen') === 'yes' ? 'yes' : 'no';
194 $existingSettings['emoji'] = CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', ''));
195 $existingSettings['shape_svg'] = CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', ''));
196
197 $courseData['settings'] = $existingSettings;
198
199
200 $previousStatus = $course->status;
201
202 $course->fill($courseData);
203 $dirtyFields = $course->getDirty();
204
205 if ($dirtyFields) {
206 $course->save();
207 do_action('fluent_community/course/updated', $course, $dirtyFields);
208
209 if($previousStatus != 'published' && $course->status == 'published') {
210 do_action('fluent_community/course/published', $course);
211 }
212
213 }
214
215 $course->syncCategories($request->get('category_ids', []));
216
217 return [
218 'message' => __('Course has been updated successfully.', 'fluent-community'),
219 'course' => $course
220 ];
221 }
222
223 public function deleteCourse(Request $request, $courseId)
224 {
225 $course = Course::findOrFail($courseId);
226
227 do_action('fluent_community/course/before_delete', $course);
228
229 // Let's remove the reactions
230 Reaction::query()->whereHas('feed', function ($q) use ($course) {
231 $q->where('space_id', $course->id);
232 })->delete();
233
234 Comment::whereHas('post', function ($q) use ($course) {
235 $q->where('space_id', $course->id);
236 })->delete();
237
238 Feed::withoutGlobalScopes()->where('space_id', $course->id)->delete();
239
240 // Let's delete the student enrollments
241 SpaceUserPivot::where('space_id', $course->id)
242 ->delete();
243
244 $courseId = $course->id;
245 $course->delete();
246
247 do_action('fluent_community/course/deleted', $courseId);
248
249 return [
250 'message' => __('Course has been deleted successfully along with all the associated data', 'fluent-community')
251 ];
252 }
253
254 public function getCourseComments(Request $request, $courseId)
255 {
256 Course::findOrFail($courseId);
257
258 $comments = Comment::whereHas('post', function ($q) use ($courseId) {
259 return $q->where('space_id', $courseId);
260 })
261 ->orderBy('id', 'DESC')
262 ->with([
263 'post' => function ($q) {
264 return $q->select(['id', 'title', 'slug']);
265 },
266 'xprofile' => function ($q) {
267 $q->select(ProfileHelper::getXProfilePublicFields());
268 }
269 ])
270 ->paginate();
271
272 foreach ($comments as $comment) {
273 if ($comment->user) {
274 $comment->user->makeHidden(['user_email']);
275 }
276 $likedIds = FeedsHelper::getLikedIdsByUserFeedId($comment->post_id, get_current_user_id());
277 if ($likedIds && in_array($comment->id, $likedIds)) {
278 $comment->liked = 1;
279 }
280 }
281
282 return [
283 'comments' => $comments
284 ];
285 }
286
287 public function getCourseStudents(Request $request, $courseId)
288 {
289 Course::findOrFail($courseId);
290
291 $search = $request->getSafe('search', 'sanitize_text_field');
292
293 $students = XProfile::whereHas('space_pivot', function ($q) use ($courseId) {
294 return $q->where('space_id', $courseId)
295 ->where('role', 'student');
296 })
297 ->searchBy($search)
298 ->with([
299 'space_pivot' => function ($q) use ($courseId) {
300 return $q->where('space_id', $courseId);
301 },
302 ])
303 ->select(ProfileHelper::getXProfilePublicFields())
304 ->paginate();
305
306 foreach ($students as $student) {
307 $student->progress = CourseHelper::getCourseProgress($courseId, $student->user_id);
308 }
309
310 return [
311 'students' => $students
312 ];
313 }
314
315 public function addStudent(Request $request, $courseId)
316 {
317 $course = Course::findOrFail($courseId);
318
319 $this->validate($request->all(), [
320 'user_id' => 'required|exists:users,ID'
321 ]);
322
323 $userId = (int)$request->get('user_id');
324 $targetUser = User::findOrFail($userId);
325 $xprofile = $targetUser->syncXProfile();
326
327 if ($xprofile && $xprofile->status != 'active') {
328 return $this->sendError([
329 'message' => __('Selected user is not active', 'fluent-community')
330 ]);
331 }
332
333 $admin = User::find(get_current_user_id());
334 $admin->verifySpacePermission('can_add_member', $course);
335 $enrolled = CourseHelper::enrollCourse($course, $userId, 'by_admin');
336
337 if (!$enrolled) {
338 return $this->sendError([
339 'message' => __('User is already added to this course.', 'fluent-community')
340 ]);
341 }
342
343 return [
344 'message' => __('User has been added to this course', 'fluent-community')
345 ];
346 }
347
348 public function removeStudent(Request $request, $courseId, $studentId)
349 {
350 $course = Course::findOrFail($courseId);
351
352 $admin = User::find(get_current_user_id());
353
354 $admin->verifySpacePermission('can_remove_member', $course);
355
356 $student = SpaceUserPivot::bySpace($course->id)
357 ->byUser($studentId)
358 ->first();
359
360 if (!$student) {
361 return $this->sendError([
362 'message' => __('Selected user is not a student of this course', 'fluent-community')
363 ]);
364 }
365
366 Helper::removeFromSpace($course, $studentId, 'by_admin');
367
368 return [
369 'message' => __('Student has been removed from this course', 'fluent-community')
370 ];
371 }
372
373 public function getSections(Request $request, $courseId)
374 {
375 $course = Course::findOrFail($courseId);
376
377 $sectionsQuery = CourseTopic::where('space_id', $courseId)
378 ->orderBy('priority', 'ASC');
379
380 if (in_array('only_published', $request->get('conditions', []))) {
381 $sectionsQuery->where('status', 'published')
382 ->with(['lessons' => function ($q) {
383 $q->where('status', 'published');
384 }]);
385 }
386
387 if (empty($request->get('conditions', []))) {
388 $sectionsQuery->with(['lessons']);
389 }
390
391 $sections = $sectionsQuery->get();
392
393 $data = [
394 'sections' => $sections
395 ];
396
397 if ($request->get('with_lock_screen')) {
398 $data['lockscreen'] = LockscreenService::getLockscreenSettings($course);
399 }
400
401 return $data;
402 }
403
404 public function getSection(Request $request, $courseId, $topicId)
405 {
406 $topic = CourseTopic::where('space_id', $courseId)
407 ->whereHas('course', function($query) use ($courseId) {
408 $query->where('id', $courseId);
409 })
410 ->where('id', $topicId)
411 ->with(['lessons'])
412 ->firstOrFail();
413
414 return [
415 'topic' => $topic
416 ];
417 }
418
419 public function resetSectionIndexes(Request $request, $courseId)
420 {
421 $indexes = $request->get('indexes', []);
422
423 $sections = CourseTopic::where('space_id', $courseId)
424 ->whereIn('id', array_keys($indexes))
425 ->get();
426
427 foreach ($sections as $section) {
428 if (!isset($indexes[$section->id])) continue;
429 $section->priority = $indexes[$section->id];
430 $section->save();
431 }
432
433 return [
434 'sections' => $sections,
435 'message' => __('Section indexes have been updated successfully.', 'fluent-community')
436 ];
437 }
438
439 public function resetLessonIndexes(Request $request, $courseId, $sectionId)
440 {
441 $indexes = $request->get('indexes', []);
442
443 $lessons = CourseLesson::where('space_id', $courseId)
444 ->where('parent_id', $sectionId)
445 ->whereIn('id', array_keys($indexes))
446 ->get();
447
448 foreach ($lessons as $lesson) {
449 if (!isset($indexes[$lesson->id])) continue;
450 $lesson->priority = $indexes[$lesson->id];
451 $lesson->save();
452 }
453
454 return [
455 'lessons' => $lessons,
456 'message' => __('Lesson indexes have been updated successfully.', 'fluent-community')
457 ];
458 }
459
460 public function createSection(Request $request, $courseId)
461 {
462 $this->validate($request->all(), [
463 'title' => 'required'
464 ]);
465
466 $sectionData = [
467 'title' => $request->getSafe('title'),
468 'space_id' => $courseId,
469 'status' => 'published'
470 ];
471
472 Course::findOrFail($courseId);
473
474 $section = CourseTopic::create($sectionData);
475
476 $section->load('lessons');
477
478 return [
479 'message' => __('Topic has been created successfully.', 'fluent-community'),
480 'section' => $section
481 ];
482 }
483
484 public function updateSection(Request $request, $courseId, $tipicId)
485 {
486 $this->validate($request->all(), [
487 'title' => 'required',
488 'status' => 'required|in:draft,published,archived'
489 ]);
490
491 $course = Course::findOrFail($courseId);
492
493 $topic = CourseTopic::where('space_id', $courseId)
494 ->where('id', $tipicId)
495 ->firstOrFail();
496
497
498 $topicData = [
499 'title' => $request->getSafe('title'),
500 'status' => $request->get('status')
501 ];
502
503 $course->update($topicData);
504
505 return [
506 'message' => __('Topic has been updated successfully.', 'fluent-community'),
507 'topic' => $topic
508 ];
509 }
510
511 public function patchSection(Request $request, $courseId, $tipicId)
512 {
513 $course = Course::findOrFail($courseId);
514
515 $topic = CourseTopic::where('space_id', $courseId)
516 ->where('id', $tipicId)
517 ->firstOrFail();
518
519 $acceptedFields = ['title', 'status'];
520
521 if ($course->getCourseType() == 'scheduled') {
522 $acceptedFields[] = 'scheduled_at';
523 } else if ($course->getCourseType() == 'structured') {
524 $acceptedFields[] = 'reactions_count';
525 }
526
527
528 $topicData = $request->only($acceptedFields);
529
530 if (!empty($topicData['scheduled_at'])) {
531 $topic->reactions_count = 0;
532 } else if (isset($topicData['reactions_count'])) {
533 $topic->scheduled_at = null;
534 $topic->reactions_count = $topicData['reactions_count'];
535 }
536
537 $topicData = array_filter($topicData);
538 $topic->fill($topicData);
539 $topic->save();
540
541 return [
542 'message' => __('Topic has been updated successfully.', 'fluent-community'),
543 'topic' => $topic
544 ];
545 }
546
547 public function deleteSection(Request $request, $courseId, $sectionId)
548 {
549 $topic = CourseTopic::where([
550 'id' => $sectionId,
551 'space_id' => $courseId
552 ])->firstOrFail();
553
554 $topic->delete();
555
556 CourseLesson::where([
557 'parent_id' => $sectionId,
558 'space_id' => $courseId
559 ])->delete();
560
561 return [
562 'message' => __('Section has been deleted successfully.', 'fluent-community')
563 ];
564 }
565
566 public function getLessons(Request $request, $courseId)
567 {
568 Course::findOrFail($courseId);
569
570 $lessons = CourseLesson::where('space_id', $courseId)
571 ->orderBy('priority', 'ASC');
572
573 $topicId = (int)$request->get('topic_id');
574
575 if ($topicId) {
576 $lessons = $lessons->where('parent_id', $topicId);
577 }
578
579 $lessons = $lessons->get();
580
581 return [
582 'lessons' => $lessons
583 ];
584 }
585
586 public function getLesson(Request $request, $courseId, $lessonId)
587 {
588 $lesson = CourseLesson::whereHas('course', function($query) use ($courseId) {
589 $query->where('id', $courseId);
590 })
591 ->where('id', $lessonId)
592 ->with(['topic', 'course'])
593 ->firstOrFail();
594
595 return [
596 'lesson' => $lesson
597 ];
598 }
599
600 public function createLesson(Request $request, $courseId)
601 {
602 $this->validate($request->all(), [
603 'title' => 'required',
604 'section_id' => 'required'
605 ]);
606
607 $sectionId = (int)$request->get('section_id');
608
609 $topic = CourseTopic::whereHas('course', function($query) use ($courseId) {
610 $query->where('id', $courseId);
611 })
612 ->where('id', $sectionId)
613 ->firstOrFail();
614
615 $lessonData = [
616 'title' => $request->getSafe('title'),
617 'parent_id' => $topic->id,
618 'space_id' => $courseId,
619 'status' => 'draft'
620 ];
621
622 $lesson = CourseLesson::create($lessonData);
623
624 $lesson = CourseLesson::findOrFail($lesson->id);
625
626 return [
627 'message' => __('Lesson has been created successfully.', 'fluent-community'),
628 'lesson' => $lesson
629 ];
630 }
631
632 public function updateLesson(Request $request, $courseId, $lessionId)
633 {
634 $lessonData = $request->get('lesson');
635
636 $this->validate($lessonData, [
637 'title' => 'required',
638 'parent_id' => 'required',
639 'status' => 'required|in:draft,published,archived'
640 ]);
641
642 CourseTopic::whereHas('course', function($query) use ($courseId) {
643 $query->where('id', $courseId);
644 })
645 ->where('id', $lessonData['parent_id'])
646 ->firstOrFail();
647
648 $lesson = CourseLesson::where('id', $lessionId)
649 ->where('space_id', $courseId)
650 ->firstOrFail();
651
652 $previousStatus = $lesson->status;
653
654 $updateData = array_filter([
655 'title' => sanitize_text_field(Arr::get($lessonData, 'title')),
656 'message' => wp_kses_post(Arr::get($lessonData, 'message')),
657 'status' => Arr::get($lessonData, 'status'),
658 'meta' => CourseHelper::sanitizeLessonMeta(Arr::get($lessonData, 'meta', []))
659 ]);
660
661 $lesson->fill($updateData);
662 $dirtyFields = $lesson->getDirty();
663
664 if($dirtyFields) {
665 $lesson->save();
666 $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
667 do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
668 }
669
670 return [
671 'message' => __('Lesson has been updated successfully.', 'fluent-community'),
672 'lesson' => $lesson
673 ];
674 }
675
676 public function patchLesson(Request $request, $courseId, $lessionId)
677 {
678 $lesson = CourseLesson::whereHas('course', function($query) use ($courseId) {
679 $query->where('id', $courseId);
680 })
681 ->where('id', $lessionId)
682 ->firstOrFail();
683
684 $acceptedFields = ['title', 'status'];
685
686 $lessonData = array_filter($request->only($acceptedFields));
687
688 $lesson->fill($lessonData);
689 $lesson->save();
690
691 return [
692 'message' => __('Lesson has been updated successfully.', 'fluent-community'),
693 'lesson' => $lesson
694 ];
695 }
696
697 public function deleteLesson(Request $request, $courseId, $lessionId)
698 {
699 $lesson = CourseLesson::whereHas('course', function($query) use ($courseId) {
700 $query->where('id', $courseId);
701 })
702 ->where('id', $lessionId)
703 ->firstOrFail();
704
705 $lesson->delete();
706
707 return [
708 'message' => __('Lesson has been deleted successfully.', 'fluent-community')
709 ];
710 }
711
712 public function updateLockscreenSettings(Request $request, $courseId)
713 {
714 $course = Course::findOrFail($courseId);
715
716 $settingFields = $request->get('lockscreen', []);
717
718 $formattedFields = LockscreenService::formatLockscreenFields($settingFields);
719
720 $course->setLockscreen($formattedFields);
721
722 return [
723 'message' => __('Lockscreen settings have been updated successfully.', 'fluent-community'),
724 'course' => $course
725 ];
726 }
727
728 public function getOtherUsers(Request $request)
729 {
730 $this->validate($request->all(), [
731 'course_id' => 'required|exists:fcom_spaces,id'
732 ]);
733
734 $courseId = (int)$request->get('course_id');
735
736 $search = $request->getSafe('search');
737
738 $users = User::whereDoesntHave('courses', function ($q) use ($courseId) {
739 $q->where('space_id', $courseId);
740 })
741 ->select(['ID', 'display_name'])
742 ->searchBy($search)
743 ->paginate();
744
745 return [
746 'users' => $users
747 ];
748 }
749
750 public function updateLinks(Request $request, $id)
751 {
752 $course = Course::findOrFail($id);
753 $links = $request->get('links', []);
754
755 $links = array_map(function ($link) {
756 return CustomSanitizer::santizeLinkItem($link);
757 }, $links);
758
759 $settings = $course->settings;
760 $settings['links'] = $links;
761 $course->settings = $settings;
762 $course->save();
763
764 return [
765 'message' => __('Links have been updated for the course', 'fluent-community'),
766 'links' => $links
767 ];
768 }
769 }
770