PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / Modules / Course / Http / Controllers / CourseController.php

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

576 lines 21.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\SpaceUserPivot;
7 use FluentCommunity\App\Services\Helper;
8 use FluentCommunity\App\Services\LockscreenService;
9 use FluentCommunity\Framework\Http\Request\Request;
10 use FluentCommunity\Framework\Support\Arr;
11 use FluentCommunity\Modules\Course\Model\Course;
12 use FluentCommunity\Modules\Course\Model\CourseLesson;
13 use FluentCommunity\Modules\Course\Model\CourseTopic;
14 use FluentCommunity\App\Services\FeedsHelper;
15 use FluentCommunity\Modules\Course\Services\CourseHelper;
16 use FluentCommunity\Modules\Course\Services\LessonVideoGateService;
17
18 class CourseController extends Controller
19 {
20 public function getCourses(Request $request)
21 {
22 $user = $this->getUser();
23 $isCourseCreator = $user && $user->hasCourseCreatorAccess();
24 $enrolledIds = CourseHelper::getEnrolledCourseIds();
25
26 $search = $request->getSafe('search');
27 $topicSlug = $request->getSafe('topic_slug');
28 $isEnrolled = $request->getSafe('type') == 'enrolled';
29 $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'alphabetical');
30
31 $courses = Course::searchBy($search)
32 ->where(function ($q) use ($isCourseCreator, $isEnrolled, $user) {
33 if ($isCourseCreator && !$isEnrolled) {
34 $q->where('status', 'published')
35 ->orWhere('created_by', $user->ID);
36 } else {
37 $q->where('status', 'published');
38 }
39 })
40 ->byPostTopic($topicSlug)
41 ->where(function ($q) use ($enrolledIds) {
42 $q->whereIn('privacy', ['public', 'private']);
43 $q->orWhereIn('id', $enrolledIds);
44 })
45 ->when($isEnrolled, function ($q) use ($enrolledIds) {
46 $q->whereIn('id', $enrolledIds);
47 })
48 ->when($sortBy == 'alphabetical', function ($q) {
49 $q->orderBy('title', 'ASC');
50 })
51 ->when($sortBy == 'oldest', function ($q) {
52 $q->orderBy('created_at', 'ASC');
53 })
54 ->when($sortBy == 'latest', function ($q) {
55 $q->orderBy('created_at', 'DESC');
56 })
57 ->paginate();
58
59 foreach ($courses as $course) {
60 $course->isEnrolled = CourseHelper::isEnrolled($course->id);
61 if ($course->isEnrolled) {
62 $course->progress = CourseHelper::getCourseProgress($course->id);
63 }
64
65 if (!$course->cover_photo) {
66 $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
67 }
68
69 $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
70 $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
71 if (Arr::get($course->settings, 'hide_members_count') != 'yes') {
72 $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count();
73 } else {
74 $course->studentsCount = 0;
75 }
76
77 do_action_ref_array('fluent_community/course', [&$course]);
78 }
79
80 $data = [
81 'courses' => $courses,
82 'course_categories' => $request->get('with_categories') ? CourseHelper::getCourseCategories() : []
83 ];
84
85 return apply_filters('fluent_community/courses_api_response', $data, $request->all());
86 }
87
88 public function getCourse(Request $request, $courseId)
89 {
90 $user = $this->getUser();
91
92 $course = Course::findOrFail($courseId);
93
94 $isCourseCreator = $course->isCourseAdmin($user);
95
96 if ($course->status !== 'published' && !$isCourseCreator) {
97 return $this->sendError([
98 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community')
99 ]);
100 }
101
102 $intendtedLessonSlug = $request->get('intended_lesson_slug');
103
104 $data = $this->processCourse($course, $isCourseCreator, $intendtedLessonSlug);
105
106 return apply_filters('fluent_community/get_course_api_response', $data, $request->all());
107 }
108
109 public function getCourseBySlug(Request $request, $slug)
110 {
111 $user = $this->getUser();
112
113 $course = Course::where('slug', $slug)->firstOrFail();
114
115 $isCourseCreator = $course->isCourseAdmin($user);
116
117 if ($course->status !== 'published' && !$isCourseCreator) {
118 return $this->sendError([
119 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community')
120 ]);
121 }
122
123 $hideInstructorView = Arr::get($course->settings, 'hide_instructor_view') == 'yes';
124 $showStudentCount = Arr::get($course->settings, 'show_instructor_students_count') == 'yes';
125 if (!$hideInstructorView) {
126 $course->load(['creator']);
127 if ($course->creator) {
128 $creatorCourseIds = Course::where('created_by', $course->creator->user_id)->pluck('id');
129 $course->creator->total_courses = count($creatorCourseIds);
130 if ($showStudentCount) {
131 $course->creator->total_students = SpaceUserPivot::whereIn('space_id', $creatorCourseIds)->distinct('user_id')->count('user_id');
132 }
133 if ($course->creator->short_description) {
134 $course->creator->short_description_rendered = wp_kses_post(FeedsHelper::mdToHtml($course->creator->short_description));
135 }
136 }
137 }
138
139 $intendtedLessonSlug = $request->get('intended_lesson_slug');
140
141 $data = $this->processCourse($course, $isCourseCreator, $intendtedLessonSlug);
142
143 return apply_filters('fluent_community/course_api_response', $data, $request->all());
144 }
145
146 public function getLessonBySlug(Request $request, $courseSlug, $lessonSlug)
147 {
148 $user = $this->getUser();
149
150 $course = Course::where('slug', $courseSlug)->firstOrFail();
151
152 $isCourseCreator = $course->isCourseAdmin($user);
153
154 if ($course->status !== 'published' && !$isCourseCreator) {
155 return $this->sendError([
156 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community')
157 ]);
158 }
159
160 $lesson = CourseLesson::where('space_id', $course->id)
161 ->where('slug', $lessonSlug)
162 ->when(!$isCourseCreator, function ($q) {
163 $q->where('status', 'published');
164 })
165 ->with(['topic'])
166 ->firstOrFail();
167
168 if (!$lesson || !$lesson->topic) {
169 return $this->sendError([
170 'message' => __('Lesson not found. maybe the lesson is not published yet!', 'fluent-community')
171 ]);
172 }
173
174 $enrollment = CourseHelper::getCourseEnrollment($course->id);
175
176 if ($course->privacy == 'secret' && !$enrollment && !$isCourseCreator) {
177 return $this->sendError([
178 'message' => __('You must need to be invited to view this course', 'fluent-community')
179 ]);
180 }
181
182 $section = $lesson->topic;
183 $hasAcessSectionAcess = !!$enrollment;
184 $unlockDate = '';
185 $courseType = $course->getCourseType();
186 $hasPublicViewAccess = $course->privacy == 'public' && Arr::get($course->settings, 'public_lesson_view') == 'yes' && $courseType == 'self_paced';
187 if ($hasAcessSectionAcess) {
188 $unlockDate = CourseHelper::getSectionAccessDate($section, $courseType, $enrollment);
189 if ($unlockDate && strtotime($unlockDate) > current_time('timestamp')) {
190 $hasAcessSectionAcess = false;
191 } else {
192 $unlockDate = null;
193 }
194 }
195
196 $initialCanView = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess;
197 $accessInfo = CourseHelper::resolveLessonAccess($initialCanView, $lesson, $course, $this->getUser(), [
198 'enrollment' => $enrollment,
199 'is_admin' => $isCourseCreator,
200 'has_section_access' => $hasAcessSectionAcess,
201 'has_public_access' => $hasPublicViewAccess,
202 ]);
203 $canViewLesson = $accessInfo['can_view'];
204 $lockType = $accessInfo['lock_type'];
205
206 $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $user, [
207 'can_view' => $canViewLesson,
208 'parse_content' => $canViewLesson,
209 'is_locked' => !$canViewLesson && !$isCourseCreator,
210 'unlock_date' => $unlockDate,
211 'lock_type' => $lockType
212 ]);
213
214 $data = [
215 'lesson' => $formattedLesson
216 ];
217
218 return apply_filters('fluent_community/course_lesson_api_response', $data, $request->all());
219 }
220
221 private function processCourse(Course $course, $isCourseCreator, $intendtedLessonSlug = '')
222 {
223 $sections = CourseTopic::where('space_id', $course->id)
224 ->orderBy('priority', 'ASC')
225 ->orderBy('id', 'ASC')
226 ->with(['lessons' => function ($query) use ($isCourseCreator) {
227 if (!$isCourseCreator) {
228 $query->where('status', 'published');
229 }
230 }])
231 ->get();
232
233 $enrollment = CourseHelper::getCourseEnrollment($course->id);
234
235 $currentUser = Helper::getCurrentUser();
236
237 if ($course->privacy == 'private' && !$enrollment) {
238 $course->lockscreen_config = LockscreenService::getLockscreenConfig($course, null, true);
239 }
240
241 if ($course->privacy == 'secret' && !$enrollment && !$isCourseCreator) {
242 return $this->sendError([
243 'message' => __('You must need to be invited to view this course', 'fluent-community')
244 ]);
245 }
246
247 $course->is_course_admin = $isCourseCreator;
248 $course->can_self_enroll = $isCourseCreator || ($course->status === 'published' && $course->privacy === 'public');
249
250 $course = apply_filters('fluent_community/course/processed', $course, [
251 'is_enrolled' => !!$enrollment,
252 ]);
253
254 $courseSettings = $course->settings;
255 if (Arr::get($courseSettings, 'course_details')) {
256 $courseSettings['course_details_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($courseSettings['course_details']));
257 $course->settings = $courseSettings;
258 }
259
260 $courseType = $course->getCourseType();
261
262 $hasPublicViewAccess = $course->privacy == 'public' && Arr::get($course->settings, 'public_lesson_view') == 'yes' && $courseType == 'self_paced';
263
264 $formattedSections = [];
265
266 foreach ($sections as $section) {
267 if ($section->lessons->isEmpty()) {
268 continue;
269 }
270
271 $hasAcessSectionAcess = !!$enrollment;
272 $unlockDate = '';
273
274 if ($hasAcessSectionAcess) {
275 $unlockDate = CourseHelper::getSectionAccessDate($section, $courseType, $enrollment);
276 if ($unlockDate && strtotime($unlockDate) > current_time('timestamp')) {
277 $hasAcessSectionAcess = false;
278 } else {
279 $unlockDate = null;
280 }
281 }
282
283 $sectionCtx = [
284 'enrollment' => $enrollment,
285 'is_admin' => $isCourseCreator,
286 'has_section_access' => $hasAcessSectionAcess,
287 'has_public_access' => $hasPublicViewAccess,
288 ];
289
290 $formattedLessons = [];
291 foreach ($section->lessons as $lesson) {
292 $initialCanView = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess;
293 $accessInfo = CourseHelper::resolveLessonAccess($initialCanView, $lesson, $course, $this->getUser(), $sectionCtx);
294 $canViewLesson = $accessInfo['can_view'];
295 $lockType = $accessInfo['lock_type'];
296
297 $parseContent = $intendtedLessonSlug === $lesson->slug && $canViewLesson;
298
299 $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $currentUser, [
300 'can_view' => $canViewLesson,
301 'parse_content' => $parseContent,
302 'is_locked' => !$canViewLesson && !$isCourseCreator,
303 'unlock_date' => $unlockDate,
304 'lock_type' => $lockType
305 ]);
306
307 $isLazyLoad = !$parseContent;
308 if(!$canViewLesson && $lockType !== 'sequential') {
309 $isLazyLoad = false;
310 }
311
312 $formattedLesson['lazy_load'] = $isLazyLoad;
313
314 $formattedLessons[] = $formattedLesson;
315 }
316
317 $formattedSections[] = [
318 'id' => $section->id,
319 'title' => $section->title,
320 'lessons' => $formattedLessons,
321 'created_at' => $section->created_at,
322 'slug' => $section->slug,
323 'type' => 'section',
324 'is_locked' => !$hasAcessSectionAcess && !$isCourseCreator,
325 'unlock_date' => $unlockDate
326 ];
327 }
328
329 return [
330 'course' => $course,
331 'sections' => $formattedSections,
332 'track' => CourseHelper::getCourseProgressTrack($course->id)
333 ];
334 }
335
336 public function enrollCourse(Request $request, $courseId)
337 {
338 $user = $this->getUser();
339
340 if (!$user) {
341 return $this->sendError([
342 'message' => __('You need to login to enroll in this course.', 'fluent-community')
343 ]);
344 }
345
346 $course = Course::findOrFail($courseId);
347
348 $isCourseCreator = $course->isCourseAdmin($user);
349
350 if ($course->status !== 'published' && !$isCourseCreator) {
351 return $this->sendError([
352 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community')
353 ]);
354 }
355
356 if (!$isCourseCreator && $course->privacy != 'public') {
357 return $this->sendError([
358 'message' => __('You are not allowed to enroll in this course.', 'fluent-community')
359 ]);
360 }
361
362 $enrolled = CourseHelper::enrollCourse($course, get_current_user_id());
363
364 if (!$enrolled) {
365 return [
366 'message' => __('You are already enrolled in this course.', 'fluent-community'),
367 'track' => CourseHelper::getCourseProgressTrack($courseId)
368 ];
369 }
370
371 return [
372 'message' => __('You have been enrolled in this course.', 'fluent-community'),
373 'track' => CourseHelper::getCourseProgressTrack($courseId)
374 ];
375 }
376
377 public function updateCompletionLesson(Request $request, $courseId, $lessonId)
378 {
379 $course = Course::findOrFail($courseId);
380
381 if ($course->status != 'published') {
382 return $this->sendError([
383 'message' => __('Course is not in published state.', 'fluent-community')
384 ]);
385 }
386
387
388 if (!CourseHelper::isEnrolled($courseId)) {
389 return $this->sendError([
390 'message' => __('Please enroll this course first', 'fluent-community')
391 ]);
392 }
393
394 $lesson = CourseLesson::where('space_id', $courseId)
395 ->where('status', 'published')
396 ->where('id', $lessonId)
397 ->first();
398
399 if (!$lesson) {
400 return $this->sendError([
401 'message' => __('Lesson is not on published state.', 'fluent-community')
402 ]);
403 }
404
405 $state = $request->get('state');
406
407 if (!in_array($state, ['completed', 'incomplete'])) {
408 return $this->sendError([
409 'message' => __('Invalid state.', 'fluent-community')
410 ]);
411 }
412
413 $isAllowed = apply_filters('fluent_community/is_allowed_to_complete_lesson', true, $lesson, $state);
414
415 if (!$isAllowed) {
416 $errorData = [
417 'message' => __('You are not allowed to complete this lesson.', 'fluent-community')
418 ];
419
420 if (LessonVideoGateService::isCompletionBlockedFor($lesson, Helper::getCurrentUser())) {
421 $errorData['code'] = 'video_watch_required';
422 $errorData['threshold'] = LessonVideoGateService::getThreshold($lesson);
423 }
424
425 return $this->sendError($errorData);
426 }
427
428 CourseHelper::updateLessonCompletion($lesson, get_current_user_id(), $state);
429 $track = CourseHelper::getCourseProgressTrack($courseId);
430 $isCompleted = $track['progress'] == 100;
431
432 if ($isCompleted) {
433 CourseHelper::completeCourse($course, $lesson, get_current_user_id());
434 }
435
436 return [
437 'message' => __('Lesson completion state updated.', 'fluent-community'),
438 'track' => $track,
439 'is_completed' => $isCompleted
440 ];
441 }
442
443 public function markLessonVideoWatched(Request $request, $courseId, $lessonId)
444 {
445 $course = Course::findOrFail($courseId);
446
447 if ($course->status != 'published') {
448 return $this->sendError([
449 'message' => __('Course is not in published state.', 'fluent-community')
450 ]);
451 }
452
453 if (!CourseHelper::isEnrolled($courseId)) {
454 return $this->sendError([
455 'message' => __('Please enroll this course first', 'fluent-community')
456 ]);
457 }
458
459 $lesson = CourseLesson::where('space_id', $courseId)
460 ->where('status', 'published')
461 ->where('id', $lessonId)
462 ->first();
463
464 if (!$lesson) {
465 return $this->sendError([
466 'message' => __('Lesson is not on published state.', 'fluent-community')
467 ]);
468 }
469
470 if (!LessonVideoGateService::isGatedLesson($lesson)) {
471 return [
472 'message' => __('This lesson does not require video completion.', 'fluent-community'),
473 'is_gated' => false,
474 'watched' => false
475 ];
476 }
477
478 $watchedPercent = min(100, absint($request->get('watched_percent')));
479 $threshold = LessonVideoGateService::getThreshold($lesson);
480
481 if ($watchedPercent < $threshold) {
482 return $this->sendError([
483 /* translators: %d is the watched percentage required to complete the lesson */
484 'message' => sprintf(__('Please watch at least %d%% of the video.', 'fluent-community'), $threshold)
485 ]);
486 }
487
488 LessonVideoGateService::markWatched($lesson, get_current_user_id());
489
490 return [
491 'message' => __('Video watch progress has been recorded.', 'fluent-community'),
492 'is_gated' => true,
493 'watched' => true
494 ];
495 }
496
497 public function resetMyProgress(Request $request, $courseId)
498 {
499 $course = Course::findOrFail($courseId);
500
501 if ($course->status !== 'published') {
502 return $this->sendError([
503 'message' => __('Course is not in published state.', 'fluent-community')
504 ]);
505 }
506
507 if (!CourseHelper::isEnrolled($courseId)) {
508 return $this->sendError([
509 'message' => __('Please enroll this course first', 'fluent-community')
510 ]);
511 }
512
513 CourseHelper::resetCourseProgress($courseId, get_current_user_id());
514
515 return [
516 'message' => __('Your course progress has been reset.', 'fluent-community'),
517 'track' => [
518 'completed_lessons' => [],
519 'isEnrolled' => true,
520 'progress' => 0
521 ]
522 ];
523 }
524
525 public function getAllCourses(Request $request)
526 {
527 $user = $this->getUser();
528 $isCourseCreator = $user && $user->hasCourseCreatorAccess();
529
530 $courses = Course::where(function ($q) use ($isCourseCreator, $user) {
531 if ($isCourseCreator) {
532 $q->where('status', 'published')
533 ->orWhere('created_by', $user->ID);
534 } else {
535 $q->where('status', 'published');
536 }
537 })
538 ->with(['creator'])
539 ->paginate();
540
541 $formattedCourses = [];
542 foreach ($courses as $course) {
543 $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
544 $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
545
546 if (Arr::get($course->settings, 'hide_members_count') != 'yes') {
547 $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count();
548 } else {
549 $course->studentsCount = 0;
550 }
551
552 $showInstructorView = Arr::get($course->settings, 'hide_instructor_view') != 'yes';
553 $showStudentCount = Arr::get($course->settings, 'show_instructor_students_count') == 'yes';
554 if ($showInstructorView) {
555 if ($course->creator) {
556 $creatorCourseIds = Course::where('created_by', $course->creator->user_id)->pluck('id');
557 $course->creator->total_courses = count($creatorCourseIds);
558 if ($showStudentCount) {
559 $course->creator->total_students = SpaceUserPivot::whereIn('space_id', $creatorCourseIds)->distinct('user_id')->count('user_id');
560 }
561 if ($course->creator->short_description) {
562 $course->creator->short_description_rendered = wp_kses_post(FeedsHelper::mdToHtml($course->creator->short_description));
563 }
564 }
565 }
566
567 $formattedCourses[] = $this->processCourse($course, $course->isCourseAdmin($user));
568 }
569
570 return apply_filters('fluent_community/all_courses_api_response', [
571 'courses' => $formattedCourses,
572 'total' => $courses->total()
573 ], $request->all());
574 }
575 }
576