PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.4.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.4.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
fluent-community / Modules / Course / Services / CourseHelper.php

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

599 lines 18.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\Activity;
6 use FluentCommunity\App\Models\Meta;
7 use FluentCommunity\App\Models\Reaction;
8 use FluentCommunity\App\Models\SpaceUserPivot;
9 use FluentCommunity\App\Models\Term;
10 use FluentCommunity\App\Services\CustomSanitizer;
11 use FluentCommunity\App\Services\Helper;
12 use FluentCommunity\App\Services\SmartCodeParser;
13 use FluentCommunity\Framework\Support\Arr;
14 use FluentCommunity\Modules\Course\Model\Course;
15 use FluentCommunity\Modules\Course\Model\CourseLesson;
16 use FluentCommunity\Modules\Course\Model\CourseTopic;
17
18 class CourseHelper
19 {
20 public static function getCourseProgressTrack($courseId, $userId = null)
21 {
22 $isEnrolled = self::isEnrolled($courseId, $userId);
23
24 return [
25 'completed_lessons' => $isEnrolled ? self::getCompletedLessonIds($courseId, $userId) : [],
26 'isEnrolled' => $isEnrolled,
27 'progress' => $isEnrolled ? self::getCourseProgress($courseId, $userId) : 0,
28 ];
29 }
30
31 public static function isEnrolled($courseId, $userId = null)
32 {
33 if (!$userId) {
34 $userId = get_current_user_id();
35 }
36
37 if (!$userId) {
38 return false;
39 }
40
41 return SpaceUserPivot::where('space_id', $courseId)
42 ->where('user_id', $userId)
43 ->exists();
44 }
45
46 public static function getCourseEnrollment($courseId, $userId = null)
47 {
48 if (!$userId) {
49 $userId = get_current_user_id();
50 }
51
52 if (!$userId) {
53 return null;
54 }
55
56 return SpaceUserPivot::where('space_id', $courseId)
57 ->where('user_id', $userId)
58 ->first();
59
60 }
61
62 public static function getCoursePublishedLessonIds($courseId)
63 {
64 static $counts = [];
65
66 if (isset($counts[$courseId])) {
67 return $counts[$courseId];
68 }
69
70 $counts[$courseId] = CourseLesson::where('space_id', $courseId)
71 ->where('status', 'published')
72 ->pluck('id')
73 ->toArray();
74
75 return $counts[$courseId];
76 }
77
78 public static function getCourseProgress($courseId, $userId = null)
79 {
80 if (!$userId) {
81 $userId = get_current_user_id();
82 }
83
84 if (!$userId) {
85 return 0;
86 }
87
88 $progressMap = self::getBulkCourseProgress($courseId, [$userId]);
89
90 return $progressMap[$userId] ?? 0;
91 }
92
93 public static function getBulkCourseProgress($courseId, $userIds = [])
94 {
95 if (empty($userIds)) {
96 return [];
97 }
98
99 $progressMap = array_fill_keys($userIds, 0);
100
101 $lessonIds = self::getCoursePublishedLessonIds($courseId);
102 $totalLessons = count($lessonIds);
103
104 if (!$totalLessons) {
105 return $progressMap;
106 }
107
108 $completionCounts = Reaction::whereIn('user_id', $userIds)
109 ->whereIn('object_id', $lessonIds)
110 ->where('object_type', 'lesson_completed')
111 ->where('type', 'completed')
112 ->groupBy('user_id')
113 ->selectRaw('user_id, COUNT(*) as completed_count')
114 ->pluck('completed_count', 'user_id');
115
116 foreach ($completionCounts as $userId => $completed) {
117 $result = floor(($completed / $totalLessons) * 100);
118 $progressMap[$userId] = min($result, 100);
119 }
120
121 return $progressMap;
122 }
123
124 public static function getCompletedLessonIds($courseId, $userId = null)
125 {
126 if (!$userId) {
127 $userId = get_current_user_id();
128 }
129
130 if (!$userId) {
131 return [];
132 }
133
134 return Reaction::where('user_id', $userId)
135 ->where('parent_id', $courseId)
136 ->where('object_type', 'lesson_completed')
137 ->where('type', 'completed')
138 ->pluck('object_id')
139 ->toArray();
140 }
141
142 public static function overallCourseProgressAverage($course)
143 {
144 $lessonsCount = CourseLesson::where('space_id', $course->id)
145 ->where('status', 'published')
146 ->count();
147
148 if (!$lessonsCount) {
149 return 0;
150 }
151
152 $allCompletedCount = Reaction::where('object_type', 'lesson_completed')
153 ->where('type', 'completed')
154 ->where('parent_id', $course->id)
155 ->count();
156
157 $studentsCount = $course->students_count ? $course->students_count : $course->students()->count();
158
159 if (!$studentsCount) {
160 return 0;
161 }
162
163 // calculate the average of all students
164 return ceil(($allCompletedCount / ($studentsCount * $lessonsCount)) * 100);
165 }
166
167 public static function updateLessonCompletion($lesson, $userId, $state = 'completed')
168 {
169 $reaction = Reaction::where('user_id', $userId)
170 ->where('object_id', $lesson->id)
171 ->where('object_type', 'lesson_completed')
172 ->first();
173
174 if ($reaction) {
175 if ($state != 'completed') {
176 $reaction->type = 'incomplete';
177 $reaction->save();
178 } else {
179 $reaction->type = 'completed';
180 $reaction->save();
181 }
182 return true;
183 }
184
185 Reaction::create([
186 'user_id' => $userId,
187 'object_id' => $lesson->id,
188 'object_type' => 'lesson_completed',
189 'parent_id' => $lesson->space_id,
190 'type' => 'completed'
191 ]);
192
193 do_action('fluent_community/course/lesson_completed', $lesson, $userId);
194
195 // check if the whole module is completed
196 $allTopicLessonIds = CourseLesson::where('space_id', $lesson->space_id)
197 ->where('parent_id', $lesson->parent_id)
198 ->where('status', 'published')
199 ->pluck('id')
200 ->toArray();
201
202 $completedLessonCount = Reaction::where('user_id', $userId)
203 ->whereIn('object_id', $allTopicLessonIds)
204 ->where('object_type', 'lesson_completed')
205 ->where('type', 'completed')
206 ->count();
207
208 if (count($allTopicLessonIds) == $completedLessonCount) {
209 $topic = CourseTopic::find($lesson->parent_id);
210 do_action('fluent_community/course/topic_completed', $topic, $userId, $lesson);
211 }
212
213 return true;
214 }
215
216 public static function getCourseMeta($key, $default = false, $withModel = false)
217 {
218 $meta = Meta::where('meta_key', $key)
219 ->where('object_type', 'course')
220 ->first();
221
222 if ($withModel) {
223 return $meta;
224 }
225
226 return $meta ? $meta->value : $default;
227 }
228
229 public static function updateCourseMeta($key, $value)
230 {
231 $meta = Meta::where('meta_key', $key)
232 ->where('object_type', 'course')
233 ->first();
234
235 if ($meta) {
236 $meta->update([
237 'value' => $value
238 ]);
239 return $meta;
240 }
241
242 return Meta::create([
243 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
244 'value' => $value,
245 'object_type' => 'course'
246 ]);
247 }
248
249 public static function completeCourse($course, $lesson, $userId)
250 {
251 $activity = Activity::where('feed_id', $course->id)
252 ->where('user_id', $userId)
253 ->where('action_name', 'course_completed')
254 ->first();
255
256 if ($activity) {
257 if (strtotime($lesson->scheduled_at) > strtotime($activity->updated_at)) {
258 $activity->updated_at = current_time('mysql');
259 $activity->save();
260 do_action('fluent_community/course/completed', $course, $userId);
261 return true;
262 }
263 return false;
264 }
265
266 Activity::create([
267 'feed_id' => $course->id,
268 'user_id' => $userId,
269 'action_name' => 'course_completed'
270 ]);
271
272 do_action('fluent_community/course/completed', $course, $userId);
273
274 return true;
275 }
276
277 public static function enrollCourse($course, $userId = null, $by = 'self', $skipSync = false)
278 {
279 return Helper::addToSpace($course, $userId, 'student', $by, $skipSync);
280 }
281
282 public static function enrollCourses($courseIds, $userId = null, $by = 'self')
283 {
284 if (!$userId) {
285 $userId = get_current_user_id();
286 }
287
288 $enrolledCourseIds = [];
289
290 foreach ($courseIds as $courseId) {
291 $course = Course::find($courseId);
292 if ($course) {
293 if (self::enrollCourse($course, $userId, $by)) {
294 $enrolledCourseIds[] = $courseId;
295 }
296 }
297 }
298
299 return $enrolledCourseIds;
300 }
301
302 public static function leaveCourse($course, $userId = null, $by = 'self')
303 {
304 if (!$userId) {
305 $userId = get_current_user_id();
306 }
307
308 if (!$userId) {
309 return false;
310 }
311
312 return Helper::removeFromSpace($course, $userId, $by);
313 }
314
315 public static function leaveCourses($courseIds, $userId = null, $by = 'self')
316 {
317 if (!$userId) {
318 $userId = get_current_user_id();
319 }
320
321 $processedCourseIds = [];
322
323 foreach ($courseIds as $courseId) {
324 $course = Course::find($courseId);
325 if ($course) {
326 if (self::leaveCourse($course, $userId, $by)) {
327 $processedCourseIds[] = $courseId;
328 }
329 }
330
331 }
332
333 return $processedCourseIds;
334 }
335
336 public static function getEnrolledCourseIds($userId = null)
337 {
338 if (!$userId) {
339 $userId = get_current_user_id();
340 }
341
342 if (!$userId) {
343 return [];
344 }
345
346 return SpaceUserPivot::where('user_id', $userId)
347 ->where('role', 'student')
348 ->pluck('space_id')
349 ->toArray();
350
351 }
352
353 public static function getSectionAccessDate($section, $courseType, $enrollment = null)
354 {
355 if ($courseType == 'self_paced') {
356 return null;
357 }
358
359 if ($courseType == 'scheduled') {
360 return $section->scheduled_at;
361 }
362
363 // This is for structured course
364 if (!$enrollment) {
365 return null;
366 }
367
368 $accessAfterEnrollment = $section->reactions_count;
369
370 return gmdate('Y-m-d H:i:s', strtotime($enrollment->created_at) + $accessAfterEnrollment * 86400);
371 }
372
373 public static function sanitizeLessonMeta($meta, $lesson)
374 {
375 $validFields = [
376 'enable_comments',
377 'enable_media',
378 'media',
379 'video_length',
380 'featured_image_id',
381 'free_preview_lesson'
382 ];
383
384 if ($lesson->isQuizType()) {
385 $quizFields = ['quiz_questions', 'passing_score', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
386 $validFields = wp_parse_args($validFields, $quizFields);
387 }
388
389 $meta = Arr::only($meta, $validFields);
390
391 $yesNoFields = ['enable_comments', 'enable_media', 'free_preview_lesson', 'enable_passing_score', 'enforce_passing_score', 'hide_result'];
392
393 foreach ($yesNoFields as $field) {
394 $meta[$field] = Arr::get($meta, $field, 'no') == 'yes' ? 'yes' : 'no';
395 }
396
397 if (Arr::get($meta, 'enable_media') == 'yes') {
398 if ($media = Arr::get($meta, 'media', [])) {
399 $meta['media'] = self::sanitizeMedia($media);
400 }
401 } else {
402 $meta['media'] = [
403 'provider' => ''
404 ];
405 }
406
407 $numericFields = ['video_length', 'passing_score'];
408
409 foreach ($numericFields as $field) {
410 $meta[$field] = absint(Arr::get($meta, $field, 0));
411 }
412
413 return apply_filters('fluent_community/lesson/sanitize_meta', $meta, $lesson);
414 }
415
416 public static function sanitizeMedia($media)
417 {
418 return array_filter([
419 'type' => sanitize_text_field(Arr::get($media, 'type', '')),
420 'url' => sanitize_url(Arr::get($media, 'url', '')),
421 'content_type' => sanitize_text_field(Arr::get($media, 'content_type', '')),
422 'provider' => sanitize_url(Arr::get($media, 'provider', '')),
423 'title' => sanitize_text_field(Arr::get($media, 'title', '')),
424 'author_name' => sanitize_text_field(Arr::get($media, 'author_name', '')),
425 'html' => CustomSanitizer::sanitizeRichText(Arr::get($media, 'html', '')),
426 'image' => sanitize_url(Arr::get($media, 'image', ''))
427 ]);
428 }
429
430 public static function getCourseCategories()
431 {
432 $terms = Term::whereHas('base_spaces', function ($q) {
433 $q->where('type', 'course');
434 })->get();
435
436 $formattedTerms = [];
437
438 foreach ($terms as $term) {
439 $formattedTerms[] = [
440 'id' => $term->id,
441 'title' => $term->title,
442 'slug' => $term->slug
443 ];
444 }
445
446 return $formattedTerms;
447 }
448
449 public static function getUserCourses($userId = null)
450 {
451 if (!$userId) {
452 $userId = get_current_user_id();
453 }
454
455 if (!$userId) {
456 return null;
457 }
458
459 return Course::whereHas('students', function ($q) use ($userId) {
460 $q->where('user_id', $userId);
461 })
462 ->with('enrollment', function ($q) use ($userId) {
463 $q->where('user_id', $userId);
464 })
465 ->get();
466 }
467
468 public static function santizeLessonBody($body)
469 {
470 if (current_user_can('unfiltered_html')) {
471 return $body;
472 }
473
474 return wp_kses_post($body);
475 }
476
477 public static function formatLessonData($course, $lesson, $user = null, $config = [])
478 {
479 $canViewLesson = Arr::get($config, 'can_view', false);
480 $parseContent = Arr::get($config, 'parse_content', true);
481
482 $inlineCss = '';
483 if ($parseContent && $canViewLesson) {
484 $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, []);
491 }
492 }
493
494 if (!$content) {
495 $content = '';
496 }
497 $content = (new SmartCodeParser())->parse($content, $user);
498 } else {
499 $content = '';
500 }
501
502 $formattedLesson = [
503 'id' => $lesson->id,
504 'title' => $lesson->title,
505 'content' => $content,
506 'slug' => $lesson->slug,
507 'course_id' => $lesson->space_id,
508 'section_id' => $lesson->parent_id,
509 'created_at' => $lesson->created_at->format('Y-m-d H:i:s'),
510 'content_type' => $lesson->content_type,
511 'featured_image' => $lesson->featured_image,
512 'meta' => $lesson->getPublicLessonMeta($canViewLesson),
513 'comments_count' => $lesson->comments_count,
514 'is_locked' => Arr::get($config, 'is_locked', false),
515 'unclock_date' => Arr::get($config, 'unclock_date', ''),
516 'can_view' => $canViewLesson,
517 'inline_css' => $inlineCss
518 ];
519
520 if (!$canViewLesson) {
521 $formattedLesson['access_message'] = static::getAccessMessage($course, $lesson, $config);
522 }
523
524 return $formattedLesson;
525 }
526
527 public static function copyLessonDocuments($lesson, $newLesson)
528 {
529 if (!$lesson || !$newLesson) {
530 return;
531 }
532
533 $newLessonMeta = $newLesson->meta;
534 $newLessonMeta['document_lists'] = [];
535 foreach ($lesson->media as $media) {
536 $newMedia = $media->replicate();
537 $newMedia->feed_id = $newLesson->id;
538 $newMedia->save();
539 if ($media->object_source == 'lesson_document') {
540 $newLessonMeta['document_lists'][] = $newMedia->getPrivateFileMeta();
541 }
542 }
543
544 if (!empty($newLessonMeta['document_lists'])) {
545 $newLesson->meta = $newLessonMeta;
546 $newLesson->save();
547 }
548 }
549
550 public static function getAccessMessage($course, $lesson, $config)
551 {
552 $isLocked = Arr::get($config, 'is_locked', false);
553 $unlockDate = Arr::get($config, 'unclock_date', '');
554 $courseLessonsUrl = Helper::baseUrl('/course/' . $course->slug . '/lessons');
555
556 $headerMessage = __('This lesson is currently locked', 'fluent-community');
557 $bodyMessage = __('Please enroll in this course to access this lesson', 'fluent-community');
558 $backToCourseText = __('Back to Course', 'fluent-community');
559
560 if ($isLocked && $unlockDate) {
561 $headerMessage = __('This lesson is not published for you yet', 'fluent-community');
562 /* translators: %s is replaced by the date */
563 $bodyMessage = sprintf(__('It will be available to you on %s', 'fluent-community'),
564 date_i18n(get_option('date_format'), strtotime($unlockDate))
565 );
566 }
567
568 /* translators: %s is replaced by the header message, %s is replaced by the body message, %s is replaced by the course lessons url, %s is replaced by the back to course text */
569 $accessMessage = sprintf('<div class="fcom_locker"><h1>%s</h1><p>%s</p><a href="%s" class="el-button el-button--info">%s</a></div>',
570 $headerMessage,
571 $bodyMessage,
572 $courseLessonsUrl,
573 $backToCourseText
574 );
575
576 return apply_filters('fluent_community/course/access_message_html', $accessMessage, $course, $lesson, $config);
577 }
578
579 public static function getParsedEmailSubject($text, $section, $user)
580 {
581 $parsedSubject = (new SmartCodeParser())->parse($text, $user, $section, false);
582
583 return $parsedSubject;
584 }
585
586 public static function getParsedEmailBody($text, $section, $user)
587 {
588 $parsedBody = (new SmartCodeParser())->parse($text, $user, $section);
589
590 $emailComposer = new \FluentCommunity\App\Services\Libs\EmailComposer();
591
592 $emailComposer->addBlock('html_content', $parsedBody);
593 $emailComposer->setDefaultLogo();
594 $emailComposer->setDefaultFooter();
595
596 return $emailComposer->getHtml();
597 }
598 }
599