PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
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
← All changes | Modules/Course/Services/CourseHelper.php +117 -29 2.4.012.10.01 View file →
@@ -16,8 +16,10 @@
16 16 use FluentCommunity\Modules\Course\Model\CourseTopic;
17 17
18 18 class CourseHelper
19 19 {
20 + protected static $completedLessonsCache = [];
21 +
20 22 public static function getCourseProgressTrack($courseId, $userId = null)
21 23 {
22 24 $isEnrolled = self::isEnrolled($courseId, $userId);
23 25
@@ -27,21 +29,26 @@
27 29 'progress' => $isEnrolled ? self::getCourseProgress($courseId, $userId) : 0,
28 30 ];
29 31 }
30 32
31 - public static function isEnrolled($courseId, $userId = null)
33 + public static function resolveLessonAccess($initialCanView, $lesson, $course, $user, $ctx = [])
32 34 {
33 - if (!$userId) {
34 - $userId = get_current_user_id();
35 + $canView = apply_filters('fluent_community/course/can_view_lesson', $initialCanView, $lesson, $course, $user);
36 + $access = apply_filters('fluent_community/course/lesson_access_info', [
37 + 'can_view' => $canView,
38 + 'lock_type' => '',
39 + ], $lesson, $course, $user, $ctx);
40 +
41 + if (!empty($access['can_view'])) {
42 + $access['lock_type'] = '';
35 43 }
36 44
37 - if (!$userId) {
38 - return false;
39 - }
45 + return $access;
46 + }
40 47
41 - return SpaceUserPivot::where('space_id', $courseId)
42 - ->where('user_id', $userId)
43 - ->exists();
48 + public static function isEnrolled($courseId, $userId = null)
49 + {
50 + return (bool) self::getCourseEnrollment($courseId, $userId);
44 51 }
45 52
46 53 public static function getCourseEnrollment($courseId, $userId = null)
47 54 {
@@ -52,12 +59,17 @@
52 59 if (!$userId) {
53 60 return null;
54 61 }
55 62
56 - return SpaceUserPivot::where('space_id', $courseId)
63 + static $enrollmentCache = [];
64 + $key = $courseId . '_' . $userId;
65 + if (array_key_exists($key, $enrollmentCache)) {
66 + return $enrollmentCache[$key];
67 + }
68 +
69 + return $enrollmentCache[$key] = SpaceUserPivot::where('space_id', $courseId)
57 70 ->where('user_id', $userId)
58 71 ->first();
59 -
60 72 }
61 73
62 74 public static function getCoursePublishedLessonIds($courseId)
63 75 {
@@ -130,9 +142,14 @@
130 142 if (!$userId) {
131 143 return [];
132 144 }
133 145
134 - return Reaction::where('user_id', $userId)
146 + $key = $courseId . '_' . $userId;
147 + if (isset(self::$completedLessonsCache[$key])) {
148 + return self::$completedLessonsCache[$key];
149 + }
150 +
151 + return self::$completedLessonsCache[$key] = Reaction::where('user_id', $userId)
135 152 ->where('parent_id', $courseId)
136 153 ->where('object_type', 'lesson_completed')
137 154 ->where('type', 'completed')
138 155 ->pluck('object_id')
@@ -174,8 +191,9 @@
174 191 if ($reaction) {
175 192 if ($state != 'completed') {
176 193 $reaction->type = 'incomplete';
177 194 $reaction->save();
195 + do_action('fluent_community/course/lesson_marked_incomplete', $lesson, $userId);
178 196 } else {
179 197 $reaction->type = 'completed';
180 198 $reaction->save();
181 199 }
@@ -273,8 +291,41 @@
273 291
274 292 return true;
275 293 }
276 294
295 + public static function resetCourseProgress($courseId, $userId)
296 + {
297 + $course = Course::find($courseId);
298 + if (!$course || !$userId) {
299 + return false;
300 + }
301 +
302 + do_action('fluent_community/course/before_progress_reset', $course, $userId);
303 +
304 + try {
305 + Helper::dbTransaction(function () use ($userId, $courseId) {
306 + // Wipe completion state entirely (both 'completed' and 'incomplete' types) so the student starts pristine.
307 + Reaction::where('user_id', $userId)
308 + ->where('parent_id', $courseId)
309 + ->where('object_type', 'lesson_completed')
310 + ->delete();
311 +
312 + Activity::where('user_id', $userId)
313 + ->where('feed_id', $courseId)
314 + ->where('action_name', 'course_completed')
315 + ->delete();
316 + });
317 + } catch (\Exception $e) {
318 + return false;
319 + }
320 +
321 + unset(self::$completedLessonsCache[$courseId . '_' . $userId]);
322 +
323 + do_action('fluent_community/course/progress_reset', $course, $userId);
324 +
325 + return true;
326 + }
327 +
277 328 public static function enrollCourse($course, $userId = null, $by = 'self', $skipSync = false)
278 329 {
279 330 return Helper::addToSpace($course, $userId, 'student', $by, $skipSync);
280 331 }
@@ -377,9 +428,12 @@
377 428 'enable_media',
378 429 'media',
379 430 'video_length',
380 431 'featured_image_id',
381 - 'free_preview_lesson'
432 + 'free_preview_lesson',
433 + 'require_video_completion',
434 + 'video_completion_threshold',
435 + 'auto_complete_on_video_end'
382 436 ];
383 437
384 438 if ($lesson->isQuizType()) {
385 439 $quizFields = ['quiz_questions', 'passing_score', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
@@ -387,9 +441,9 @@
387 441 }
388 442
389 443 $meta = Arr::only($meta, $validFields);
390 444
391 - $yesNoFields = ['enable_comments', 'enable_media', 'free_preview_lesson', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
445 + $yesNoFields = ['enable_comments', 'enable_media', 'free_preview_lesson', 'require_video_completion', 'auto_complete_on_video_end', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
392 446
393 447 foreach ($yesNoFields as $field) {
394 448 $meta[$field] = Arr::get($meta, $field, 'no') == 'yes' ? 'yes' : 'no';
395 449 }
@@ -403,14 +457,19 @@
403 457 'provider' => ''
404 458 ];
405 459 }
406 460
407 - $numericFields = ['video_length', 'passing_score'];
461 + $numericFields = ['video_length', 'passing_score', 'video_completion_threshold'];
408 462
409 463 foreach ($numericFields as $field) {
410 464 $meta[$field] = absint(Arr::get($meta, $field, 0));
411 465 }
412 466
467 + // 0 means "not set" — the gate falls back to its default threshold
468 + if ($meta['video_completion_threshold']) {
469 + $meta['video_completion_threshold'] = min(100, max(1, $meta['video_completion_threshold']));
470 + }
471 +
413 472 return apply_filters('fluent_community/lesson/sanitize_meta', $meta, $lesson);
414 473 }
415 474
416 475 public static function sanitizeMedia($media)
@@ -430,9 +489,24 @@
430 489 public static function getCourseCategories()
431 490 {
432 491 $terms = Term::whereHas('base_spaces', function ($q) {
433 492 $q->where('type', 'course');
434 - })->get();
493 + })->orderBy('title', 'ASC')->get();
494 + // Respect the admin-defined order (settings.serial)
495 + $terms = $terms->sort(function ($a, $b) {
496 + $serialA = Arr::get($a->settings, 'serial');
497 + $serialB = Arr::get($b->settings, 'serial');
498 + if ($serialA === null && $serialB === null) {
499 + return 0;
500 + }
501 + if ($serialA === null) {
502 + return 1;
503 + }
504 + if ($serialB === null) {
505 + return -1;
506 + }
507 + return (int) $serialA <=> (int) $serialB;
508 + })->values();
435 509
436 510 $formattedTerms = [];
437 511
438 512 foreach ($terms as $term) {
@@ -481,14 +555,22 @@
481 555
482 556 $inlineCss = '';
483 557 if ($parseContent && $canViewLesson) {
484 558 $content = $lesson->message_rendered;
485 - if (!$content && $lesson->message) {
486 - $content = apply_filters('the_content', $lesson->message); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
487 - $core_styles_keys = array('block-supports');
488 - // Adds comment if code is prettified to identify core styles sections in debugging.
489 - foreach ($core_styles_keys as $style_key) {
490 - $inlineCss .= wp_style_engine_get_stylesheet_from_context($style_key, []);
559 +
560 + // message_rendered still holding block markup never went through the_content
561 + if (!$content || has_blocks($content)) {
562 + $source = $lesson->message ?: $content;
563 +
564 + if ($source) {
565 + $content = apply_filters('the_content', $source); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
566 + if (function_exists('wp_style_engine_get_stylesheet_from_context')) {
567 + $core_styles_keys = array('block-supports');
568 + // Adds comment if code is prettified to identify core styles sections in debugging.
569 + foreach ($core_styles_keys as $style_key) {
570 + $inlineCss .= wp_style_engine_get_stylesheet_from_context($style_key, []);
571 + }
572 + }
491 573 }
492 574 }
493 575
494 576 if (!$content) {
@@ -511,9 +593,10 @@
511 593 'featured_image' => $lesson->featured_image,
512 594 'meta' => $lesson->getPublicLessonMeta($canViewLesson),
513 595 'comments_count' => $lesson->comments_count,
514 596 'is_locked' => Arr::get($config, 'is_locked', false),
515 - 'unclock_date' => Arr::get($config, 'unclock_date', ''),
597 + 'unlock_date' => Arr::get($config, 'unlock_date', ''),
598 + 'lock_type' => Arr::get($config, 'lock_type', ''),
516 599 'can_view' => $canViewLesson,
517 600 'inline_css' => $inlineCss
518 601 ];
519 602
@@ -532,8 +615,11 @@
532 615
533 616 $newLessonMeta = $newLesson->meta;
534 617 $newLessonMeta['document_lists'] = [];
535 618 foreach ($lesson->media as $media) {
619 + if (!$media->is_active) {
620 + continue;
621 + }
536 622 $newMedia = $media->replicate();
537 623 $newMedia->feed_id = $newLesson->id;
538 624 $newMedia->save();
539 625 if ($media->object_source == 'lesson_document') {
@@ -540,18 +626,16 @@
540 626 $newLessonMeta['document_lists'][] = $newMedia->getPrivateFileMeta();
541 627 }
542 628 }
543 629
544 - if (!empty($newLessonMeta['document_lists'])) {
545 - $newLesson->meta = $newLessonMeta;
546 - $newLesson->save();
547 - }
630 + $newLesson->meta = $newLessonMeta;
631 + $newLesson->save();
548 632 }
549 633
550 634 public static function getAccessMessage($course, $lesson, $config)
551 635 {
552 636 $isLocked = Arr::get($config, 'is_locked', false);
553 - $unlockDate = Arr::get($config, 'unclock_date', '');
637 + $unlockDate = Arr::get($config, 'unlock_date', '');
554 638 $courseLessonsUrl = Helper::baseUrl('/course/' . $course->slug . '/lessons');
555 639
556 640 $headerMessage = __('This lesson is currently locked', 'fluent-community');
557 641 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');
@@ -556,9 +640,12 @@
556 640 $headerMessage = __('This lesson is currently locked', 'fluent-community');
557 641 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');
558 642 $backToCourseText = __('Back to Course', 'fluent-community');
559 643
560 - if ($isLocked && $unlockDate) {
644 + if ($isLocked && Arr::get($config, 'lock_type') === 'sequential') {
645 + $headerMessage = __('Complete the previous lesson first', 'fluent-community');
646 + $bodyMessage = __('You need to complete the previous lesson before unlocking this one.', 'fluent-community');
647 + } elseif ($isLocked && $unlockDate) {
561 648 $headerMessage = __('This lesson is not published for you yet', 'fluent-community');
562 649 /* translators: %s is replaced by the date */
563 650 $bodyMessage = sprintf(__('It will be available to you on %s', 'fluent-community'),
564 651 date_i18n(get_option('date_format'), strtotime($unlockDate))
@@ -594,5 +681,6 @@
594 681 $emailComposer->setDefaultFooter();
595 682
596 683 return $emailComposer->getHtml();
597 684 }
685 +
598 686 }