Course::getPriceAttribute()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace App\Models;
4
5
use App\Events\CourseCreated;
6
use App\Events\CourseUpdated;
7
use App\Models\Partner;
8
use App\Models\Skills\Skill;
9
use Backpack\CRUD\app\Models\Traits\CrudTrait;
10
use Carbon\Carbon;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Support\Facades\App;
13
use Illuminate\Support\Facades\Log;
14
use Illuminate\Support\Str;
15
use Spatie\Activitylog\Traits\LogsActivity;
16
17
/**
18
 * @mixin IdeHelperCourse
19
 */
20
class Course extends Model
21
{
22
    use CrudTrait;
0 ignored issues
show
introduced by
The trait Backpack\CRUD\app\Models\Traits\CrudTrait requires some properties which are not provided by App\Models\Course: $fakeColumns, $identifiableAttribute, $Type
Loading history...
23
    use LogsActivity;
24
25
    protected $dispatchesEvents = [
26
        'updated' => CourseUpdated::class,
27
        'created' => CourseCreated::class,
28
    ];
29
30
    /*
31
    |--------------------------------------------------------------------------
32
    | GLOBAL VARIABLES
33
    |--------------------------------------------------------------------------
34
    */
35
    public $timestamps = true;
36
37
    protected $guarded = ['id'];
38
39
    protected $dates = ['start_date', 'end_date'];
40
41
    protected $with = ['times', 'evaluationType'];
42
43
    protected $appends = [
44
        'course_times',
45
        'course_teacher_name',
46
        'course_period_name',
47
        'course_enrollments_count',
48
        'accepts_new_students',
49
        'takes_attendance',
50
        'sortable_id',
51
    ];
52
53
    protected static bool $logUnguarded = true;
54
55
    /*
56
    |--------------------------------------------------------------------------
57
    | SCOPES
58
    |--------------------------------------------------------------------------
59
    */
60
61
    /** filter only the courses that have no parent */
62
    public function scopeParent($query)
63
    {
64
        return $query->where('parent_course_id', null);
65
    }
66
67
    public function scopeChildren($query)
68
    {
69
        return $query->where('parent_course_id', '!=', null);
70
    }
71
72
    public function scopeInternal($query)
73
    {
74
        return $query->where('campus_id', 1);
75
    }
76
77
    public function scopeExternal($query)
78
    {
79
        return $query->where('campus_id', 2);
80
    }
81
82
    public function scopeRealcourses($query)
83
    {
84
        return $query->doesntHave('children');
85
    }
86
87
    /*
88
    |--------------------------------------------------------------------------
89
    | FUNCTIONS
90
    |--------------------------------------------------------------------------
91
    */
92
93
    /** returns all courses that are open for enrollments */
94
    public static function get_available_courses(Period $period)
95
    {
96
        return self::where('period_id', $period->id)
97
        ->where('campus_id', 1)
98
        ->with('times')
99
        ->with('teacher')
100
        ->with('room')
101
        ->with('rhythm')
102
        ->with('level')
103
        ->withCount('enrollments')
104
        ->get();
105
    }
106
107
    /*
108
    |--------------------------------------------------------------------------
109
    | RELATIONS
110
    |--------------------------------------------------------------------------
111
    */
112
113
    /** the scheduled day/times for the course, that repeat throughout the course date span */
114
    public function times()
115
    {
116
        return $this->hasMany(CourseTime::class, 'course_id');
117
    }
118
119
    /** course sessions (classes) with a specific start and end date/time */
120
    public function events()
121
    {
122
        return $this->hasMany(Event::class)->orderBy('start');
123
    }
124
125
    public function remoteEvents()
126
    {
127
        return $this->hasMany(RemoteEvent::class);
128
    }
129
130
    /** may be null if the teacher is not yet assigned */
131
    public function teacher()
132
    {
133
        return $this->belongsTo(Teacher::class)->withTrashed();
134
    }
135
136
    public function campus()
137
    {
138
        return $this->belongsTo(Campus::class);
139
    }
140
141
    /** may be null if the room is not yet assigned */
142
    public function room()
143
    {
144
        return $this->belongsTo(Room::class)->withTrashed();
145
    }
146
147
    /** the "category" of course */
148
    public function rhythm()
149
    {
150
        return $this->belongsTo(Rhythm::class)->withTrashed();
151
    }
152
153
    /** a course can only have one level. Parent courses would generally have no level defined */
154
    public function level()
155
    {
156
        return $this->belongsTo(Level::class)->withTrashed();
157
    }
158
159
    /** a course needs to belong to a period */
160
    public function period()
161
    {
162
        return $this->belongsTo(Period::class);
163
    }
164
165
    /** children courses = sub-courses, or course modules */
166
    public function children()
167
    {
168
        return $this->hasMany(self::class, 'parent_course_id');
169
    }
170
171
    public function parent()
172
    {
173
        return $this->belongsTo(self::class, 'parent_course_id');
174
    }
175
176
    /** evaluation methods associated to the course - grades, skill-based evaluation... */
177
    public function evaluationType()
178
    {
179
        return $this->belongsTo(EvaluationType::class);
180
    }
181
182
    /** a Grade model = an individual grade, belongs to a student */
183
    public function grades()
184
    {
185
        return $this->hasManyThrough(Grade::class, Enrollment::class);
186
    }
187
188
    /** the different grade types associated to the course, ie. criteria that will receive the grades */
189
    public function grade_types()
190
    {
191
        if ($this->evaluationType) {
192
            return $this->evaluationType->gradeTypes()->orderBy('order');
193
        }
194
195
        return GradeType::query();
196
    }
197
198
    /** in the case of skills-based evaluation, Skill models are attached to the course
199
     * This represents the "criteria" that will need to be evaluated to each student (enrollment) in the course.
200
     */
201
    public function skills()
202
    {
203
        return $this->evaluationType?->skills()->orderBy('order');
204
    }
205
206
    public function books()
207
    {
208
        return $this->belongsToMany(Book::class);
209
    }
210
211
    /**
212
     * return attendance records associated to the course
213
     * Since the attendance records are linked to the event, we use a hasManyThrough relation.
214
     */
215
    public function attendance()
216
    {
217
        return $this->hasManyThrough(Attendance::class, Event::class);
218
    }
219
220
    /**
221
     * Return events for which the attendance records do not match the course student count.
222
     *
223
     * todo - optimize this method (reduce the number of queries and avoid the foreach loop)
224
     * but filtering the collection increases the number of DB queries... (why ?)
225
     */
226
    public function getPendingAttendanceAttribute()
227
    {
228
        $events = Event::where(function ($query) {
229
            $query->where('course_id', $this->id);
230
            $query->where('exempt_attendance', '!=', true);
231
            $query->where('exempt_attendance', '!=', 1);
232
            $query->orWhereNull('exempt_attendance');
233
        })
234
        ->where('course_id', '!=', null)
235
        ->with('attendance')
236
        ->with('teacher')
237
        ->with('course.enrollments')
238
        ->where('start', '<', Carbon::now(config('settings.courses_timezone'))->toDateTimeString())
239
        ->get();
240
241
        $pending_events = [];
242
243
        foreach ($events as $event) {
244
            // if the attendance record count do not match the enrollment count, push the event to array
245
            $pending_attendance = $event->course->enrollments->count() - $event->attendance->count();
246
247
            if ($pending_attendance != 0) {
248
                $pending_events[$event->id]['event'] = $event->name ?? '';
249
                $pending_events[$event->id]['event_id'] = $event->id;
250
                $pending_events[$event->id]['course_id'] = $event->course_id;
251
                $pending_events[$event->id]['event_date'] = Carbon::parse($event->start)->toDateString();
252
                $pending_events[$event->id]['teacher'] = $event->teacher->name ?? '';
253
                $pending_events[$event->id]['pending'] = $pending_attendance ?? '';
254
            }
255
        }
256
257
        return $pending_events;
258
    }
259
260
    public function enrollments()
261
    {
262
        return $this
263
            ->hasMany(Enrollment::class, 'course_id', 'id')
264
            ->whereIn('status_id', [1, 2]) // pending or paid enrollments only
265
            ->with('student');
266
    }
267
268
    /** returns only pending or paid enrollments, without the child enrollments */
269
    public function real_enrollments()
270
    {
271
        return $this->hasMany(Enrollment::class, 'course_id', 'id')
272
        ->whereIn('status_id', ['1', '2']) // pending or paid
273
        ->where('parent_id', null);
274
    }
275
276
    public function partner()
277
    {
278
        return $this->belongsTo(Partner::class);
279
    }
280
281
    public function saveCourseTimes($newCourseTimes)
282
    {
283
        // before updating, retrieve existing course times
284
        $oldCourseTimes = $this->times;
285
286
        // check existing coursetimes
287
        foreach ($oldCourseTimes as $oldCourseTime) {
288
            $newCourseTime = $newCourseTimes
289
                ->where('day', $oldCourseTime->day)
290
                ->where('start', Carbon::parse($oldCourseTime->start)->toTimeString())
291
                ->where('end', Carbon::parse($oldCourseTime->end)->toTimeString());
292
293
            // remove the course time if no longer exists
294
            if ($newCourseTime->count() == 0) {
295
                $oldCourseTime->delete();
296
            }
297
        }
298
299
        foreach ($newCourseTimes as $courseTime) {
300
            // create missing course times
301
            if ($this->times()
302
                    ->where('day', $courseTime->day)
303
                    ->where('start', Carbon::parse($courseTime->start)->toTimeString())
304
                    ->where('end', Carbon::parse($courseTime->end)->toTimeString())
305
                    ->count() == 0) {
306
                $this->times()->create([
307
                    'day' => $courseTime->day,
308
                    'start' => Carbon::parse($courseTime->start)->toTimeString(),
309
                    'end' => Carbon::parse($courseTime->end)->toTimeString(),
310
                ]);
311
            }
312
        }
313
    }
314
315
    public function saveRemoteEvents($events)
316
    {
317
        $this->remoteEvents()->delete();
318
        foreach ($events as $event) {
319
            $this->remoteEvents()->create([
320
                'name' => $event->name ?? $this->name,
321
                'worked_hours' => $event->worked_hours,
322
            ]);
323
        }
324
    }
325
326
    /*
327
    |--------------------------------------------------------------------------
328
    | ACCESORS
329
    |--------------------------------------------------------------------------
330
    */
331
332
    /**
333
     * returns the course repeating schedule
334
     * todo improve this method.
335
     */
336
    public function getCourseTimesAttribute()
337
    {
338
        $parsedCourseTimes = [];
339
        // TODO localize these
340
        $daysInitials = [
341
            __('Sun'),
342
            __('Mon'),
343
            __('Tue'),
344
            __('Wed'),
345
            __('Thu'),
346
            __('Fri'),
347
            __('Sat'),
348
        ];
349
350
        $courseTimes = null;
351
        if ($this->times->count() > 0) {
352
            $courseTimes = $this->times;
353
        } elseif (($this->children->count() > 0) && ($this->children->first()->times->count() > 0)) {
354
            $courseTimes = $this->children->first()->times;
355
        }
356
357
        if ($courseTimes) {
358
            foreach ($courseTimes as $courseTime) {
359
                $initial = $daysInitials[$courseTime->day];
360
361
                if (! isset($parsedCourseTimes[$initial])) {
362
                    $parsedCourseTimes[$initial] = [];
363
                }
364
365
                $parsedCourseTimes[$initial][] = sprintf(
366
                    '%s - %s',
367
                    Carbon::parse($courseTime->start)->locale(App::getLocale())->isoFormat('LT'),
368
                    Carbon::parse($courseTime->end)->locale(App::getLocale())->isoFormat('LT')
369
                );
370
            }
371
        }
372
373
        $result = '';
374
        foreach ($parsedCourseTimes as $day => $times) {
375
            $result .= $day.' '.implode(' / ', $times).' | ';
376
        }
377
378
        return trim($result, ' | ');
379
    }
380
381
    public function getCourseRoomNameAttribute()
382
    {
383
        return strtoupper($this->room->name);
384
    }
385
386
    public function getCourseLevelNameAttribute(): string
387
    {
388
        if ($this->level->exists()) {
389
            return $this->level->name;
390
        }
391
392
        return '';
393
    }
394
395
    public function getCourseRhythmNameAttribute()
396
    {
397
        return strtoupper($this->rhythm->name);
398
    }
399
400
    public function getCoursePeriodNameAttribute()
401
    {
402
        return $this->period->name ?? '';
403
    }
404
405
    public function getCourseTeacherNameAttribute()
406
    {
407
        if ($this->teacher_id) {
408
            return $this->teacher?->name;
409
        } else {
410
            return '-';
411
        }
412
    }
413
414
    public function getShortnameAttribute()
415
    {
416
        return Str::slug($this->name);
417
    }
418
419
    public function getDescriptionAttribute()
420
    {
421
        return '['.$this->course_period_name.'] - '.$this->name;
422
    }
423
424
    public function getChildrenCountAttribute()
425
    {
426
        return self::where('parent_course_id', $this->id)->count();
427
    }
428
429
    public function getChildrenAttribute()
430
    {
431
        return self::where('parent_course_id', $this->id)->get();
432
    }
433
434
    public function getCourseEnrollmentsCountAttribute()
435
    {
436
        return $this->enrollments()->count();
437
    }
438
439
    public function getAcceptsNewStudentsAttribute(): bool
440
    {
441
        if (! $this->spots || $this->spots == 0) {
442
            return true;
443
        }
444
445
        return $this->spots - $this->enrollments()->whereIn('status_id', [1, 2])->count() > 0;
446
    }
447
448
    public function getTakesAttendanceAttribute(): bool
449
    {
450
        return $this->events_count > 0 && $this->exempt_attendance !== 1 && $this->course_enrollments_count > 0;
0 ignored issues
show
Bug introduced by
The property events_count does not exist on App\Models\Course. Did you mean events?
Loading history...
451
    }
452
453
    public function getParentAttribute()
454
    {
455
        if ($this->parent_course_id !== null) {
456
            return $this->parent_course_id;
457
        } else {
458
            return $this->id;
459
        }
460
    }
461
462
    public function eventsWithExpectedAttendance()
463
    {
464
        return $this->events()->where(function ($query) {
465
            $query->where('exempt_attendance', '!=', true);
466
            $query->where('exempt_attendance', '!=', 1);
467
            $query->orWhereNull('exempt_attendance');
468
        })->where('start', '<', Carbon::now(config('settings.courses_timezone'))->toDateTimeString());
469
    }
470
471
    public function getSortableIdAttribute()
472
    {
473
        if ($this->parent_course_id !== null) {
474
            return $this->parent_course_id;
475
        } else {
476
            return $this->id;
477
        }
478
    }
479
480
    public function getTotalVolumeAttribute()
481
    {
482
        return $this->volume + $this->remote_volume;
483
    }
484
485
    public function getPriceAttribute($value)
486
    {
487
        return $value / 100;
488
    }
489
490
    public function getPriceWithCurrencyAttribute()
491
    {
492
        if (config('app.currency_position') === 'before') {
493
            return config('app.currency_symbol').' '.$this->price;
494
        }
495
496
        return $this->price.' '.config('app.currency_symbol');
497
    }
498
499
    public function getPriceBAttribute($value)
500
    {
501
        return $value / 100;
502
    }
503
504
    public function getPriceCAttribute($value)
505
    {
506
        return $value / 100;
507
    }
508
509
    public function getFormattedStartDateAttribute()
510
    {
511
        return Carbon::parse($this->start_date, 'UTC')->locale(App::getLocale())->isoFormat('LL');
512
    }
513
514
    public function getFormattedEndDateAttribute()
515
    {
516
        return Carbon::parse($this->end_date, 'UTC')->locale(App::getLocale())->isoFormat('LL');
517
    }
518
519
    /*
520
    |--------------------------------------------------------------------------
521
    | MUTATORS
522
    |--------------------------------------------------------------------------
523
    */
524
525
    public function setPriceAttribute($value)
526
    {
527
        $this->attributes['price'] = $value * 100;
528
    }
529
530
    public function setPriceBAttribute($value)
531
    {
532
        $this->attributes['price_b'] = $value * 100;
533
    }
534
535
    public function setPriceCAttribute($value)
536
    {
537
        $this->attributes['price_c'] = $value * 100;
538
    }
539
}
540