| @@ -2,16 +2,20 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace FluentCommunity\Modules\Course\Model; |
| 4 | 4 | |
| 5 | 5 | |
| 6 | +use FluentCommunity\App\Functions\Utility; | |
| 6 | 7 | use FluentCommunity\App\Models\Model; |
| 7 | 8 | use FluentCommunity\App\Models\Reaction; |
| 8 | 9 | use FluentCommunity\App\Models\Term; |
| 9 | 10 | use FluentCommunity\App\Models\User; |
| 11 | +use FluentCommunity\App\Models\Media; | |
| 12 | +use FluentCommunity\App\Models\Comment; | |
| 10 | 13 | use FluentCommunity\App\Services\Helper; |
| 14 | +use FluentCommunity\Framework\Support\Arr; | |
| 11 | 15 | |
| 12 | 16 | /** |
| 13 | - * Clourse Lesson Model - DB Model for Individual Clourse Lesson | |
| 17 | + * Course Lesson Model - DB Model for Individual Course Lesson | |
| 14 | 18 | * |
| 15 | 19 | * Database Model |
| 16 | 20 | * |
| 17 | 21 | * @package FluentCrm\App\Models |
| @@ -16,18 +20,47 @@ | ||
| 16 | 20 | * |
| 17 | 21 | * @package FluentCrm\App\Models |
| 18 | 22 | * |
| 19 | 23 | * @version 1.1.0 |
| 24 | + * | |
| 25 | + * @property int $id | |
| 26 | + * @property int|null $user_id | |
| 27 | + * @property string|null $title | |
| 28 | + * @property string|null $slug | |
| 29 | + * @property string|null $message | |
| 30 | + * @property string|null $message_rendered | |
| 31 | + * @property string|null $type | |
| 32 | + * @property int|null $space_id | |
| 33 | + * @property string|null $privacy | |
| 34 | + * @property string|null $status | |
| 35 | + * @property string|null $featured_image | |
| 36 | + * @property int|null $is_sticky | |
| 37 | + * @property string|null $scheduled_at | |
| 38 | + * @property string|null $expired_at | |
| 39 | + * @property string|null $content_type | |
| 40 | + * @property int $comments_count | |
| 41 | + * @property int $reactions_count | |
| 42 | + * @property array $meta | |
| 43 | + * @property int|null $priority | |
| 44 | + * @property int|null $parent_id | |
| 45 | + * @property string|null $created_at | |
| 46 | + * @property string|null $updated_at | |
| 47 | + * @property-read array $questions | |
| 48 | + * @property-read array $enabled_questions | |
| 49 | + * @property-read bool $is_enforce_pass | |
| 50 | + * @property-read bool $is_free_preview | |
| 51 | + * @property-read int $passing_score | |
| 52 | + * @property-read Course|null $course | |
| 20 | 53 | */ |
| 21 | 54 | class CourseLesson extends Model |
| 22 | 55 | { |
| 23 | 56 | protected $table = 'fcom_posts'; |
| 24 | 57 | |
| 25 | - protected $guarded = ['id']; | |
| 58 | + protected $guarded = [ 'id' ]; | |
| 26 | 59 | |
| 27 | 60 | protected $casts = [ |
| 28 | 61 | 'comments_count' => 'int', |
| 29 | - 'reactions_count' => 'int' | |
| 62 | + 'reactions_count' => 'int', | |
| 30 | 63 | ]; |
| 31 | 64 | |
| 32 | 65 | protected $fillable = [ |
| 33 | 66 | 'user_id', |
| @@ -40,22 +73,25 @@ | ||
| 40 | 73 | 'privacy', |
| 41 | 74 | 'status', |
| 42 | 75 | 'featured_image', |
| 43 | 76 | 'is_sticky', |
| 77 | + 'scheduled_at', | |
| 44 | 78 | 'expired_at', |
| 79 | + 'content_type', | |
| 45 | 80 | 'comments_count', |
| 46 | 81 | 'reactions_count', |
| 47 | 82 | 'meta', |
| 48 | - 'parent_id' | |
| 83 | + 'priority', | |
| 84 | + 'parent_id', | |
| 49 | 85 | ]; |
| 50 | 86 | |
| 51 | 87 | protected $searchable = [ |
| 52 | 88 | 'message', |
| 53 | - 'title' | |
| 89 | + 'title', | |
| 54 | 90 | ]; |
| 55 | 91 | |
| 56 | 92 | public static $publicColumns = [ |
| 57 | - 'id', 'slug', 'title', 'message_rendered', 'featured_image', 'created_at', 'privacy', 'type', 'status', 'slug', 'space_id', 'user_id', 'meta', 'comments_count', 'reactions_count' | |
| 93 | + 'id', 'slug', 'title', 'message_rendered', 'featured_image', 'created_at', 'privacy', 'type', 'status', 'slug', 'space_id', 'user_id', 'meta', 'content_type', 'comments_count', 'reactions_count', | |
| 58 | 94 | ]; |
| 59 | 95 | |
| 60 | 96 | protected static $type = 'course_lesson'; |
| 61 | 97 | |
| @@ -70,8 +106,16 @@ | ||
| 70 | 106 | } |
| 71 | 107 | |
| 72 | 108 | $model->type = self::$type; |
| 73 | 109 | |
| 110 | + if (empty($model->content_type)) { | |
| 111 | + $model->content_type = 'text'; | |
| 112 | + } | |
| 113 | + | |
| 114 | + if (empty($model->message)) { | |
| 115 | + $model->message = ''; | |
| 116 | + } | |
| 117 | + | |
| 74 | 118 | if (empty($model->meta)) { |
| 75 | 119 | $model->meta = self::getDefaultMeta(); |
| 76 | 120 | } |
| 77 | 121 | }); |
| @@ -87,24 +131,55 @@ | ||
| 87 | 131 | 'media' => [ |
| 88 | 132 | 'type' => 'oembed', |
| 89 | 133 | 'url' => '', |
| 90 | 134 | 'content_type' => 'video', |
| 91 | - 'html' => '' | |
| 135 | + 'html' => '', | |
| 92 | 136 | ], |
| 93 | 137 | 'enable_comments' => 'yes', |
| 94 | - 'enable_media' => 'yes' | |
| 138 | + 'enable_media' => 'yes', | |
| 139 | + 'document_lists' => [], | |
| 95 | 140 | ]; |
| 96 | 141 | } |
| 97 | 142 | |
| 98 | - protected static function generateNewSlug($newModel) | |
| 143 | + /** | |
| 144 | + * A lesson slug only has to be unique among the lessons of one course. | |
| 145 | + * | |
| 146 | + * A lesson is read at course/{courseSlug}/lessons/{lessonSlug} and | |
| 147 | + * CourseController::getLessonBySlug() looks it up with a space_id filter, so the | |
| 148 | + * same slug in two courses never competes. The type global scope keeps this off | |
| 149 | + * the other row types sharing fcom_posts. | |
| 150 | + * | |
| 151 | + * Every write path goes through here - the creating hook below for generated | |
| 152 | + * slugs, CourseAdminController::patchLesson() for author supplied ones. A | |
| 153 | + * collision gets a -{time()} suffix. | |
| 154 | + * | |
| 155 | + * @param string $slug | |
| 156 | + * @param int|null $courseId the owning course, fcom_posts.space_id | |
| 157 | + * @param int|null $ignoreId the lesson being renamed, so it can keep its own slug | |
| 158 | + * @param string $fallbackTitle used when $slug sanitizes down to nothing | |
| 159 | + * @return string | |
| 160 | + */ | |
| 161 | + public static function uniqueSlug($slug, $courseId, $ignoreId = null, $fallbackTitle = '') | |
| 99 | 162 | { |
| 100 | - $slug = sanitize_title($newModel->title); | |
| 163 | + $slug = sanitize_title($slug); | |
| 101 | 164 | |
| 102 | - // check if the slug is available for this type | |
| 103 | - $exist = self::where('slug', $slug) | |
| 104 | - ->exists(); | |
| 165 | + if (!$slug) { | |
| 166 | + $slug = Utility::slugify($fallbackTitle, 'lesson-' . time()); | |
| 167 | + } | |
| 105 | 168 | |
| 106 | - if ($exist) { | |
| 169 | + $query = self::where('slug', $slug); | |
| 170 | + | |
| 171 | + if ($courseId) { | |
| 172 | + $query->where('space_id', $courseId); | |
| 173 | + } else { | |
| 174 | + $query->whereNull('space_id'); | |
| 175 | + } | |
| 176 | + | |
| 177 | + if ($ignoreId) { | |
| 178 | + $query->where('id', '!=', $ignoreId); | |
| 179 | + } | |
| 180 | + | |
| 181 | + if ($query->exists()) { | |
| 107 | 182 | $slug = $slug . '-' . time(); |
| 108 | 183 | } |
| 109 | 184 | |
| 110 | 185 | return $slug; |
| @@ -109,8 +184,13 @@ | ||
| 109 | 184 | |
| 110 | 185 | return $slug; |
| 111 | 186 | } |
| 112 | 187 | |
| 188 | + protected static function generateNewSlug($newModel) | |
| 189 | + { | |
| 190 | + return self::uniqueSlug('', $newModel->space_id, null, $newModel->title); | |
| 191 | + } | |
| 192 | + | |
| 113 | 193 | public function topic() |
| 114 | 194 | { |
| 115 | 195 | return $this->belongsTo(CourseTopic::class, 'parent_id'); |
| 116 | 196 | } |
| @@ -116,9 +196,9 @@ | ||
| 116 | 196 | } |
| 117 | 197 | |
| 118 | 198 | public function course() |
| 119 | 199 | { |
| 120 | - return $this->belongsTo(Course::class, 'space_id'); | |
| 200 | + return $this->belongsTo(Course::class, 'space_id', 'id'); | |
| 121 | 201 | } |
| 122 | 202 | |
| 123 | 203 | public function setMetaAttribute($value) |
| 124 | 204 | { |
| @@ -126,9 +206,9 @@ | ||
| 126 | 206 | } |
| 127 | 207 | |
| 128 | 208 | public function getMetaAttribute($value) |
| 129 | 209 | { |
| 130 | - $meta = maybe_unserialize($value); | |
| 210 | + $meta = Utility::safeUnserialize($value); | |
| 131 | 211 | |
| 132 | 212 | if (!$meta) { |
| 133 | 213 | $meta = self::getDefaultMeta(); |
| 134 | 214 | } |
| @@ -135,8 +215,38 @@ | ||
| 135 | 215 | |
| 136 | 216 | return $meta; |
| 137 | 217 | } |
| 138 | 218 | |
| 219 | + public function getQuestionsAttribute() | |
| 220 | + { | |
| 221 | + return Arr::get($this->meta, 'quiz_questions', []); | |
| 222 | + } | |
| 223 | + | |
| 224 | + public function getEnabledQuestionsAttribute() | |
| 225 | + { | |
| 226 | + return array_filter($this->questions, function ($question) { | |
| 227 | + return Arr::isTrue($question, 'enabled'); | |
| 228 | + }); | |
| 229 | + } | |
| 230 | + | |
| 231 | + public function getIsEnforcePassAttribute() | |
| 232 | + { | |
| 233 | + return Arr::isTrue($this->meta, 'enforce_passing_score'); | |
| 234 | + } | |
| 235 | + | |
| 236 | + public function getIsFreePreviewAttribute() | |
| 237 | + { | |
| 238 | + return Arr::isTrue($this->meta, 'free_preview_lesson'); | |
| 239 | + } | |
| 240 | + | |
| 241 | + public function getPassingScoreAttribute() | |
| 242 | + { | |
| 243 | + if (Arr::isTrue($this->meta, 'enable_passing_score')) { | |
| 244 | + return Arr::get($this->meta, 'passing_score', 0); | |
| 245 | + } | |
| 246 | + return 0; | |
| 247 | + } | |
| 248 | + | |
| 139 | 249 | public function scopeSearchBy($query, $search) |
| 140 | 250 | { |
| 141 | 251 | if (!$search) { |
| 142 | 252 | return $query; |
| @@ -157,22 +267,44 @@ | ||
| 157 | 267 | { |
| 158 | 268 | return $this->belongsTo(User::class, 'user_id', 'ID'); |
| 159 | 269 | } |
| 160 | 270 | |
| 271 | + public function comments() | |
| 272 | + { | |
| 273 | + return $this->hasMany(Comment::class, 'post_id', 'id'); | |
| 274 | + } | |
| 275 | + | |
| 161 | 276 | public function reactions() |
| 162 | 277 | { |
| 278 | + return $this->hasMany(Reaction::class, 'parent_id', 'id') | |
| 279 | + ->where('object_type', 'comment'); | |
| 280 | + } | |
| 281 | + | |
| 282 | + public function lessonCompleted() | |
| 283 | + { | |
| 163 | 284 | return $this->hasMany(Reaction::class, 'object_id', 'id') |
| 164 | - ->where('object_type', 'feed'); | |
| 285 | + ->where('object_type', 'lesson_completed'); | |
| 165 | 286 | } |
| 166 | 287 | |
| 288 | + public function media() | |
| 289 | + { | |
| 290 | + return $this->hasMany(Media::class, 'feed_id', 'id'); | |
| 291 | + } | |
| 292 | + | |
| 167 | 293 | public function terms() |
| 168 | 294 | { |
| 169 | 295 | return $this->belongsToMany(Term::class, 'fcom_term_feed', 'post_id', 'term_id'); |
| 170 | 296 | } |
| 171 | 297 | |
| 298 | + public function isQuizType() | |
| 299 | + { | |
| 300 | + return $this->content_type == 'quiz'; | |
| 301 | + } | |
| 302 | + | |
| 172 | 303 | public function getPermalink() |
| 173 | 304 | { |
| 174 | - return Helper::baseUrl() . '/course/' . $this->course ? $this->course->slug : 'undefined' . '/lessons/' . $this->slug . '/view'; | |
| 305 | + $uri = '/course/' . ($this->course ? $this->course->slug : 'undefined') . '/lessons/' . $this->slug . '/view'; | |
| 306 | + return Helper::baseUrl($uri); | |
| 175 | 307 | } |
| 176 | 308 | |
| 177 | 309 | public function hasUserReact($userId, $type = 'like') |
| 178 | 310 | { |
| @@ -180,9 +312,9 @@ | ||
| 180 | 312 | return false; |
| 181 | 313 | } |
| 182 | 314 | |
| 183 | 315 | return (bool)Reaction::where('object_id', $this->id) |
| 184 | - ->select(['id']) | |
| 316 | + ->select([ 'id' ]) | |
| 185 | 317 | ->where('object_type', 'feed') |
| 186 | 318 | ->where('user_id', $userId) |
| 187 | 319 | ->where('type', $type) |
| 188 | 320 | ->first(); |
| @@ -197,11 +329,11 @@ | ||
| 197 | 329 | if (!$content) { |
| 198 | 330 | return ''; |
| 199 | 331 | } |
| 200 | 332 | // remove all tags |
| 201 | - $content = strip_tags($content); | |
| 333 | + $content = wp_strip_all_tags($content); | |
| 202 | 334 | // remove new lines and tabs |
| 203 | - $content = str_replace(["\r", "\n", "\t"], ' ', $content); | |
| 335 | + $content = str_replace([ "\r", "\n", "\t" ], ' ', $content); | |
| 204 | 336 | // remove multiple spaces |
| 205 | 337 | $content = preg_replace('/\s+/', ' ', $content); |
| 206 | 338 | |
| 207 | 339 | // trim |
| @@ -213,6 +345,31 @@ | ||
| 213 | 345 | } |
| 214 | 346 | |
| 215 | 347 | // return the first $length chars of the content with ... at the end |
| 216 | 348 | return mb_substr($content, 0, $length) . '...'; |
| 349 | + } | |
| 350 | + | |
| 351 | + public function getPublicLessonMeta($canView = true) | |
| 352 | + { | |
| 353 | + $meta = $this->meta; | |
| 354 | + | |
| 355 | + if(!empty($meta['document_lists'])) { | |
| 356 | + $docLists = $meta['document_lists']; | |
| 357 | + foreach ($docLists as $index => $docList) { | |
| 358 | + if(!empty($docList['media_key']) && !empty($docList['id'])) { | |
| 359 | + $docLists[$index]['url'] = Helper::baseUrl('?fcom_action=download_document&media_key='.$docList['media_key'].'&media_id='.$docList['id']); | |
| 360 | + } | |
| 361 | + } | |
| 362 | + $meta['document_lists'] = $docLists; | |
| 363 | + } | |
| 364 | + | |
| 365 | + if(!$canView) { | |
| 366 | + $meta['document_lists'] = []; | |
| 367 | + $meta['document_ids'] = []; | |
| 368 | + unset($meta['media']); | |
| 369 | + } | |
| 370 | + | |
| 371 | + $meta = apply_filters('fluent_community/lesson/get_public_meta', $meta, $this); | |
| 372 | + | |
| 373 | + return $meta; | |
| 217 | 374 | } |
| 218 | 375 | } |