Period   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 121
dl 0
loc 299
rs 9.2
c 0
b 0
f 0
wmc 40

27 Methods

Rating   Name   Duplication   Size   Complexity  
A get_default_period() 0 15 3
A getPendingEnrollmentsCountAttribute() 0 7 1
A getPaidEnrollmentsCountAttribute() 0 7 1
A get_enrollments_period() 0 15 3
A enrollments() 0 4 1
A year() 0 3 1
A external_courses() 0 3 1
A courses() 0 3 1
A internal_courses() 0 3 1
A boot() 0 6 1
A real_enrollments() 0 5 1
A getExternalTaughtHoursCountAttribute() 0 4 1
A getExternalCoursesCountAttribute() 0 3 1
A getInternalEnrollmentsCountAttribute() 0 3 1
A getCoursesWithPendingAttendanceAttribute() 0 30 5
A getExternalEnrollmentsCountAttribute() 0 3 1
A getPeriodTaughtHoursCountAttribute() 0 4 1
A getTakingsAttribute() 0 3 1
A getPartnershipsCountAttribute() 0 3 1
A newStudents() 0 10 1
A getExternalSoldHoursCountAttribute() 0 8 2
A getAcquisitionRateAttribute() 0 12 1
A studentCount() 0 38 3
A getExternalStudentsCountAttribute() 0 3 1
A getPreviousPeriodAttribute() 0 8 2
A getNextPeriodAttribute() 0 3 1
A getPeriodSoldHoursCountAttribute() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Period often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Period, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Models;
4
5
use Backpack\CRUD\app\Models\Traits\CrudTrait;
6
use Carbon\Carbon;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Support\Facades\DB;
10
use Spatie\Activitylog\Traits\LogsActivity;
11
12
/**
13
 * @mixin IdeHelperPeriod
14
 */
