← All changes
|
Modules/Course/Http/Controllers/CourseController.php
+148
-16
2.4.01
→
2.10.0
View file →
| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | use FluentCommunity\Modules\Course\Model\CourseLesson; |
| 13 | 13 | use FluentCommunity\Modules\Course\Model\CourseTopic; |
| 14 | 14 | use FluentCommunity\App\Services\FeedsHelper; |
| 15 | 15 | use FluentCommunity\Modules\Course\Services\CourseHelper; |
| 16 | +use FluentCommunity\Modules\Course\Services\LessonVideoGateService; | |
| 16 | 17 | |
| 17 | 18 | class CourseController extends Controller |
| 18 | 19 | { |
| 19 | 20 | public function getCourses(Request $request) |
| @@ -71,8 +72,10 @@ | ||
| 71 | 72 | $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count(); |
| 72 | 73 | } else { |
| 73 | 74 | $course->studentsCount = 0; |
| 74 | 75 | } |
| 76 | + | |
| 77 | + do_action_ref_array('fluent_community/course', [&$course]); | |
| 75 | 78 | } |
| 76 | 79 | |
| 77 | 80 | $data = [ |
| 78 | 81 | 'courses' => $courses, |
| @@ -86,8 +89,9 @@ | ||
| 86 | 89 | { |
| 87 | 90 | $user = $this->getUser(); |
| 88 | 91 | |
| 89 | 92 | $course = Course::findOrFail($courseId); |
| 93 | + /** @var Course $course */ | |
| 90 | 94 | |
| 91 | 95 | $isCourseCreator = $course->isCourseAdmin($user); |
| 92 | 96 | |
| 93 | 97 | if ($course->status !== 'published' && !$isCourseCreator) { |
| @@ -107,8 +111,9 @@ | ||
| 107 | 111 | { |
| 108 | 112 | $user = $this->getUser(); |
| 109 | 113 | |
| 110 | 114 | $course = Course::where('slug', $slug)->firstOrFail(); |
| 115 | + /** @var Course $course */ | |
| 111 | 116 | |
| 112 | 117 | $isCourseCreator = $course->isCourseAdmin($user); |
| 113 | 118 | |
| 114 | 119 | if ($course->status !== 'published' && !$isCourseCreator) { |
| @@ -143,8 +148,9 @@ | ||
| 143 | 148 | public function getLessonBySlug(Request $request, $courseSlug, $lessonSlug) |
| 144 | 149 | { |
| 145 | 150 | $user = $this->getUser(); |
| 146 | 151 | |
| 152 | + /** @var Course $course */ | |
| 147 | 153 | $course = Course::where('slug', $courseSlug)->firstOrFail(); |
| 148 | 154 | |
| 149 | 155 | $isCourseCreator = $course->isCourseAdmin($user); |
| 150 | 156 | |
| @@ -189,16 +195,24 @@ | ||
| 189 | 195 | $unlockDate = null; |
| 190 | 196 | } |
| 191 | 197 | } |
| 192 | 198 | |
| 193 | - $canViewLesson = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess; | |
| 194 | - $canViewLesson = apply_filters('fluent_community/course/can_view_lesson', $canViewLesson, $lesson, $course, $this->getUser()); | |
| 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']; | |
| 195 | 208 | |
| 196 | 209 | $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $user, [ |
| 197 | 210 | 'can_view' => $canViewLesson, |
| 198 | 211 | 'parse_content' => $canViewLesson, |
| 199 | - 'is_locked' => !$hasAcessSectionAcess && !$isCourseCreator, | |
| 200 | - 'unclock_date' => $unlockDate | |
| 212 | + 'is_locked' => !$canViewLesson && !$isCourseCreator, | |
| 213 | + 'unlock_date' => $unlockDate, | |
| 214 | + 'lock_type' => $lockType | |
| 201 | 215 | ]); |
| 202 | 216 | |
| 203 | 217 | $data = [ |
| 204 | 218 | 'lesson' => $formattedLesson |
| @@ -210,8 +224,9 @@ | ||
| 210 | 224 | private function processCourse(Course $course, $isCourseCreator, $intendtedLessonSlug = '') |
| 211 | 225 | { |
| 212 | 226 | $sections = CourseTopic::where('space_id', $course->id) |
| 213 | 227 | ->orderBy('priority', 'ASC') |
| 228 | + ->orderBy('id', 'ASC') | |
| 214 | 229 | ->with(['lessons' => function ($query) use ($isCourseCreator) { |
| 215 | 230 | if (!$isCourseCreator) { |
| 216 | 231 | $query->where('status', 'published'); |
| 217 | 232 | } |
| @@ -232,9 +247,14 @@ | ||
| 232 | 247 | ]); |
| 233 | 248 | } |
| 234 | 249 | |
| 235 | 250 | $course->is_course_admin = $isCourseCreator; |
| 251 | + $course->can_self_enroll = $isCourseCreator || ($course->status === 'published' && $course->privacy === 'public'); | |
| 236 | 252 | |
| 253 | + $course = apply_filters('fluent_community/course/processed', $course, [ | |
| 254 | + 'is_enrolled' => !!$enrollment, | |
| 255 | + ]); | |
| 256 | + | |
| 237 | 257 | $courseSettings = $course->settings; |
| 238 | 258 | if (Arr::get($courseSettings, 'course_details')) { |
| 239 | 259 | $courseSettings['course_details_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($courseSettings['course_details'])); |
| 240 | 260 | $course->settings = $courseSettings; |
| @@ -262,12 +282,21 @@ | ||
| 262 | 282 | $unlockDate = null; |
| 263 | 283 | } |
| 264 | 284 | } |
| 265 | 285 | |
| 286 | + $sectionCtx = [ | |
| 287 | + 'enrollment' => $enrollment, | |
| 288 | + 'is_admin' => $isCourseCreator, | |
| 289 | + 'has_section_access' => $hasAcessSectionAcess, | |
| 290 | + 'has_public_access' => $hasPublicViewAccess, | |
| 291 | + ]; | |
| 292 | + | |
| 266 | 293 | $formattedLessons = []; |
| 267 | 294 | foreach ($section->lessons as $lesson) { |
| 268 | - $canViewLesson = $hasAcessSectionAcess || $isCourseCreator || $hasPublicViewAccess; | |
| 269 | - $canViewLesson = apply_filters('fluent_community/course/can_view_lesson', $canViewLesson, $lesson, $course, $this->getUser()); | |
| 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']; | |
| 270 | 299 | |
| 271 | 300 | $parseContent = $intendtedLessonSlug === $lesson->slug && $canViewLesson; |
| 272 | 301 | |
| 273 | 302 | $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $currentUser, [ |
| @@ -272,14 +301,15 @@ | ||
| 272 | 301 | |
| 273 | 302 | $formattedLesson = CourseHelper::formatLessonData($course, $lesson, $currentUser, [ |
| 274 | 303 | 'can_view' => $canViewLesson, |
| 275 | 304 | 'parse_content' => $parseContent, |
| 276 | - 'is_locked' => !$hasAcessSectionAcess && !$isCourseCreator, | |
| 277 | - 'unclock_date' => $unlockDate | |
| 305 | + 'is_locked' => !$canViewLesson && !$isCourseCreator, | |
| 306 | + 'unlock_date' => $unlockDate, | |
| 307 | + 'lock_type' => $lockType | |
| 278 | 308 | ]); |
| 279 | 309 | |
| 280 | 310 | $isLazyLoad = !$parseContent; |
| 281 | - if(!$canViewLesson) { | |
| 311 | + if(!$canViewLesson && $lockType !== 'sequential') { | |
| 282 | 312 | $isLazyLoad = false; |
| 283 | 313 | } |
| 284 | 314 | |
| 285 | 315 | $formattedLesson['lazy_load'] = $isLazyLoad; |
| @@ -316,8 +346,9 @@ | ||
| 316 | 346 | ]); |
| 317 | 347 | } |
| 318 | 348 | |
| 319 | 349 | $course = Course::findOrFail($courseId); |
| 350 | + /** @var Course $course */ | |
| 320 | 351 | |
| 321 | 352 | $isCourseCreator = $course->isCourseAdmin($user); |
| 322 | 353 | |
| 323 | 354 | if ($course->status !== 'published' && !$isCourseCreator) { |
| @@ -382,14 +413,21 @@ | ||
| 382 | 413 | 'message' => __('Invalid state.', 'fluent-community') |
| 383 | 414 | ]); |
| 384 | 415 | } |
| 385 | 416 | |
| 386 | - $isAllowed = apply_filters('fluent_community/is_allowed_to_complete_lesson', true, $lesson); | |
| 417 | + $isAllowed = apply_filters('fluent_community/is_allowed_to_complete_lesson', true, $lesson, $state); | |
| 387 | 418 | |
| 388 | 419 | if (!$isAllowed) { |
| 389 | - return $this->sendError([ | |
| 420 | + $errorData = [ | |
| 390 | 421 | 'message' => __('You are not allowed to complete this lesson.', 'fluent-community') |
| 391 | - ]); | |
| 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); | |
| 392 | 430 | } |
| 393 | 431 | |
| 394 | 432 | CourseHelper::updateLessonCompletion($lesson, get_current_user_id(), $state); |
| 395 | 433 | $track = CourseHelper::getCourseProgressTrack($courseId); |
| @@ -405,14 +443,96 @@ | ||
| 405 | 443 | 'is_completed' => $isCompleted |
| 406 | 444 | ]; |
| 407 | 445 | } |
| 408 | 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 | + | |
| 409 | 529 | public function getAllCourses(Request $request) |
| 410 | 530 | { |
| 411 | 531 | $user = $this->getUser(); |
| 412 | 532 | $isCourseCreator = $user && $user->hasCourseCreatorAccess(); |
| 413 | 533 | |
| 414 | - $courses = Course::where(function ($q) use ($isCourseCreator, $user) { | |
| 534 | + $coursesQuery = Course::where(function ($q) use ($isCourseCreator, $user) { | |
| 415 | 535 | if ($isCourseCreator) { |
| 416 | 536 | $q->where('status', 'published') |
| 417 | 537 | ->orWhere('created_by', $user->ID); |
| 418 | 538 | } else { |
| @@ -417,11 +537,23 @@ | ||
| 417 | 537 | ->orWhere('created_by', $user->ID); |
| 418 | 538 | } else { |
| 419 | 539 | $q->where('status', 'published'); |
| 420 | 540 | } |
| 421 | - }) | |
| 422 | - ->with(['creator']) | |
| 423 | - ->paginate(); | |
| 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(); | |
| 424 | 556 | |
| 425 | 557 | $formattedCourses = []; |
| 426 | 558 | foreach ($courses as $course) { |
| 427 | 559 | $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count(); |