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 +92 -21 2.6.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
@@ -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,24 +428,31 @@
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 - $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'];
402 440 $validFields = wp_parse_args($validFields, $quizFields);
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', 'shuffle_options', 'shuffle_questions'];
408 446
409 447 foreach ($yesNoFields as $field) {
410 448 $meta[$field] = Arr::get($meta, $field, 'no') == 'yes' ? 'yes' : 'no';
411 449 }
412 450
451 + if ($lesson->isQuizType()) {
452 + $meta['layout'] = Arr::get($meta, 'layout') === 'one_by_one' ? 'one_by_one' : 'all';
453 + }
454 +
413 455 if (Arr::get($meta, 'enable_media') == 'yes') {
414 456 if ($media = Arr::get($meta, 'media', [])) {
415 457 $meta['media'] = self::sanitizeMedia($media);
416 458 }
@@ -419,14 +461,19 @@
419 461 'provider' => ''
420 462 ];
421 463 }
422 464
423 - $numericFields = ['video_length', 'passing_score'];
465 + $numericFields = ['video_length', 'passing_score', 'video_completion_threshold'];
424 466
425 467 foreach ($numericFields as $field) {
426 468 $meta[$field] = absint(Arr::get($meta, $field, 0));
427 469 }
428 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 +
429 476 return apply_filters('fluent_community/lesson/sanitize_meta', $meta, $lesson);
430 477 }
431 478
432 479 public static function sanitizeMedia($media)
@@ -446,9 +493,24 @@
446 493 public static function getCourseCategories()
447 494 {
448 495 $terms = Term::whereHas('base_spaces', function ($q) {
449 496 $q->where('type', 'course');
450 - })->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();
451 513
452 514 $formattedTerms = [];
453 515
454 516 foreach ($terms as $term) {
@@ -497,14 +559,22 @@
497 559
498 560 $inlineCss = '';
499 561 if ($parseContent && $canViewLesson) {
500 562 $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, []);
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 + }
507 577 }
508 578 }
509 579
510 580 if (!$content) {
@@ -527,9 +597,9 @@
527 597 'featured_image' => $lesson->featured_image,
528 598 'meta' => $lesson->getPublicLessonMeta($canViewLesson),
529 599 'comments_count' => $lesson->comments_count,
530 600 'is_locked' => Arr::get($config, 'is_locked', false),
531 - 'unclock_date' => Arr::get($config, 'unclock_date', ''),
601 + 'unlock_date' => Arr::get($config, 'unlock_date', ''),
532 602 'lock_type' => Arr::get($config, 'lock_type', ''),
533 603 'can_view' => $canViewLesson,
534 604 'inline_css' => $inlineCss
535 605 ];
@@ -549,8 +619,11 @@
549 619
550 620 $newLessonMeta = $newLesson->meta;
551 621 $newLessonMeta['document_lists'] = [];
552 622 foreach ($lesson->media as $media) {
623 + if (!$media->is_active) {
624 + continue;
625 + }
553 626 $newMedia = $media->replicate();
554 627 $newMedia->feed_id = $newLesson->id;
555 628 $newMedia->save();
556 629 if ($media->object_source == 'lesson_document') {
@@ -557,18 +630,16 @@
557 630 $newLessonMeta['document_lists'][] = $newMedia->getPrivateFileMeta();
558 631 }
559 632 }
560 633
561 - if (!empty($newLessonMeta['document_lists'])) {
562 - $newLesson->meta = $newLessonMeta;
563 - $newLesson->save();
564 - }
634 + $newLesson->meta = $newLessonMeta;
635 + $newLesson->save();
565 636 }
566 637
567 638 public static function getAccessMessage($course, $lesson, $config)
568 639 {
569 640 $isLocked = Arr::get($config, 'is_locked', false);
570 - $unlockDate = Arr::get($config, 'unclock_date', '');
641 + $unlockDate = Arr::get($config, 'unlock_date', '');
571 642 $courseLessonsUrl = Helper::baseUrl('/course/' . $course->slug . '/lessons');
572 643
573 644 $headerMessage = __('This lesson is currently locked', 'fluent-community');
574 645 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');