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.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
← All changes | Modules/Course/Services/CourseHelper.php +87 -20 2.6.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
@@ -140,15 +142,14 @@
140 142 if (!$userId) {
141 143 return [];
142 144 }
143 145
144 - static $completedLessonsCache = [];
145 146 $key = $courseId . '_' . $userId;
146 - if (isset($completedLessonsCache[$key])) {
147 - return $completedLessonsCache[$key];
147 + if (isset(self::$completedLessonsCache[$key])) {
148 + return self::$completedLessonsCache[$key];
148 149 }
149 150
150 - return $completedLessonsCache[$key] = Reaction::where('user_id', $userId)
151 + return self::$completedLessonsCache[$key] = Reaction::where('user_id', $userId)
151 152 ->where('parent_id', $courseId)
152 153 ->where('object_type', 'lesson_completed')
153 154 ->where('type', 'completed')
154 155 ->pluck('object_id')
@@ -190,8 +191,9 @@
190 191 if ($reaction) {
191 192 if ($state != 'completed') {
192 193 $reaction->type = 'incomplete';
193 194 $reaction->save();
195 + do_action('fluent_community/course/lesson_marked_incomplete', $lesson, $userId);
194 196 } else {
195 197 $reaction->type = 'completed';
196 198 $reaction->save();
197 199 }
@@ -289,8 +291,41 @@
289 291
290 292 return true;
291 293 }
292 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 +
293 328 public static function enrollCourse($course, $userId = null, $by = 'self', $skipSync = false)
294 329 {
295 330 return Helper::addToSpace($course, $userId, 'student', $by, $skipSync);
296 331 }
@@ -393,9 +428,12 @@
393 428 'enable_media',
394 429 'media',
395 430 'video_length',
396 431 'featured_image_id',
397 - 'free_preview_lesson'
432 + 'free_preview_lesson',
433 + 'require_video_completion',
434 + 'video_completion_threshold',
435 + 'auto_complete_on_video_end'
398 436 ];
399 437
400 438 if ($lesson->isQuizType()) {
401 439 $quizFields = ['quiz_questions', 'passing_score', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
@@ -403,9 +441,9 @@
403 441 }
404 442
405 443 $meta = Arr::only($meta, $validFields);
406 444
407 - $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'];
408 446
409 447 foreach ($yesNoFields as $field) {
410 448 $meta[$field] = Arr::get($meta, $field, 'no') == 'yes' ? 'yes' : 'no';
411 449 }
@@ -419,14 +457,19 @@
419 457 'provider' => ''
420 458 ];
421 459 }
422 460
423 - $numericFields = ['video_length', 'passing_score'];
461 + $numericFields = ['video_length', 'passing_score', 'video_completion_threshold'];
424 462
425 463 foreach ($numericFields as $field) {
426 464 $meta[$field] = absint(Arr::get($meta, $field, 0));
427 465 }
428 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 +
429 472 return apply_filters('fluent_community/lesson/sanitize_meta', $meta, $lesson);
430 473 }
431 474
432 475 public static function sanitizeMedia($media)
@@ -446,9 +489,24 @@
446 489 public static function getCourseCategories()
447 490 {
448 491 $terms = Term::whereHas('base_spaces', function ($q) {
449 492 $q->where('type', 'course');
450 - })->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();
451 509
452 510 $formattedTerms = [];
453 511
454 512 foreach ($terms as $term) {
@@ -497,14 +555,22 @@
497 555
498 556 $inlineCss = '';
499 557 if ($parseContent && $canViewLesson) {
500 558 $content = $lesson->message_rendered;
501 - if (!$content && $lesson->message) {
502 - $content = apply_filters('the_content', $lesson->message); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
503 - $core_styles_keys = array('block-supports');
504 - // Adds comment if code is prettified to identify core styles sections in debugging.
505 - foreach ($core_styles_keys as $style_key) {
506 - $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 + }
507 573 }
508 574 }
509 575
510 576 if (!$content) {
@@ -527,9 +593,9 @@
527 593 'featured_image' => $lesson->featured_image,
528 594 'meta' => $lesson->getPublicLessonMeta($canViewLesson),
529 595 'comments_count' => $lesson->comments_count,
530 596 'is_locked' => Arr::get($config, 'is_locked', false),
531 - 'unclock_date' => Arr::get($config, 'unclock_date', ''),
597 + 'unlock_date' => Arr::get($config, 'unlock_date', ''),
532 598 'lock_type' => Arr::get($config, 'lock_type', ''),
533 599 'can_view' => $canViewLesson,
534 600 'inline_css' => $inlineCss
535 601 ];
@@ -549,8 +615,11 @@
549 615
550 616 $newLessonMeta = $newLesson->meta;
551 617 $newLessonMeta['document_lists'] = [];
552 618 foreach ($lesson->media as $media) {
619 + if (!$media->is_active) {
620 + continue;
621 + }
553 622 $newMedia = $media->replicate();
554 623 $newMedia->feed_id = $newLesson->id;
555 624 $newMedia->save();
556 625 if ($media->object_source == 'lesson_document') {
@@ -557,18 +626,16 @@
557 626 $newLessonMeta['document_lists'][] = $newMedia->getPrivateFileMeta();
558 627 }
559 628 }
560 629
561 - if (!empty($newLessonMeta['document_lists'])) {
562 - $newLesson->meta = $newLessonMeta;
563 - $newLesson->save();
564 - }
630 + $newLesson->meta = $newLessonMeta;
631 + $newLesson->save();
565 632 }
566 633
567 634 public static function getAccessMessage($course, $lesson, $config)
568 635 {
569 636 $isLocked = Arr::get($config, 'is_locked', false);
570 - $unlockDate = Arr::get($config, 'unclock_date', '');
637 + $unlockDate = Arr::get($config, 'unlock_date', '');
571 638 $courseLessonsUrl = Helper::baseUrl('/course/' . $course->slug . '/lessons');
572 639
573 640 $headerMessage = __('This lesson is currently locked', 'fluent-community');
574 641 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');