PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.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
← All changes | Modules/Course/Services/CourseHelper.php +122 -30 2.4.012.11.0 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,24 +428,31 @@
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 - $quizFields = ['quiz_questions', 'passing_score', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
439 + $quizFields = ['quiz_questions', 'passing_score', 'enable_passing_score', 'enforce_passing_score', 'hide_result', 'shuffle_options', 'shuffle_questions', 'layout'];
386 440 $validFields = wp_parse_args($validFields, $quizFields);
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', 'shuffle_options', 'shuffle_questions'];
392 446
393 447 foreach ($yesNoFields as $field) {
394 448 $meta[$field] = Arr::get($meta, $field, 'no') == 'yes' ? 'yes' : 'no';
395 449 }
396 450
451 + if ($lesson->isQuizType()) {
452 + $meta['layout'] = Arr::get($meta, 'layout') === 'one_by_one' ? 'one_by_one' : 'all';
453 + }
454 +
397 455 if (Arr::get($meta, 'enable_media') == 'yes') {
398 456 if ($media = Arr::get($meta, 'media', [])) {
399 457 $meta['media'] = self::sanitizeMedia($media);
400 458 }
@@ -403,14 +461,19 @@
403 461 'provider' => ''
404 462 ];
405 463 }
406 464
407 - $numericFields = ['video_length', 'passing_score'];
465 + $numericFields = ['video_length', 'passing_score', 'video_completion_threshold'];
408 466
409 467 foreach ($numericFields as $field) {
410 468 $meta[$field] = absint(Arr::get($meta, $field, 0));
411 469 }
412 470
471 + // 0 means "not set" — the gate falls back to its default threshold
472 + if ($meta['video_completion_threshold']) {
473 + $meta['video_completion_threshold'] = min(100, max(1, $meta['video_completion_threshold']));
474 + }
475 +
413 476 return apply_filters('fluent_community/lesson/sanitize_meta', $meta, $lesson);
414 477 }
415 478
416 479 public static function sanitizeMedia($media)
@@ -430,9 +493,24 @@
430 493 public static function getCourseCategories()
431 494 {
432 495 $terms = Term::whereHas('base_spaces', function ($q) {
433 496 $q->where('type', 'course');
434 - })->get();
497 + })->orderBy('title', 'ASC')->get();
498 + // Respect the admin-defined order (settings.serial)
499 + $terms = $terms->sort(function ($a, $b) {
500 + $serialA = Arr::get($a->settings, 'serial');
501 + $serialB = Arr::get($b->settings, 'serial');
502 + if ($serialA === null && $serialB === null) {
503 + return 0;
504 + }
505 + if ($serialA === null) {
506 + return 1;
507 + }
508 + if ($serialB === null) {
509 + return -1;
510 + }
511 + return (int) $serialA <=> (int) $serialB;
512 + })->values();
435 513
436 514 $formattedTerms = [];
437 515
438 516 foreach ($terms as $term) {
@@ -481,14 +559,22 @@
481 559
482 560 $inlineCss = '';
483 561 if ($parseContent && $canViewLesson) {
484 562 $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, []);
563 +
564 + // message_rendered still holding block markup never went through the_content
565 + if (!$content || has_blocks($content)) {
566 + $source = $lesson->message ?: $content;
567 +
568 + if ($source) {
569 + $content = apply_filters('the_content', $source); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
570 + if (function_exists('wp_style_engine_get_stylesheet_from_context')) {
571 + $core_styles_keys = array('block-supports');
572 + // Adds comment if code is prettified to identify core styles sections in debugging.
573 + foreach ($core_styles_keys as $style_key) {
574 + $inlineCss .= wp_style_engine_get_stylesheet_from_context($style_key, []);
575 + }
576 + }
491 577 }
492 578 }
493 579
494 580 if (!$content) {
@@ -511,9 +597,10 @@
511 597 'featured_image' => $lesson->featured_image,
512 598 'meta' => $lesson->getPublicLessonMeta($canViewLesson),
513 599 'comments_count' => $lesson->comments_count,
514 600 'is_locked' => Arr::get($config, 'is_locked', false),
515 - 'unclock_date' => Arr::get($config, 'unclock_date', ''),
601 + 'unlock_date' => Arr::get($config, 'unlock_date', ''),
602 + 'lock_type' => Arr::get($config, 'lock_type', ''),
516 603 'can_view' => $canViewLesson,
517 604 'inline_css' => $inlineCss
518 605 ];
519 606
@@ -532,8 +619,11 @@
532 619
533 620 $newLessonMeta = $newLesson->meta;
534 621 $newLessonMeta['document_lists'] = [];
535 622 foreach ($lesson->media as $media) {
623 + if (!$media->is_active) {
624 + continue;
625 + }
536 626 $newMedia = $media->replicate();
537 627 $newMedia->feed_id = $newLesson->id;
538 628 $newMedia->save();
539 629 if ($media->object_source == 'lesson_document') {
@@ -540,18 +630,16 @@
540 630 $newLessonMeta['document_lists'][] = $newMedia->getPrivateFileMeta();
541 631 }
542 632 }
543 633
544 - if (!empty($newLessonMeta['document_lists'])) {
545 - $newLesson->meta = $newLessonMeta;
546 - $newLesson->save();
547 - }
634 + $newLesson->meta = $newLessonMeta;
635 + $newLesson->save();
548 636 }
549 637
550 638 public static function getAccessMessage($course, $lesson, $config)
551 639 {
552 640 $isLocked = Arr::get($config, 'is_locked', false);
553 - $unlockDate = Arr::get($config, 'unclock_date', '');
641 + $unlockDate = Arr::get($config, 'unlock_date', '');
554 642 $courseLessonsUrl = Helper::baseUrl('/course/' . $course->slug . '/lessons');
555 643
556 644 $headerMessage = __('This lesson is currently locked', 'fluent-community');
557 645 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');
@@ -556,9 +644,12 @@
556 644 $headerMessage = __('This lesson is currently locked', 'fluent-community');
557 645 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');
558 646 $backToCourseText = __('Back to Course', 'fluent-community');
559 647
560 - if ($isLocked && $unlockDate) {
648 + if ($isLocked && Arr::get($config, 'lock_type') === 'sequential') {
649 + $headerMessage = __('Complete the previous lesson first', 'fluent-community');
650 + $bodyMessage = __('You need to complete the previous lesson before unlocking this one.', 'fluent-community');
651 + } elseif ($isLocked && $unlockDate) {
561 652 $headerMessage = __('This lesson is not published for you yet', 'fluent-community');
562 653 /* translators: %s is replaced by the date */
563 654 $bodyMessage = sprintf(__('It will be available to you on %s', 'fluent-community'),
564 655 date_i18n(get_option('date_format'), strtotime($unlockDate))
@@ -594,5 +685,6 @@
594 685 $emailComposer->setDefaultFooter();
595 686
596 687 return $emailComposer->getHtml();
597 688 }
689 +
598 690 }