PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.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 1.1.0 All 77 releases
fluent-community / Modules / Course / Services / LessonVideoGateService.php

LessonVideoGateService.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.10.0, at Modules/Course/Services/LessonVideoGateService.php

192 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Modules\Course\Services;
4
5 use FluentCommunity\App\Models\Reaction;
6 use FluentCommunity\Framework\Support\Arr;
7
8 /**
9 * Video-gated lesson completion.
10 *
11 * When a lesson's feature media is a FluentPlayer video and
12 * `require_video_completion` is enabled in the lesson meta, students must
13 * watch the video up to `video_completion_threshold` percent (default 80,
14 * the `ended` event always satisfies it) before the lesson can be marked
15 * as completed.
16 *
17 * The watched state is stored as a Reaction row — the same convention used
18 * for `lesson_completed`:
19 * object_type = lesson_video_watched, object_id = lesson, parent_id = course
20 */
21 class LessonVideoGateService
22 {
23 const WATCHED_OBJECT_TYPE = 'lesson_video_watched';
24
25 const DEFAULT_THRESHOLD = 80;
26
27 const AUTO_COMPLETE_DELAY = 3;
28
29 // Per-request watched lesson ids, keyed by "{course_id}_{user_id}"
30 private static $watchedCache = [];
31
32 public static function isFluentPlayerActive()
33 {
34 return defined('FLUENT_PLAYER_VERSION');
35 }
36
37 public static function isGatedLesson($lesson)
38 {
39 // Without FluentPlayer the tracker can never run, so never enforce
40 if (!self::isFluentPlayerActive()) {
41 return false;
42 }
43
44 $meta = $lesson->meta;
45
46 if (Arr::get($meta, 'require_video_completion') != 'yes') {
47 return false;
48 }
49
50 if (Arr::get($meta, 'enable_media') != 'yes') {
51 return false;
52 }
53
54 return Arr::get($meta, 'media.type') == 'fluent_player';
55 }
56
57 public static function isAutoCompleteEnabled($lesson)
58 {
59 if (!self::isFluentPlayerActive()) {
60 return false;
61 }
62
63 $meta = $lesson->meta;
64
65 if (Arr::get($meta, 'auto_complete_on_video_end') != 'yes') {
66 return false;
67 }
68
69 if (Arr::get($meta, 'enable_media') != 'yes') {
70 return false;
71 }
72
73 return Arr::get($meta, 'media.type') == 'fluent_player';
74 }
75
76 public static function getAutoCompleteDelay()
77 {
78 $delay = (int) apply_filters('fluent_community/lesson_video_gate/auto_complete_delay', self::AUTO_COMPLETE_DELAY);
79
80 return $delay >= 0 ? $delay : self::AUTO_COMPLETE_DELAY;
81 }
82
83 public static function getDefaultThreshold()
84 {
85 $default = (int) apply_filters('fluent_community/lesson_video_gate/default_threshold', self::DEFAULT_THRESHOLD);
86
87 return $default > 0 ? min(100, $default) : self::DEFAULT_THRESHOLD;
88 }
89
90 public static function getThreshold($lesson)
91 {
92 $threshold = absint(Arr::get($lesson->meta, 'video_completion_threshold', 0));
93
94 return $threshold > 0 ? min(100, $threshold) : self::getDefaultThreshold();
95 }
96
97 public static function isCompletionBlockedFor($lesson, $user)
98 {
99 if (!$user || !self::isGatedLesson($lesson)) {
100 return false;
101 }
102
103 // Course admins bypass everywhere; course creators only on their own courses
104 $course = $lesson->course;
105 if ($course && $course->isCourseAdmin($user)) {
106 return false;
107 }
108
109 return !self::hasWatched($lesson, $user->ID);
110 }
111
112 public static function hasWatched($lesson, $userId)
113 {
114 if (!$userId) {
115 return false;
116 }
117
118 $cacheKey = $lesson->space_id . '_' . $userId;
119
120 if (!isset(self::$watchedCache[$cacheKey])) {
121 self::$watchedCache[$cacheKey] = Reaction::where('user_id', $userId)
122 ->where('parent_id', $lesson->space_id)
123 ->where('object_type', self::WATCHED_OBJECT_TYPE)
124 ->where('type', 'watched')
125 ->pluck('object_id')
126 ->toArray();
127 }
128
129 return in_array($lesson->id, self::$watchedCache[$cacheKey]);
130 }
131
132 public static function markWatched($lesson, $userId)
133 {
134 $existing = Reaction::where('user_id', $userId)
135 ->where('object_id', $lesson->id)
136 ->where('object_type', self::WATCHED_OBJECT_TYPE)
137 ->first();
138
139 if ($existing) {
140 if ($existing->type != 'watched') {
141 $existing->type = 'watched';
142 $existing->save();
143 unset(self::$watchedCache[$lesson->space_id . '_' . $userId]);
144 }
145 return $existing;
146 }
147
148 $reaction = Reaction::create([
149 'user_id' => $userId,
150 'object_id' => $lesson->id,
151 'object_type' => self::WATCHED_OBJECT_TYPE,
152 'parent_id' => $lesson->space_id,
153 'type' => 'watched'
154 ]);
155
156 unset(self::$watchedCache[$lesson->space_id . '_' . $userId]);
157
158 do_action('fluent_community/lesson/video_watched', $lesson, $userId);
159
160 return $reaction;
161 }
162
163 public static function resetWatchedForCourse($courseId, $userId)
164 {
165 Reaction::where('user_id', $userId)
166 ->where('parent_id', $courseId)
167 ->where('object_type', self::WATCHED_OBJECT_TYPE)
168 ->delete();
169
170 unset(self::$watchedCache[$courseId . '_' . $userId]);
171 }
172
173 public static function resetWatchedForLesson($lesson, $userId)
174 {
175 Reaction::where('user_id', $userId)
176 ->where('object_id', $lesson->id)
177 ->where('object_type', self::WATCHED_OBJECT_TYPE)
178 ->delete();
179
180 unset(self::$watchedCache[$lesson->space_id . '_' . $userId]);
181 }
182
183 public static function deleteWatchedForLesson($lessonId)
184 {
185 Reaction::where('object_id', $lessonId)
186 ->where('object_type', self::WATCHED_OBJECT_TYPE)
187 ->delete();
188
189 self::$watchedCache = [];
190 }
191 }
192