PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / trunk
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses vtrunk
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 / CourseController.php

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

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