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/Http/Controllers/CourseAdminController.php +83 -19 2.7.72.11.0 View file →
@@ -85,9 +85,9 @@
85 85 $courseData = [
86 86 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
87 87 'title' => $request->getSafe('title', 'sanitize_text_field'),
88 88 'privacy' => $request->get('privacy'),
89 - 'description' => wp_kses_post($request->get('description')),
89 + 'description' => wp_kses_post((string) $request->get('description', '')),
90 90 'status' => $request->get('status', 'draft'),
91 91 'settings' => [
92 92 'course_type' => $request->get('course_type'),
93 93 'emoji' => CustomSanitizer::sanitizeEmoji($request->get('settings.emoji', '')),
@@ -94,9 +94,9 @@
94 94 'shape_svg' => CustomSanitizer::sanitizeSvg($request->get('settings.shape_svg', '')),
95 95 'disable_comments' => $request->get('settings.disable_comments') === 'yes' ? 'yes' : 'no',
96 96 'hide_members_count' => $request->get('settings.hide_members_count') === 'yes' ? 'yes' : 'no',
97 97 'course_layout' => $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic',
98 - 'course_details' => CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details'))),
98 + 'course_details' => CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', ''))),
99 99 'hide_instructor_view' => $request->get('settings.hide_instructor_view') === 'yes' ? 'yes' : 'no',
100 100 'show_instructor_students_count' => $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no',
101 101 'sequential_lesson_order' => $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no',
102 102 ],
@@ -229,9 +229,9 @@
229 229
230 230 $courseData = [
231 231 'title' => $request->getSafe('title', 'sanitize_text_field'),
232 232 'privacy' => $request->get('privacy'),
233 - 'description' => wp_kses_post($request->get('description')),
233 + 'description' => wp_kses_post((string) $request->get('description', '')),
234 234 'status' => $request->get('status'),
235 235 'cover_photo' => $request->getSafe('cover_photo', 'sanitize_url'),
236 236 'parent_id' => $request->get('parent_id') ?: null, // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
237 237 ];
@@ -303,9 +303,9 @@
303 303 $existingSettings['show_instructor_students_count'] = $request->get('settings.show_instructor_students_count') === 'yes' ? 'yes' : 'no';
304 304 $existingSettings['show_paywalls'] = $request->get('settings.show_paywalls') === 'yes' ? 'yes' : 'no';
305 305 $existingSettings['show_welcome_banner'] = $request->get('settings.show_welcome_banner') === 'yes' ? 'yes' : 'no';
306 306 $existingSettings['course_layout'] = $request->get('settings.course_layout') === 'modern' ? 'modern' : 'classic';
307 - $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim($request->get('settings.course_details')));
307 + $existingSettings['course_details'] = CustomSanitizer::unslashMarkdown(trim((string) $request->get('settings.course_details', '')));
308 308 $existingSettings['sequential_lesson_order'] = $request->get('settings.sequential_lesson_order') === 'yes' ? 'yes' : 'no';
309 309
310 310 if ($request->get('privacy') == 'public' && $existingSettings['course_type'] == 'self_paced') {
311 311 $existingSettings['public_lesson_view'] = $request->get('settings.public_lesson_view') == 'yes' ? 'yes' : 'no';
@@ -383,8 +383,10 @@
383 383 $newTopic = $topic->replicate();
384 384 $newTopic->space_id = $newCourse->id;
385 385 $newTopic->save();
386 386
387 + do_action('fluent_community/section/created', $newTopic, $newCourse);
388 +
387 389 foreach ($topic->lessons as $lesson) {
388 390 /** @var CourseLesson $lesson */
389 391 $newLesson = $lesson->replicate();
390 392 $newLesson->space_id = $newCourse->id;
@@ -390,8 +392,10 @@
390 392 $newLesson->space_id = $newCourse->id;
391 393 $newLesson->parent_id = $newTopic->id;
392 394 $newLesson->save();
393 395 CourseHelper::copyLessonDocuments($lesson, $newLesson);
396 +
397 + do_action('fluent_community/lesson/created', $newLesson, $newTopic);
394 398 }
395 399 }
396 400
397 401 do_action('fluent_community/course/created', $newCourse);
@@ -487,13 +491,27 @@
487 491 Course::findOrFail($courseId);
488 492
489 493 $search = $request->getSafe('search');
490 494
491 - $students = XProfile::whereHas('space_pivot', function ($q) use ($courseId) {
492 - return $q->where('space_id', $courseId)
493 - ->where('role', 'student');
494 - })
495 - ->searchBy($search)
495 + $defaultDirections = [
496 + 'display_name' => 'ASC',
497 + 'created_at' => 'DESC',
498 + 'last_activity' => 'DESC',
499 + ];
500 +
501 + $sortBy = $request->getSafe('sort_by', 'sanitize_text_field', 'created_at');
502 + $sortColumn = in_array($sortBy, array_keys($defaultDirections), true) ? $sortBy : 'created_at';
503 + $sortDir = strtoupper($request->getSafe('sort_dir', 'sanitize_text_field', ''));
504 + $sortDirection = in_array($sortDir, ['ASC', 'DESC'], true) ? $sortDir : $defaultDirections[$sortColumn];
505 + $orderColumn = $sortColumn === 'created_at'
506 + ? 'fcom_space_user.created_at'
507 + : 'fcom_xprofile.' . $sortColumn;
508 +
509 + $publicFields = array_map(function ($field) {
510 + return 'fcom_xprofile.' . $field;
511 + }, ProfileHelper::getXProfilePublicFields());
512 +
513 + $students = XProfile::searchBy($search)
496 514 ->whereHas('user')
497 515 ->with([
498 516 'space_pivot' => function ($q) use ($courseId) {
499 517 return $q->where('space_id', $courseId);
@@ -498,9 +516,15 @@
498 516 'space_pivot' => function ($q) use ($courseId) {
499 517 return $q->where('space_id', $courseId);
500 518 },
501 519 ])
502 - ->select(ProfileHelper::getXProfilePublicFields())
520 + ->join('fcom_space_user', function ($join) use ($courseId) {
521 + $join->on('fcom_space_user.user_id', '=', 'fcom_xprofile.user_id')
522 + ->where('fcom_space_user.space_id', $courseId)
523 + ->where('fcom_space_user.role', 'student');
524 + })
525 + ->select($publicFields)
526 + ->orderBy($orderColumn, $sortDirection)
503 527 ->paginate();
504 528
505 529 $studentUserIds = $students->pluck('user_id')->toArray();
506 530
@@ -583,9 +607,13 @@
583 607 'message' => __('This student is not enrolled in this course.', 'fluent-community'),
584 608 ]);
585 609 }
586 610
587 - CourseHelper::resetCourseProgress($courseId, (int) $studentId);
611 + if (!CourseHelper::resetCourseProgress($courseId, (int) $studentId)) {
612 + return $this->sendError([
613 + 'message' => __("Student's progress could not be reset. Please try again.", 'fluent-community'),
614 + ]);
615 + }
588 616
589 617 return [
590 618 'message' => __("Student's progress has been reset.", 'fluent-community'),
591 619 ];
@@ -691,14 +719,13 @@
691 719 $lessonId = $request->getSafe('lesson_id', 'intval');
692 720 $sectionId = $request->getSafe('section_id', 'intval');
693 721
694 722 Course::findOrFail($courseId);
695 - CourseTopic::findOrFail($sectionId);
723 + $section = CourseTopic::where('space_id', $courseId)->findOrFail($sectionId);
724 + $lesson = CourseLesson::where('space_id', $courseId)->findOrFail($lessonId);
696 725
697 - $lesson = CourseLesson::findOrFail($lessonId);
698 -
699 726 $lesson->update([
700 - 'parent_id' => $sectionId,
727 + 'parent_id' => $section->id,
701 728 ]);
702 729
703 730 return [
704 731 'message' => __('Lesson has been moved successfully', 'fluent-community'),
@@ -716,9 +743,9 @@
716 743 'space_id' => $courseId,
717 744 'status' => 'published',
718 745 ];
719 746
720 - Course::findOrFail($courseId);
747 + $course = Course::findOrFail($courseId);
721 748
722 749 $latestPriority = CourseTopic::where('type', 'course_section')->where('space_id', $courseId)->max('priority');
723 750
724 751 $sectionData['priority'] = (int) $latestPriority + 1;
@@ -726,8 +753,10 @@
726 753 $section = CourseTopic::create($sectionData);
727 754
728 755 $section->load('lessons');
729 756
757 + do_action('fluent_community/section/created', $section, $course);
758 +
730 759 return [
731 760 'message' => __('Section has been created successfully.', 'fluent-community'),
732 761 'section' => $section,
733 762 ];
@@ -849,8 +878,10 @@
849 878 $newSection->space_id = $toCourse->id;
850 879 $newSection->priority = (int) $latestPriority + 1;
851 880 $newSection->save();
852 881
882 + do_action('fluent_community/section/created', $newSection, $toCourse);
883 +
853 884 $originalLessons = CourseLesson::where('parent_id', $originalSection->id)->get();
854 885 foreach ($originalLessons as $lesson) {
855 886 /** @var CourseLesson $lesson */
856 887 $newLesson = $lesson->replicate();
@@ -857,8 +888,10 @@
857 888 $newLesson->space_id = $toCourse->id;
858 889 $newLesson->parent_id = $newSection->id;
859 890 $newLesson->save();
860 891 CourseHelper::copyLessonDocuments($lesson, $newLesson);
892 +
893 + do_action('fluent_community/lesson/created', $newLesson, $newSection);
861 894 }
862 895
863 896 $newSection->load('lessons');
864 897
@@ -932,8 +965,11 @@
932 965
933 966 return apply_filters('fluent_community/admin_course_lesson_api_response', $data, $request->all());
934 967 }
935 968
969 + /**
970 + * Expects `title` and `section_id` at the top level of the request.
971 + */
936 972 public function createLesson(Request $request, $courseId)
937 973 {
938 974 $this->validate($request->all(), [
939 975 'title' => 'required',
@@ -967,8 +1003,10 @@
967 1003 $lesson = CourseLesson::create($lessonData);
968 1004
969 1005 $lesson = CourseLesson::findOrFail($lesson->id);
970 1006
1007 + do_action('fluent_community/lesson/created', $lesson, $topic);
1008 +
971 1009 return [
972 1010 'message' => __('Lesson has been created successfully.', 'fluent-community'),
973 1011 'lesson' => $lesson,
974 1012 ];
@@ -973,13 +1011,16 @@
973 1011 'lesson' => $lesson,
974 1012 ];
975 1013 }
976 1014
1015 + /**
1016 + * Expects the fields nested under `lesson`, referencing the section as `parent_id`.
1017 + */
977 1018 public function updateLesson(Request $request, $courseId, $lessionId)
978 1019 {
979 1020 Course::findOrFail($courseId);
980 1021
981 - $lessonData = $request->get('lesson');
1022 + $lessonData = (array)$request->get('lesson');
982 1023
983 1024 $this->validate($lessonData, [
984 1025 'title' => 'required',
985 1026 'parent_id' => 'required',
@@ -1043,8 +1084,12 @@
1043 1084 if ($dirtyFields) {
1044 1085 $lesson->save();
1045 1086 $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1046 1087 do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1088 +
1089 + if ($isNewlyPublished) {
1090 + do_action('fluent_community/lesson/published', $lesson);
1091 + }
1047 1092 }
1048 1093
1049 1094 do_action('fluent_community/lesson/additional_media_updated', $request->all(), $lesson, $updateData);
1050 1095
@@ -1061,8 +1106,10 @@
1061 1106 })
1062 1107 ->where('id', $lessionId)
1063 1108 ->firstOrFail();
1064 1109
1110 + $previousStatus = $lesson->status;
1111 +
1065 1112 $acceptedFields = [ 'title', 'status', 'slug' ];
1066 1113
1067 1114 // empty title/slug/status must not overwrite, but a literal "0" is a valid value
1068 1115 // Request::only() is a plain array pick with no sanitization, so each field is
@@ -1076,9 +1123,16 @@
1076 1123 $lessonData['title'] = sanitize_text_field($lessonData['title']);
1077 1124 }
1078 1125
1079 1126 if (isset($lessonData['slug'])) {
1080 - $lessonData['slug'] = sanitize_title($lessonData['slug']);
1127 + // sanitize_title() alone let an author set a slug already taken by a
1128 + // sibling lesson, which getLessonBySlug() then resolves arbitrarily.
1129 + $lessonData['slug'] = CourseLesson::uniqueSlug(
1130 + $lessonData['slug'],
1131 + $lesson->space_id,
1132 + $lesson->id,
1133 + Arr::get($lessonData, 'title', $lesson->title)
1134 + );
1081 1135 }
1082 1136
1083 1137 if (isset($lessonData['status']) && !in_array($lessonData['status'], ['draft', 'published', 'archived'], true)) {
1084 1138 unset($lessonData['status']);
@@ -1096,10 +1150,19 @@
1096 1150 }
1097 1151
1098 1152 if (!empty($lessonData)) {
1099 1153 $lesson->fill($lessonData);
1100 - if ($lesson->isDirty()) {
1154 + $dirtyFields = $lesson->getDirty();
1155 +
1156 + if ($dirtyFields) {
1101 1157 $lesson->save();
1158 +
1159 + $isNewlyPublished = $lesson->status === 'published' && $previousStatus !== 'published';
1160 + do_action('fluent_community/lesson/updated', $lesson, $dirtyFields, $isNewlyPublished);
1161 +
1162 + if ($isNewlyPublished) {
1163 + do_action('fluent_community/lesson/published', $lesson);
1164 + }
1102 1165 }
1103 1166 }
1104 1167
1105 1168 return [
@@ -1174,8 +1237,9 @@
1174 1237 $newLesson = CourseLesson::findOrFail($newLesson->id);
1175 1238
1176 1239 CourseHelper::copyLessonDocuments($lesson, $newLesson);
1177 1240
1241 + do_action('fluent_community/lesson/created', $newLesson, $newLesson->topic);
1178 1242 do_action('fluent_community/lesson/duplicated', $newLesson, $lesson);
1179 1243
1180 1244 return [
1181 1245 'message' => __('Lesson has been duplicated successfully.', 'fluent-community'),