getUser(); $isCourseCreator = $user && $user->hasCourseCreatorAccess(); $enrolledIds = CourseHelper::getEnrolledCourseIds(); $search = $request->getSafe('search'); $topicSlug = $request->getSafe('topic_slug'); $isEnrolled = $request->getSafe('type') == 'enrolled'; $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'alphabetical'); $courses = Course::searchBy($search) ->where(function ($q) use ($isCourseCreator, $isEnrolled, $user) { if ($isCourseCreator && !$isEnrolled) { $q->where('status', 'published') ->orWhere('created_by', $user->ID); } else { $q->where('status', 'published'); } }) ->byPostTopic($topicSlug) ->where(function ($q) use ($enrolledIds) { $q->whereIn('privacy', ['public', 'private']); $q->orWhereIn('id', $enrolledIds); }) ->when($isEnrolled, function ($q) use ($enrolledIds) { $q->whereIn('id', $enrolledIds); }) ->when($sortBy == 'alphabetical', function ($q) { $q->orderBy('title', 'ASC'); }) ->when($sortBy == 'oldest', function ($q) { $q->orderBy('created_at', 'ASC'); }) ->when($sortBy == 'latest', function ($q) { $q->orderBy('created_at', 'DESC'); }) ->paginate(); foreach ($courses as $course) { $course->isEnrolled = CourseHelper::isEnrolled($course->id); if ($course->isEnrolled) { $course->progress = CourseHelper::getCourseProgress($course->id); } if (!$course->cover_photo) { $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg'; } $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count(); $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count(); if (Arr::get($course->settings, 'hide_members_count') != 'yes') { $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count(); } else { $course->studentsCount = 0; } do_action_ref_array('fluent_community/course', [&$course]); } $data = [ 'courses' => $courses, 'course_categories' => $request->get('with_categories') ? CourseHelper::getCourseCategories() : [] ]; return apply_filters('fluent_community/courses_api_response', $data, $request->all()); } public function getCourse(Request $request, $courseId) { $user = $this->getUser(); $course = Course::findOrFail($courseId); /** @var Course $course */ $isCourseCreator = $course->isCourseAdmin($user); if ($course->status !== 'published' && !$isCourseCreator) { return $this->sendError([ 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community') ]); } $intendtedLessonSlug = $request->get('intended_lesson_slug'); $data = $this->processCourse($course, $isCourseCreator, $intendtedLessonSlug); return apply_filters('fluent_community/get_course_api_response', $data, $request->all()); } public function getCourseBySlug(Request $request, $slug) { $user = $this->getUser(); $course = Course::where('slug', $slug)->firstOrFail(); /** @var Course $course */ $isCourseCreator = $course->isCourseAdmin($user); if ($course->status !== 'published' && !$isCourseCreator) { return $this->sendError([ 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community') ]); } $hideInstructorView = Arr::get($course->settings, 'hide_instructor_view') == 'yes'; $showStudentCount = Arr::get($course->settings, 'show_instructor_students_count') == 'yes'; if (!$hideInstructorView) { $course->load(['creator']); if ($course->creator) { $creatorCourseIds = Course::where('created_by', $course->creator->user_id)->pluck('id'); $course->creator->total_courses = count($creatorCourseIds); if ($showStudentCount) { $course->creator->total_students = SpaceUserPivot::whereIn('space_id', $creatorCourseIds)->distinct('user_id')->count('user_id'); } if ($course->creator->short_description) { $course->creator->short_description_rendered = wp_kses_post(FeedsHelper::mdToHtml($course->creator->short_description)); } } } $intendtedLessonSlug = $request->get('intended_lesson_slug'); $data = $this->processCourse($course, $isCourseCreator, $intendtedLessonSlug); return apply_filters('fluent_community/course_api_response', $data, $request->all()); } public function getLessonBySlug(Request $request, $courseSlug, $lessonSlug) { $user = $this->getUser(); /** @var Course $course */ $course = Course::where('slug', $courseSlug)->firstOrFail(); $isCourseCreator = $course->isCourseAdmin($user); if ($course->status !== 'published' && !$isCourseCreator) { return $this->sendError([ 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community') ]); } $lesson = CourseLesson::where('space_id', $course->id) ->where('slug', $lessonSlug) ->when(!$isCourseCreator, function ($q) { $q->where('status', 'published'); }) ->with(['topic']) ->firstOrFail(); if (!$lesson || !$lesson->topic) { return $this->sendError([ 'message' => __('Lesson not found. maybe the lesson is not published yet!', 'fluent-community') ]); } $enrollment = CourseHelper::getCourseEnrollment($course->id); if ($course->privacy == 'secret' && !$enrollment && !$isCourseCreator) { return $this->sendError([ 'message' => __('You must need to be invited to view this course', 'fluent-community') ]); } $section = $lesson->topic; $hasAcessSectionAcess = !!$enrollment; $unlockDate = ''; $courseType = $course->getCourseType(); $hasPublicViewAccess = $course->privacy == 'public' && Arr::get($course->settings, 'public_lesson_view') == 'yes' && $courseType == 'self_paced'; if ($hasAcessSectionAcess) { $unlockDate = CourseHelper::getSectionAccessDate($section, $courseType, $enrollment); if ($unlockDate && strtotime($unlockDate) > current_time('timestamp')) { $hasAcessSectionAcess = false; } else { $unlockDate = null; } } $initialCanView = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess; $accessInfo = CourseHelper::resolveLessonAccess($initialCanView, $lesson, $course, $this->getUser(), [ 'enrollment' => $enrollment, 'is_admin' => $isCourseCreator, 'has_section_access' => $hasAcessSectionAcess, 'has_public_access' => $hasPublicViewAccess, ]); $canViewLesson = $accessInfo['can_view']; $lockType = $accessInfo['lock_type']; $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $user, [ 'can_view' => $canViewLesson, 'parse_content' => $canViewLesson, 'is_locked' => !$canViewLesson && !$isCourseCreator, 'unlock_date' => $unlockDate, 'lock_type' => $lockType ]); $data = [ 'lesson' => $formattedLesson ]; return apply_filters('fluent_community/course_lesson_api_response', $data, $request->all()); } private function processCourse(Course $course, $isCourseCreator, $intendtedLessonSlug = '') { $sections = CourseTopic::where('space_id', $course->id) ->orderBy('priority', 'ASC') ->orderBy('id', 'ASC') ->with(['lessons' => function ($query) use ($isCourseCreator) { if (!$isCourseCreator) { $query->where('status', 'published'); } }]) ->get(); $enrollment = CourseHelper::getCourseEnrollment($course->id); $currentUser = Helper::getCurrentUser(); if ($course->privacy == 'private' && !$enrollment) { $course->lockscreen_config = LockscreenService::getLockscreenConfig($course, null, true); } if ($course->privacy == 'secret' && !$enrollment && !$isCourseCreator) { return $this->sendError([ 'message' => __('You must need to be invited to view this course', 'fluent-community') ]); } $course->is_course_admin = $isCourseCreator; $course->can_self_enroll = $isCourseCreator || ($course->status === 'published' && $course->privacy === 'public'); $course = apply_filters('fluent_community/course/processed', $course, [ 'is_enrolled' => !!$enrollment, ]); $courseSettings = $course->settings; if (Arr::get($courseSettings, 'course_details')) { $courseSettings['course_details_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($courseSettings['course_details'])); $course->settings = $courseSettings; } $courseType = $course->getCourseType(); $hasPublicViewAccess = $course->privacy == 'public' && Arr::get($course->settings, 'public_lesson_view') == 'yes' && $courseType == 'self_paced'; $formattedSections = []; foreach ($sections as $section) { if ($section->lessons->isEmpty()) { continue; } $hasAcessSectionAcess = !!$enrollment; $unlockDate = ''; if ($hasAcessSectionAcess) { $unlockDate = CourseHelper::getSectionAccessDate($section, $courseType, $enrollment); if ($unlockDate && strtotime($unlockDate) > current_time('timestamp')) { $hasAcessSectionAcess = false; } else { $unlockDate = null; } } $sectionCtx = [ 'enrollment' => $enrollment, 'is_admin' => $isCourseCreator, 'has_section_access' => $hasAcessSectionAcess, 'has_public_access' => $hasPublicViewAccess, ]; $formattedLessons = []; foreach ($section->lessons as $lesson) { $initialCanView = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess; $accessInfo = CourseHelper::resolveLessonAccess($initialCanView, $lesson, $course, $this->getUser(), $sectionCtx); $canViewLesson = $accessInfo['can_view']; $lockType = $accessInfo['lock_type']; $parseContent = $intendtedLessonSlug === $lesson->slug && $canViewLesson; $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $currentUser, [ 'can_view' => $canViewLesson, 'parse_content' => $parseContent, 'is_locked' => !$canViewLesson && !$isCourseCreator, 'unlock_date' => $unlockDate, 'lock_type' => $lockType ]); $isLazyLoad = !$parseContent; if(!$canViewLesson && $lockType !== 'sequential') { $isLazyLoad = false; } $formattedLesson['lazy_load'] = $isLazyLoad; $formattedLessons[] = $formattedLesson; } $formattedSections[] = [ 'id' => $section->id, 'title' => $section->title, 'lessons' => $formattedLessons, 'created_at' => $section->created_at, 'slug' => $section->slug, 'type' => 'section', 'is_locked' => !$hasAcessSectionAcess && !$isCourseCreator, 'unlock_date' => $unlockDate ]; } return [ 'course' => $course, 'sections' => $formattedSections, 'track' => CourseHelper::getCourseProgressTrack($course->id) ]; } public function enrollCourse(Request $request, $courseId) { $user = $this->getUser(); if (!$user) { return $this->sendError([ 'message' => __('You need to login to enroll in this course.', 'fluent-community') ]); } $course = Course::findOrFail($courseId); /** @var Course $course */ $isCourseCreator = $course->isCourseAdmin($user); if ($course->status !== 'published' && !$isCourseCreator) { return $this->sendError([ 'message' => __('Course not found. maybe the course is not published yet!', 'fluent-community') ]); } if (!$isCourseCreator && $course->privacy != 'public') { return $this->sendError([ 'message' => __('You are not allowed to enroll in this course.', 'fluent-community') ]); } $enrolled = CourseHelper::enrollCourse($course, get_current_user_id()); if (!$enrolled) { return [ 'message' => __('You are already enrolled in this course.', 'fluent-community'), 'track' => CourseHelper::getCourseProgressTrack($courseId) ]; } return [ 'message' => __('You have been enrolled in this course.', 'fluent-community'), 'track' => CourseHelper::getCourseProgressTrack($courseId) ]; } public function updateCompletionLesson(Request $request, $courseId, $lessonId) { $course = Course::findOrFail($courseId); if ($course->status != 'published') { return $this->sendError([ 'message' => __('Course is not in published state.', 'fluent-community') ]); } if (!CourseHelper::isEnrolled($courseId)) { return $this->sendError([ 'message' => __('Please enroll this course first', 'fluent-community') ]); } $lesson = CourseLesson::where('space_id', $courseId) ->where('status', 'published') ->where('id', $lessonId) ->first(); if (!$lesson) { return $this->sendError([ 'message' => __('Lesson is not on published state.', 'fluent-community') ]); } $state = $request->get('state'); if (!in_array($state, ['completed', 'incomplete'])) { return $this->sendError([ 'message' => __('Invalid state.', 'fluent-community') ]); } $isAllowed = apply_filters('fluent_community/is_allowed_to_complete_lesson', true, $lesson, $state); if (!$isAllowed) { $errorData = [ 'message' => __('You are not allowed to complete this lesson.', 'fluent-community') ]; if (LessonVideoGateService::isCompletionBlockedFor($lesson, Helper::getCurrentUser())) { $errorData['code'] = 'video_watch_required'; $errorData['threshold'] = LessonVideoGateService::getThreshold($lesson); } return $this->sendError($errorData); } CourseHelper::updateLessonCompletion($lesson, get_current_user_id(), $state); $track = CourseHelper::getCourseProgressTrack($courseId); $isCompleted = $track['progress'] == 100; if ($isCompleted) { CourseHelper::completeCourse($course, $lesson, get_current_user_id()); } return [ 'message' => __('Lesson completion state updated.', 'fluent-community'), 'track' => $track, 'is_completed' => $isCompleted ]; } public function markLessonVideoWatched(Request $request, $courseId, $lessonId) { $course = Course::findOrFail($courseId); if ($course->status != 'published') { return $this->sendError([ 'message' => __('Course is not in published state.', 'fluent-community') ]); } if (!CourseHelper::isEnrolled($courseId)) { return $this->sendError([ 'message' => __('Please enroll this course first', 'fluent-community') ]); } $lesson = CourseLesson::where('space_id', $courseId) ->where('status', 'published') ->where('id', $lessonId) ->first(); if (!$lesson) { return $this->sendError([ 'message' => __('Lesson is not on published state.', 'fluent-community') ]); } if (!LessonVideoGateService::isGatedLesson($lesson)) { return [ 'message' => __('This lesson does not require video completion.', 'fluent-community'), 'is_gated' => false, 'watched' => false ]; } $watchedPercent = min(100, absint($request->get('watched_percent'))); $threshold = LessonVideoGateService::getThreshold($lesson); if ($watchedPercent < $threshold) { return $this->sendError([ /* translators: %d is the watched percentage required to complete the lesson */ 'message' => sprintf(__('Please watch at least %d%% of the video.', 'fluent-community'), $threshold) ]); } LessonVideoGateService::markWatched($lesson, get_current_user_id()); return [ 'message' => __('Video watch progress has been recorded.', 'fluent-community'), 'is_gated' => true, 'watched' => true ]; } public function resetMyProgress(Request $request, $courseId) { $course = Course::findOrFail($courseId); if ($course->status !== 'published') { return $this->sendError([ 'message' => __('Course is not in published state.', 'fluent-community') ]); } if (!CourseHelper::isEnrolled($courseId)) { return $this->sendError([ 'message' => __('Please enroll this course first', 'fluent-community') ]); } CourseHelper::resetCourseProgress($courseId, get_current_user_id()); return [ 'message' => __('Your course progress has been reset.', 'fluent-community'), 'track' => [ 'completed_lessons' => [], 'isEnrolled' => true, 'progress' => 0 ] ]; } public function getAllCourses(Request $request) { $user = $this->getUser(); $isCourseCreator = $user && $user->hasCourseCreatorAccess(); $coursesQuery = Course::where(function ($q) use ($isCourseCreator, $user) { if ($isCourseCreator) { $q->where('status', 'published') ->orWhere('created_by', $user->ID); } else { $q->where('status', 'published'); } }); if (!($user && $user->isCommunityModerator())) { $userId = get_current_user_id(); $coursesQuery->where(function ($q) use ($userId) { $q->whereIn('privacy', ['public', 'private']) ->orWhereHas('space_pivot', function ($sub) use ($userId) { $sub->where('user_id', $userId) ->where('role', 'student'); }) ->orWhere('created_by', $userId); }); } $courses = $coursesQuery->with(['creator'])->paginate(); $formattedCourses = []; foreach ($courses as $course) { $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count(); $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count(); if (Arr::get($course->settings, 'hide_members_count') != 'yes') { $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count(); } else { $course->studentsCount = 0; } $showInstructorView = Arr::get($course->settings, 'hide_instructor_view') != 'yes'; $showStudentCount = Arr::get($course->settings, 'show_instructor_students_count') == 'yes'; if ($showInstructorView) { if ($course->creator) { $creatorCourseIds = Course::where('created_by', $course->creator->user_id)->pluck('id'); $course->creator->total_courses = count($creatorCourseIds); if ($showStudentCount) { $course->creator->total_students = SpaceUserPivot::whereIn('space_id', $creatorCourseIds)->distinct('user_id')->count('user_id'); } if ($course->creator->short_description) { $course->creator->short_description_rendered = wp_kses_post(FeedsHelper::mdToHtml($course->creator->short_description)); } } } $formattedCourses[] = $this->processCourse($course, $course->isCourseAdmin($user)); } return apply_filters('fluent_community/all_courses_api_response', [ 'courses' => $formattedCourses, 'total' => $courses->total() ], $request->all()); } }