15
class Period extends Model
16
{
17
    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\Period: $fakeColumns, $identifiableAttribute, $Type
Loading history...
18
    use LogsActivity;
19
20
    public $timestamps = false;
21
22
    protected $fillable = ['name', 'year_id', 'start', 'end'];
23
24
    protected static bool $logUnguarded = true;
25
26
    protected static function boot()
27
    {
28
        parent::boot();
29
30
        static::addGlobalScope('order', function (Builder $builder) {
31
            $builder->orderBy('year_id')->orderBy('order')->orderBy('id');
32
        });
33
    }
34
35
    /**
36
     * Return the current period to be used as a default system-wide.
37
     * First look in Config DB table; otherwise select current or closest next period.
38
     */
39
    public static function get_default_period()
40
    {
41
        $configPeriod = Config::where('name', 'current_period');
42
43
        if ($configPeriod->exists()) {
44
            $currentPeriodId = $configPeriod->first()->value;
45
46
            if (self::where('id', $currentPeriodId)->count() > 0) {
47
                return self::find($currentPeriodId);
48
            } else {
49
                return self::where('end', '>=', date('Y-m-d'))->first();
50
            }
51
        }
52
53
        return self::first();
54
    }
55
56
    /**
57
     * Return the period to preselect for all enrollment-related methods.
58
     */
59
    public static function get_enrollments_period()
60
    {
61
        $selected_period = Config::where('name', 'default_enrollment_period')->first()->value;
62
63
        if (self::where('id', $selected_period)->count() > 0) {
64
            return self::find($selected_period);
65
        } else {
66
            // if the current period ends within 15 days, switch to the next one
67
            $default_period = self::get_default_period();
68
69
            // the number of days between the end and today is 2x less than the number of days between start and end
70
            if (Carbon::parse($default_period->end)->diffInDays() < 0.5 * Carbon::parse($default_period->start)->diffInDays($default_period->end)) {
71
                return self::where('id', '>', $default_period->id)->orderBy('id')->first();
72
            } else {
73
                return $default_period;
74
            }
75
        }
76
    }
77
78
    public function enrollments()
79
    {
80
        return $this->hasManyThrough(Enrollment::class, Course::class)
81
            ->with('course');
82
    }
83
84
    public function courses()
85
    {
86
        return $this->hasMany(Course::class);
87
    }
88
89
    public function internal_courses()
90
    {
91
        return $this->hasMany(Course::class)->internal();
92
    }
93
94
    public function external_courses()
95
    {
96
        return $this->hasMany(Course::class)->external();
97
    }
98
99
    public function year()
100
    {
101
        return $this->belongsTo(Year::class);
102
    }
103
104
    /** returns only pending or paid enrollments, without the child enrollments */
105
    public function real_enrollments()
106
    {
107
        return $this->hasManyThrough(Enrollment::class, Course::class)
108
        ->whereIn('status_id', ['1', '2']) // pending or paid
109
        ->where('parent_id', null);
110
    }
111
112
    /**
113
     * getPendingEnrollmentsCountAttribute
114
     * Do not count children enrollments.
115
     */
116
    public function getPendingEnrollmentsCountAttribute()
117
    {
118
        return $this
119
            ->enrollments
120
            ->where('status_id', 1) // pending
121
            ->where('parent_id', null)
122
            ->count();
123
    }
124
125
    /**
126
     * getPaidEnrollmentsCountAttribute
127
     * Do not count enrollments in children courses.
128
     */
129
    public function getPaidEnrollmentsCountAttribute()
130
    {
131
        return $this
132
            ->enrollments
133
            ->where('status_id', 2) // paid
134
            ->where('parent_id', null)
135
            ->count();
136
    }
137
138
    public function studentCount($gender = null)
139
    {
140
        if (in_array($gender, [1, 2])) {
141
            return DB::table('enrollments')
142
                ->join('courses', 'enrollments.course_id', 'courses.id')
143
                ->join('students', 'enrollments.student_id', 'students.id')
144
                ->where('courses.period_id', $this->id)
145
                ->where('enrollments.deleted_at', null)
146
                ->where('enrollments.parent_id', null)
147
                ->where('students.gender_id', $gender)
148
                ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
149
                ->distinct('student_id')
150
                ->count('enrollments.student_id');
151
        }
152
153
        if ($gender === 0) {
154
            return DB::table('enrollments')
155
                ->join('courses', 'enrollments.course_id', 'courses.id')
156
                ->join('students', 'enrollments.student_id', 'students.id')
157
                ->where('courses.period_id', $this->id)
158
                ->where('enrollments.deleted_at', null)
159
                ->where('enrollments.parent_id', null)
160
                ->where(function ($query) {
161
                    return $query->where('students.gender_id', 0)->orWhereNull('students.gender_id');
162
                })
163
                ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
164
                ->distinct('student_id')
165
                ->count('enrollments.student_id');
166
        }
167
168
        return DB::table('enrollments')
169
            ->join('courses', 'enrollments.course_id', 'courses.id')
170
            ->where('courses.period_id', $this->id)
171
            ->where('enrollments.deleted_at', null)
172
            ->where('enrollments.parent_id', null)
173
            ->whereIn('enrollments.status_id', ['1', '2']) // filter out cancelled enrollments, todo make this configurable.
174
            ->distinct('student_id')
175
            ->count('enrollments.student_id');
176
    }
177
178
    public function getInternalEnrollmentsCountAttribute()
179
    {
180
        return $this->paid_enrollments_count + $this->pending_enrollments_count;
181
    }
182
183
    public function getExternalEnrollmentsCountAttribute()
184
    {
185
        return $this->external_courses->sum('head_count');
186
    }
187
188
    public function getExternalStudentsCountAttribute()
189
    {
190
        return $this->external_courses->sum('new_students');
191
    }
192
193
    public function getExternalCoursesCountAttribute()
194
    {
195
        return $this->external_courses->count();
196
    }
197
198
    public function getPartnershipsCountAttribute()
199
    {
200
        return $this->courses()->pluck('partner_id')->unique()->count();
201
    }
202
203
    public function getPreviousPeriodAttribute()
204
    {
205
        $period = self::where('id', '<', $this->id)->orderBy('id', 'desc')->first();
206
207
        if (! $period == null) {
208
            return $period;
209
        } else {
210
            return self::first();
211
        }
212
    }
213
214
    public function getNextPeriodAttribute()
215
    {
216
        return self::where('id', '>', $this->id)->orderBy('id')->first();
217
    }
218
219
    /** Compute the acquisition rate = the part of students from period P-1 who have been kept in period P */
220
    public function getAcquisitionRateAttribute()
221
    {
222
        // get students enrolled in period P-1
223
        $previous_period_student_ids = $this->previous_period->real_enrollments->pluck('student_id');
224
225
        // and students enrolled in period P
226
        $current_students_ids = $this->real_enrollments->pluck('student_id');
227
228
        // students both in period p-1 and period p
229
        $acquired_students = $previous_period_student_ids->intersect($current_students_ids);
230
231
        return number_format((100 * $acquired_students->count()) / max($previous_period_student_ids->count(), 1), 1).'%';
232
    }
233
234
    public function newStudents()
235
    {
236
        // get students IDs enrolled in all previous periods
237
        $previous_period_student_ids = DB::table('enrollments')->join('courses', 'enrollments.course_id', 'courses.id')->where('period_id', '<', $this->id)->pluck('enrollments.student_id');
238
239
        // and students enrolled in period P
240
        $current_students_ids = $this->real_enrollments->unique('student_id');
241
242
        // students in period P who have never been enrolled in previous periods
243
        return $current_students_ids->whereNotIn('student_id', $previous_period_student_ids);
244
    }
245
246
    public function getPeriodTaughtHoursCountAttribute()
247
    {
248
        // return the sum of all courses' volume for period
249
        return $this->internal_courses->where('parent_course_id', null)->sum('total_volume');
250
    }
251
252
    public function getPeriodSoldHoursCountAttribute()
253
    {
254
        $total = 0;
255
        foreach ($this->courses()->internal()->withCount('real_enrollments')->get() as $course) {
256
            $total += $course->total_volume * $course->real_enrollments_count;
257
        }
258
259
        return $total;
260
    }
261
262
    public function getTakingsAttribute()
263
    {
264
        return $this->real_enrollments->sum('total_paid_price');
265
    }
266
267
    public function getExternalTaughtHoursCountAttribute()
268
    {
269
        // return the sum of all courses' volume for period
270
        return $this->external_courses->where('parent_course_id', null)->sum('total_volume');
271
    }
272
273
    public function getExternalSoldHoursCountAttribute()
274
    {
275
        $total = 0;
276
        foreach ($this->external_courses as $course) {
277
            $total += $course->total_volume * $course->head_count;
278
        }
279
280
        return $total;
281
    }
282
283
    /** TODO this method can be furthered optimized and refactored */
284
    public function getCoursesWithPendingAttendanceAttribute()
285
    {
286
        // get all courses for period and preload relations
287
        $courses = $this->courses()->where(function ($query) {
288
            $query->where('exempt_attendance', '!=', true);
289
            $query->where('exempt_attendance', '!=', 1);
290
            $query->orWhereNull('exempt_attendance');
291
        })->whereHas('events')->with('attendance')->whereNotNull('exempt_attendance')->get();
292
        $coursesWithMissingAttendanceCount = 0;
293
294
        // loop through all courses and get the number of events with incomplete attendance
295
        foreach ($courses as $course) {
296
            foreach ($course->eventsWithExpectedAttendance as $event) {
297
                foreach ($course->enrollments as $enrollment) {
298
                    // if a student has no attendance record for the class (event)
299
                    $hasNotAttended = $course->attendance->where('student_id', $enrollment->student_id)
300
                     ->where('event_id', $event->id)
301
                     ->isEmpty();
302
303
                    // count one and break loop
304
                    if ($hasNotAttended) {
305
                        $coursesWithMissingAttendanceCount++;
306
                        break 2;
307
                    }
308
                }
309
            }
310
        }
311
312
        // sort by number of events with missing attendance
313
        return $coursesWithMissingAttendanceCount;
314
    }
315
}
316