Passed
Pull Request — master (#266)
by
unknown
05:16
created

Course::scopeRealcourses()   A

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\Skills\Skill;
8
use Backpack\CRUD\app\Models\Traits\CrudTrait;
9
use Carbon\Carbon;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Support\Facades\App;
12
use Illuminate\Support\Facades\Log;
13
use Illuminate\Support\Str;
14
use Spatie\Activitylog\Traits\LogsActivity;
15
use App\Models\Partner;
16
17
/**
18
 * App\Models\Course
19
 *
20
 * @property int $id
21
 * @property int $campus_id
22
 * @property int|null $rhythm_id
23
 * @property int|null $level_id
24
 * @property int $volume
25
 * @property string $name
26
 * @property string $price
27
 * @property string|null $hourly_price
28
 * @property \Illuminate\Support\Carbon $start_date
29
 * @property \Illuminate\Support\Carbon $end_date
30
 * @property int|null $room_id
31
 * @property int|null $teacher_id
32
 * @property int|null $parent_course_id
33
 * @property int|null $exempt_attendance
34
 * @property int $period_id
35
 * @property int|null $opened
36
 * @property int|null $spots
37
 * @property int|null $head_count
38
 * @property int|null $new_students
39
 * @property string|null $color
40
 * @property int|null $evaluation_type_id
41
 * @property \Illuminate\Support\Carbon|null $created_at
42
 * @property \Illuminate\Support\Carbon|null $updated_at
43
 * @property int|null $partner_id
44
 * @property string|null $remote_volume
45
 * @property int|null $sync_to_lms
46
 * @property int|null $lms_id
47
 * @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Activitylog\Models\Activity[] $activities
48
 * @property-read int|null $activities_count
49
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Attendance[] $attendance
50
 * @property-read int|null $attendance_count
51
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Book[] $books
52
 * @property-read int|null $books_count
53
 * @property-read \App\Models\Campus $campus
54
 * @property-read \Illuminate\Database\Eloquent\Collection|Course[] $children
55
 * @property-read int|null $children_count
56
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Enrollment[] $enrollments
57
 * @property-read int|null $enrollments_count
58
 * @property-read \App\Models\EvaluationType|null $evaluationType
59
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Event[] $events
60
 * @property-read int|null $events_count
61
 * @property-read bool $accepts_new_students
62
 * @property-read mixed $course_enrollments_count
63
 * @property-read mixed $course_level_name
64
 * @property-read mixed $course_period_name
65
 * @property-read mixed $course_rhythm_name
66
 * @property-read mixed $course_room_name
67
 * @property-read mixed $course_teacher_name
68
 * @property-read mixed $course_times
69
 * @property-read mixed $description
70
 * @property-read mixed $formatted_end_date
71
 * @property-read mixed $formatted_start_date
72
 * @property-read Course|null $parent
73
 * @property-read mixed $pending_attendance
74
 * @property-read mixed $price_with_currency
75
 * @property-read mixed $shortname
76
 * @property-read mixed $sortable_id
77
 * @property-read bool $takes_attendance
78
 * @property-read mixed $total_volume
79
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Grade[] $grades
80
 * @property-read int|null $grades_count
81
 * @property-read \App\Models\Level|null $level
82
 * @property-read Partner|null $partner
83
 * @property-read \App\Models\Period $period
84
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Enrollment[] $real_enrollments
85
 * @property-read int|null $real_enrollments_count
86
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\RemoteEvent[] $remoteEvents
87
 * @property-read int|null $remote_events_count
88
 * @property-read \App\Models\Rhythm|null $rhythm
89
 * @property-read \App\Models\Room|null $room
90
 * @property-read \App\Models\Teacher|null $teacher
91
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\CourseTime[] $times
92
 * @property-read int|null $times_count
93
 * @method static \Illuminate\Database\Eloquent\Builder|Course children()
94
 * @method static \Illuminate\Database\Eloquent\Builder|Course external()
95
 * @method static \Illuminate\Database\Eloquent\Builder|Course internal()
96
 * @method static \Illuminate\Database\Eloquent\Builder|Course newModelQuery()
97
 * @method static \Illuminate\Database\Eloquent\Builder|Course newQuery()
98
 * @method static \Illuminate\Database\Eloquent\Builder|Course parent()
99
 * @method static \Illuminate\Database\Eloquent\Builder|Course query()
100
 * @method static \Illuminate\Database\Eloquent\Builder|Course realcourses()
101
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereCampusId($value)
102
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereColor($value)
103
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereCreatedAt($value)
104
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereEndDate($value)
105
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereEvaluationTypeId($value)
106
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereExemptAttendance($value)
107
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereHeadCount($value)
108
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereHourlyPrice($value)
109
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereId($value)
110
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereLevelId($value)
111
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereLmsId($value)
112
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereName($value)
113
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereNewStudents($value)
114
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereOpened($value)
115
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereParentCourseId($value)
116
 * @method static \Illuminate\Database\Eloquent\Builder|Course wherePartnerId($value)
117
 * @method static \Illuminate\Database\Eloquent\Builder|Course wherePeriodId($value)
118
 * @method static \Illuminate\Database\Eloquent\Builder|Course wherePrice($value)
119
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereRemoteVolume($value)
120
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereRhythmId($value)
121
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereRoomId($value)
122
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereSpots($value)
123
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereStartDate($value)
124
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereSyncToLms($value)
125
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereTeacherId($value)
126
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereUpdatedAt($value)
127
 * @method static \Illuminate\Database\Eloquent\Builder|Course whereVolume($value)
128
 * @mixin \Eloquent
129
 */
130
class Course extends Model
131
{
132
    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...
133
    use LogsActivity;
134
135
    protected $dispatchesEvents = [
136
        'updated' => CourseUpdated::class,
137
        'created' => CourseCreated::class,
138
    ];
139
140
    /*
141
    |--------------------------------------------------------------------------
142
    | GLOBAL VARIABLES
143
    |--------------------------------------------------------------------------
144
    */
145
    // protected $primaryKey = 'id';
146
    public $timestamps = true;
147
    protected $guarded = ['id'];
148
    //protected $fillable = [];
149
    // protected $hidden = [];
150
    protected $dates = ['start_date', 'end_date'];
151
    protected $with = ['evaluationType'];
152
    protected $appends = [
153
        'course_times',
154
        'course_teacher_name',
155
        'course_period_name',
156
        'course_enrollments_count',
157
        'accepts_new_students',
158
        'takes_attendance',
159
        'sortable_id'
160
    ];
161
162
    protected static $logUnguarded = true;
163
164
    /*
165
    |--------------------------------------------------------------------------
166
    | SCOPES
167
    |--------------------------------------------------------------------------
168
    */
169
170
    /** filter only the courses that have no parent */
171
    public function scopeParent($query)
172
    {
173
        return $query->where('parent_course_id', null);
174
    }
175
176
    public function scopeChildren($query)
177
    {
178
        return $query->where('parent_course_id', '!=', null);
179
    }
180
181
    public function scopeInternal($query)
182
    {
183
        return $query->where('campus_id', 1);
184
    }
185
186
    public function scopeExternal($query)
187
    {
188
        return $query->where('campus_id', 2);
189
    }
190
191
    public function scopeRealcourses($query)
192
    {
193
        return $query->doesntHave('children');
194
    }
195
196
    /*
197
    |--------------------------------------------------------------------------
198
    | FUNCTIONS
199
    |--------------------------------------------------------------------------
200
    */
201
202
    /** returns all courses that are open for enrollments */
203
    public static function get_available_courses(Period $period)
204
    {
205
        return self::where('period_id', $period->id)
206
        ->where('campus_id', 1)
207
        ->with('times')
208
        ->with('teacher')
209
        ->with('room')
210
        ->with('rhythm')
211
        ->with('level')
212
        ->withCount('enrollments')
213
        ->get();
214
    }
215
216
    /*
217
    |--------------------------------------------------------------------------
218
    | RELATIONS
219
    |--------------------------------------------------------------------------
220
    */
221
222
    /** the scheduled day/times for the course, that repeat throughout the course date span */
223
    public function times()
224
    {
225
        return $this->hasMany(CourseTime::class, 'course_id');
226
    }
227
228
    /** course sessions (classes) with a specific start and end date/time */
229
    public function events()
230
    {
231
        return $this->hasMany(Event::class)->orderBy('start');
232
    }
233
234
    public function remoteEvents()
235
    {
236
        return $this->hasMany(RemoteEvent::class);
237
    }
238
239
    /** may be null if the teacher is not yet assigned */
240
    public function teacher()
241
    {
242
        return $this->belongsTo(Teacher::class)->withTrashed();
243
    }
244
245
    public function campus()
246
    {
247
        return $this->belongsTo(Campus::class);
248
    }
249
250
    /** may be null if the room is not yet assigned */
251
    public function room()
252
    {
253
        return $this->belongsTo(Room::class)->withTrashed();
254
    }
255
256
    /** the "category" of course */
257
    public function rhythm()
258
    {
259
        return $this->belongsTo(Rhythm::class)->withTrashed();
260
    }
261
262
    /** a course can only have one level. Parent courses would generally have no level defined */
263
    public function level()
264
    {
265
        return $this->belongsTo(Level::class)->withTrashed();
266
    }
267
268
    /** a course needs to belong to a period */
269
    public function period()
270
    {
271
        return $this->belongsTo(Period::class);
272
    }
273
274
    /** children courses = sub-courses, or course modules */
275
    public function children()
276
    {
277
        return $this->hasMany(self::class, 'parent_course_id');
278
    }
279
280
    public function parent()
281
    {
282
        return $this->belongsTo(self::class, 'parent_course_id');
283
    }
284
285
    /** evaluation methods associated to the course - grades, skill-based evaluation... */
286
    public function evaluationType()
287
    {
288
        return $this->belongsTo(EvaluationType::class);
289
    }
290
291
    /** a Grade model = an individual grade, belongs to a student */
292
    public function grades()
293
    {
294
        return $this->hasManyThrough(Grade::class, Enrollment::class);
295
    }
296
297
    /** the different grade types associated to the course, ie. criteria that will receive the grades */
298
    public function grade_types()
299
    {
300
        if ($this->evaluationType) {
301
            return $this->evaluationType->gradeTypes()->orderBy('order');
302
        }
303
304
        return GradeType::query();
305
    }
306
307
    /** in the case of skills-based evaluation, Skill models are attached to the course
308
     * This represents the "criteria" that will need to be evaluated to each student (enrollment) in the course.
309
     */
310
    public function skills()
311
    {
312
        return $this->evaluationType?->skills()->orderBy('order');
313
    }
314
315
    public function books()
316
    {
317
        return $this->belongsToMany(Book::class);
318
    }
319
320
    /**
321
     * return attendance records associated to the course
322
     * Since the attendance records are linked to the event, we use a hasManyThrough relation.
323
     */
324
    public function attendance()
325
    {
326
        return $this->hasManyThrough(Attendance::class, Event::class);
327
    }
328
329
    /**
330
     * Return events for which the attendance records do not match the course student count.
331
     *
332
     * todo - optimize this method (reduce the number of queries and avoid the foreach loop)
333
     * but filtering the collection increases the number of DB queries... (why ?)
334
     */
335
    public function getPendingAttendanceAttribute()
336
    {
337
        $events = Event::where(function ($query) {
338
            $query->where('course_id', $this->id);
339
            $query->where('exempt_attendance', '!=', true);
340
            $query->where('exempt_attendance', '!=', 1);
341
            $query->orWhereNull('exempt_attendance');
342
        })
343
        ->where('course_id', '!=', null)
344
        ->with('attendance')
345
        ->with('teacher')
346
        ->with('course.enrollments')
347
        ->where('start', '<', Carbon::now(config('settings.courses_timezone'))->toDateTimeString())
348
        ->get();
349
350
        $pending_events = [];
351
352
        foreach ($events as $event) {
353
            // if the attendance record count do not match the enrollment count, push the event to array
354
            $pending_attendance = $event->course->enrollments->count() - $event->attendance->count();
355
356
            if ($pending_attendance != 0) {
357
                $pending_events[$event->id]['event'] = $event->name ?? '';
358
                $pending_events[$event->id]['event_id'] = $event->id;
359
                $pending_events[$event->id]['course_id'] = $event->course_id;
360
                $pending_events[$event->id]['event_date'] = Carbon::parse($event->start)->toDateString();
361
                $pending_events[$event->id]['teacher'] = $event->teacher->name ?? '';
362
                $pending_events[$event->id]['pending'] = $pending_attendance ?? '';
363
            }
364
        }
365
366
        return $pending_events;
367
    }
368
369
    public function enrollments()
370
    {
371
        return $this
372
            ->hasMany(Enrollment::class, 'course_id', 'id')
373
            ->whereIn('status_id', [1, 2]) // pending or paid enrollments only
374
            ->with('student');
375
    }
376
377
    /** returns only pending or paid enrollments, without the child enrollments */
378
    public function real_enrollments()
379
    {
380
        return $this->hasMany(Enrollment::class, 'course_id', 'id')
381
        ->whereIn('status_id', ['1', '2']) // pending or paid
382
        ->where('parent_id', null);
383
    }
384
385
    public function partner()
386
    {
387
        return $this->belongsTo(Partner::class);
388
    }
389
390
391
    public function saveCourseTimes($newCourseTimes)
392
    {
393
        // before updating, retrieve existing course times
394
        $oldCourseTimes = $this->times;
395
396
        // check existing coursetimes
397
        foreach ($oldCourseTimes as $oldCourseTime) {
398
            $newCourseTime = $newCourseTimes
399
                ->where('day', $oldCourseTime->day)
400
                ->where('start', Carbon::parse($oldCourseTime->start)->toTimeString())
401
                ->where('end', Carbon::parse($oldCourseTime->end)->toTimeString());
402
403
            // remove the course time if no longer exists
404
            if ($newCourseTime->count() == 0) {
405
                $oldCourseTime->delete();
406
            }
407
        }
408
409
        foreach ($newCourseTimes as $courseTime) {
410
            // create missing course times
411
            if ($this->times()
412
                    ->where('day', $courseTime->day)
413
                    ->where('start', Carbon::parse($courseTime->start)->toTimeString())
414
                    ->where('end', Carbon::parse($courseTime->end)->toTimeString())
415
                    ->count() == 0) {
416
                $this->times()->create([
417
                    'day' => $courseTime->day,
418
                    'start' => Carbon::parse($courseTime->start)->toTimeString(),
419
                    'end' => Carbon::parse($courseTime->end)->toTimeString(),
420
                ]);
421
            }
422
        }
423
    }
424
425
    public function saveRemoteEvents($events)
426
    {
427
        $this->remoteEvents()->delete();
428
        foreach ($events as $event)
429
        {
430
            $this->remoteEvents()->create([
431
                'name' => $event->name ?? $this->name,
432
                'worked_hours' => $event->worked_hours,
433
            ]);
434
        }
435
    }
436
437
438
439
    /*
440
    |--------------------------------------------------------------------------
441
    | ACCESORS
442
    |--------------------------------------------------------------------------
443
    */
444
445
    /**
446
     * returns the course repeating schedule
447
     * todo improve this method.
448
     */
449
    public function getCourseTimesAttribute()
450
    {
451
        $parsedCourseTimes = [];
452
        // TODO localize these
453
        $daysInitials = [
454
            __('Sun'),
455
            __('Mon'),
456
            __('Tue'),
457
            __('Wed'),
458
            __('Thu'),
459
            __('Fri'),
460
            __('Sat'),
461
        ];
462
463
        $courseTimes = null;
464
        if ($this->times->count() > 0) {
465
            $courseTimes = $this->times;
466
        } elseif (($this->children->count() > 0) && ($this->children->first()->times->count() > 0)) {
467
            $courseTimes = $this->children->first()->times;
468
        }
469
470
        if ($courseTimes) {
471
            foreach ($courseTimes as $courseTime) {
472
                $initial = $daysInitials[$courseTime->day];
473
474
                if (! isset($parsedCourseTimes[$initial])) {
475
                    $parsedCourseTimes[$initial] = [];
476
                }
477
478
                $parsedCourseTimes[$initial][] = sprintf(
479
                    '%s - %s',
480
                    Carbon::parse($courseTime->start)->locale(App::getLocale())->isoFormat('LT'),
481
                    Carbon::parse($courseTime->end)->locale(App::getLocale())->isoFormat('LT')
482
                );
483
            }
484
        }
485
486
        $parsed = [];
487
        $times = '';
488
        $group = 1;
489
        $dayTimes = [];
490
491
        foreach ($parsedCourseTimes as $day => $times) {
492
            $dayTimes[] = [
493
                'day' => $day,
494
                'time' => implode(' / ', $times),
495
            ];
496
        }
497
498
        // grouping by time
499
        foreach ($dayTimes as $key => $value) {
500
            if ($value['time'] == $times || !$times) {
501
                $parsed[$group][] = $value;
502
            } else {
503
                $group++;
504
                $parsed[$group][] = $value;
505
            }
506
            $times = $value['time'];
507
        }
508
509
        // handle for case Mon - Friday 9:00am - 5:00pm but on the Wed 10:00am - 2:00pm, so will split up like so [Mon - Tue 9:00am - 5:00pm | Wed 10:00am -2:00pm | Thur - Friday 9:00am - 5:00pm]
510
        $result = collect($parsed)->map(function ($value) {
511
            $value = collect($value);
512
            $day = $value->first()['day'];
513
            $times = $value->first()['time'];
514
            if ($value->count() > 1) {
515
                $day .= ' - ' . $value->last()['day'];
516
            }
517
518
            return "$day $times";
519
        })->implode(' | ');
520
521
        return $result;
522
    }
523
524
    public function getCourseRoomNameAttribute()
525
    {
526
        return strtoupper($this->room->name);
527
    }
528
529
    public function getCourseLevelNameAttribute() : string
530
    {
531
        if ($this->level->exists()) {
532
            return $this->level->name;
533
        }
534
535
        return '';
536
    }
537
538
    public function getCourseRhythmNameAttribute()
539
    {
540
        return strtoupper($this->rhythm->name);
541
    }
542
543
    public function getCoursePeriodNameAttribute()
544
    {
545
        return $this->period->name ?? '';
546
    }
547
548
    public function getCourseTeacherNameAttribute()
549
    {
550
        if ($this->teacher_id) {
551
            return $this->teacher?->name;
552
        } else {
553
            return '-';
554
        }
555
    }
556
557
    public function getShortnameAttribute()
558
    {
559
        return Str::slug($this->name);
560
    }
561
562
    public function getDescriptionAttribute()
563
    {
564
        return '[' . $this->course_period_name . '] - ' . $this->name;
565
    }
566
567
    public function getChildrenCountAttribute()
568
    {
569
        return self::where('parent_course_id', $this->id)->count();
570
    }
571
572
    public function getChildrenAttribute()
573
    {
574
        return self::where('parent_course_id', $this->id)->get();
575
    }
576
577
    public function getCourseEnrollmentsCountAttribute()
578
    {
579
        return $this->enrollments()->count();
580
    }
581
582
583
    public function getAcceptsNewStudentsAttribute(): bool
584
    {
585
        if (! $this->spots || $this->spots == 0) {
586
            return true;
587
        }
588
589
        return $this->spots - $this->course_enrollments_count > 0;
590
    }
591
592
    public function getTakesAttendanceAttribute(): bool
593
    {
594
        return $this->events_count > 0 && $this->exempt_attendance !== 1 && $this->course_enrollments_count > 0;
595
    }
596
597
    public function getParentAttribute()
598
    {
599
        if ($this->parent_course_id !== null) {
600
            return $this->parent_course_id;
601
        } else {
602
            return $this->id;
603
        }
604
    }
605
606
    public function eventsWithExpectedAttendance()
607
    {
608
        return $this->events()->where(function ($query) {
609
            $query->where('exempt_attendance', '!=', true);
610
            $query->where('exempt_attendance', '!=', 1);
611
            $query->orWhereNull('exempt_attendance');
612
        })->where('start', '<', Carbon::now(config('settings.courses_timezone'))->toDateTimeString());
613
    }
614
615
    public function getSortableIdAttribute()
616
    {
617
        if ($this->parent_course_id !== null) {
618
            return $this->parent_course_id;
619
        } else {
620
            return $this->id;
621
        }
622
    }
623
624
    public function getTotalVolumeAttribute() {
625
        return $this->volume + $this->remote_volume;
626
    }
627
628
    public function getPriceAttribute($value)
629
    {
630
        return $value / 100;
631
    }
632
633
    public function getPriceWithCurrencyAttribute()
634
    {
635
        if (config('app.currency_position') === 'before')
636
        {
637
            return config('app.currency_symbol') . " ". $this->price;
638
        }
639
640
        return $this->price . " " . config('app.currency_symbol');
641
    }
642
643
644
    public function getFormattedStartDateAttribute()
645
    {
646
        return Carbon::parse($this->start_date, 'UTC')->locale(App::getLocale())->isoFormat('LL');
647
    }
648
649
    public function getFormattedEndDateAttribute()
650
    {
651
        return Carbon::parse($this->end_date, 'UTC')->locale(App::getLocale())->isoFormat('LL');
652
    }
653
654
    /*
655
    |--------------------------------------------------------------------------
656
    | MUTATORS
657
    |--------------------------------------------------------------------------
658
    */
659
660
    public function setPriceAttribute($value)
661
    {
662
        $this->attributes['price'] = $value * 100;
663
    }
664
}
665