PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.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 / CourseHelper.php

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

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