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

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