Passed
Push — feature/job-status-transitions ( a07846...768c1a )
by Tristan
04:18
created

JobPoster   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 55
eloc 237
c 2
b 0
f 1
dl 0
loc 524
rs 6

33 Methods

Rating   Name   Duplication   Size   Complexity  
A setPublishedAttribute() 0 11 3
A getClassificationMessageAttribute() 0 6 3
A getClassificationCodeAttribute() 0 6 2
A telework_allowed_frequency() 0 3 1
A province() 0 3 1
A overtime_requirement() 0 3 1
A job_applications() 0 3 1
A isPublic() 0 8 4
A language_requirement() 0 3 1
A submitted_applications_count() 0 3 1
A timeRemaining() 0 26 4
A isEditable() 0 4 1
A job_term() 0 3 1
A department() 0 3 1
A hr_advisors() 0 5 1
A isClosed() 0 8 6
A applyBy() 0 14 2
A classification() 0 3 1
A manager() 0 3 1
A criteria() 0 3 1
A isOpen() 0 7 5
A submitted_applications() 0 4 1
A job_poster_status_histories() 0 3 1
A job_poster_questions() 0 3 1
A job_poster_status() 0 3 1
A security_clearance() 0 3 1
A displayStatus() 0 3 2
A comments() 0 3 1
A isVisibleToHr() 0 3 1
A flexible_hours_frequency() 0 3 1
A resolveRouteBinding() 0 3 1
A job_poster_key_tasks() 0 3 1
A travel_requirement() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like JobPoster 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 JobPoster, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Created by Reliese Model.
5
 * Date: Thu, 12 Jul 2018 22:39:27 +0000.
6
 */
7
8
namespace App\Models;
9
10
use Spatie\Translatable\HasTranslations;
11
use Backpack\CRUD\app\Models\Traits\CrudTrait;
12
use Jenssegers\Date\Date;
13
use Illuminate\Notifications\Notifiable;
14
use Illuminate\Support\Facades\App;
15
use Illuminate\Support\Facades\Lang;
16
17
use App\Events\JobSaved;
18
19
/**
20
 * Class JobPoster
21
 *
22
 * @property int $id
23
 * @property int $job_term_id
24
 * @property string $chosen_lang
25
 * @property int $term_qty
26
 * @property \Jenssegers\Date\Date $open_date_time
27
 * @property \Jenssegers\Date\Date $close_date_time
28
 * @property \Jenssegers\Date\Date $start_date_time
29
 * @property \Jenssegers\Date\Date $review_requested_at
30
 * @property \Jenssegers\Date\Date $published_at
31
 * @property int $department_id
32
 * @property int $province_id
33
 * @property int $salary_min
34
 * @property int $salary_max
35
 * @property int $noc
36
 * @property int $classification_id
37
 * @property int $classification_level
38
 * @property int $security_clearance_id
39
 * @property int $language_requirement_id
40
 * @property boolean $remote_work_allowed
41
 * @property int $manager_id
42
 * @property boolean $published
43
 * @property boolean $internal_only
44
 * @property int $team_size
45
 * @property array $work_env_features This should be an array of boolean flags for features, ie json of shape {[feature: string]: boolean}
46
 * @property int $fast_vs_steady
47
 * @property int $horizontal_vs_vertical
48
 * @property int $experimental_vs_ongoing
49
 * @property int $citizen_facing_vs_back_office
50
 * @property int $collaborative_vs_independent
51
 * @property int $telework_allowed_frequency_id
52
 * @property int $flexible_hours_frequency_id
53
 * @property int $travel_requirement_id
54
 * @property int $overtime_requirement_id
55
 * @property int $process_number
56
 * @property int $priority_clearance_number
57
 * @property int $job_poster_status_id
58
 * @property \Jenssegers\Date\Date $loo_issuance_date
59
 * @property \Jenssegers\Date\Date $created_at
60
 * @property \Jenssegers\Date\Date $updated_at
61
 *
62
 * @property int $submitted_applications_count
63
 *
64
 * @property \App\Models\Lookup\Department $department
65
 * @property \App\Models\Lookup\JobTerm $job_term
66
 * @property \App\Models\Lookup\LanguageRequirement $language_requirement
67
 * @property \App\Models\Manager $manager
68
 * @property \App\Models\Lookup\Province $province
69
 * @property \App\Models\Lookup\SecurityClearance $security_clearance
70
 * @property \Illuminate\Database\Eloquent\Collection $criteria
71
 * @property \Illuminate\Database\Eloquent\Collection $job_applications
72
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_key_tasks
73
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_questions
74
 * @property \Illuminate\Database\Eloquent\Collection $job_poster_translations
75
 * @property \Illuminate\Database\Eloquent\Collection $submitted_applications
76
 * @property \Illuminate\Database\Eloquent\Collection $hr_advisors
77
 * @property \App\Models\Lookup\Frequency $telework_allowed_frequency
78
 * @property \App\Models\Lookup\Frequency $flexible_hours_frequency
79
 * @property \App\Models\Lookup\JobPosterStatus $job_poster_status
80
 *
81
 * Localized Properties:
82
 * @property string $city
83
 * @property string $title
84
 * @property string $dept_impact
85
 * @property string $team_impact
86
 * @property string $hire_impact
87
 * @property string $division
88
 * @property string $education
89
 * @property string $work_env_description
90
 * @property string $culture_summary
91
 * @property string $culture_special
92
 *
93
 * Methods
94
 * @method boolean isOpen()
95
 * @method boolean isClosed()
96
 * @method boolean isVisibleToHr()
97
 * @method boolean isPublic()
98
 * @method boolean isEditable()
99
 * @method string timeRemaining()
100
 * @method mixed[] toApiArray()
101
 *
102
 * Computed Properties
103
 * @property string|null $classification_code
104
 * @property string|null $classification_message
105
 */
106
class JobPoster extends BaseModel
107
{
108
    use CrudTrait;
109
    use HasTranslations;
110
    use Notifiable;
111
112
    const DATE_FORMAT = [
113
        'en' => 'M jS, Y',
114
        'fr' => 'd M Y',
115
    ];
116
    const TIME_FORMAT = [
117
        'en' => 'h:i A T',
118
        'fr' => 'H \h i T',
119
    ];
120
    const TIMEZONE = 'America/Toronto';
121
122
    /**
123
     * @var string[] $translatable
124
     */
125
    public $translatable = [
126
        'city',
127
        'title',
128
        'dept_impact',
129
        'team_impact',
130
        'hire_impact',
131
        'division',
132
        'education',
133
        'work_env_description',
134
        'culture_summary',
135
        'culture_special',
136
    ];
137
138
    /**
139
     * @var string[] $casts
140
     */
141
    protected $casts = [
142
        'job_term_id' => 'int',
143
        'department_id' => 'int',
144
        'province_id' => 'int',
145
        'salary_min' => 'int',
146
        'salary_max' => 'int',
147
        'noc' => 'int',
148
        'classification_id' => 'int',
149
        'classification_level' => 'int',
150
        'security_clearance_id' => 'int',
151
        'language_requirement_id' => 'int',
152
        'remote_work_allowed' => 'boolean',
153
        'manager_id' => 'int',
154
        'published' => 'boolean',
155
        'internal_only' => 'boolean',
156
        'team_size' => 'int',
157
        'work_env_features' => 'array',
158
        'fast_vs_steady' => 'int',
159
        'horizontal_vs_vertical' => 'int',
160
        'experimental_vs_ongoing' => 'int',
161
        'citizen_facing_vs_back_office' => 'int',
162
        'collaborative_vs_independent' => 'int',
163
        'telework_allowed_frequency_id' => 'int',
164
        'flexible_hours_frequency_id' => 'int',
165
        'travel_requirement_id' => 'int',
166
        'overtime_requirement_id' => 'int',
167
    ];
168
169
    /**
170
     * @var string[] $dates
171
     */
172
    protected $dates = [
173
        'open_date_time',
174
        'close_date_time',
175
        'start_date_time',
176
        'review_requested_at',
177
        'published_at',
178
        'loo_issuance_date',
179
    ];
180
181
    /**
182
     * @var string[] $fillable
183
     */
184
    protected $fillable = [
185
        'job_term_id',
186
        'chosen_lang',
187
        'term_qty',
188
        'open_date_time',
189
        'close_date_time',
190
        'start_date_time',
191
        'department_id',
192
        'province_id',
193
        'salary_min',
194
        'salary_max',
195
        'noc',
196
        'security_clearance_id',
197
        'language_requirement_id',
198
        'remote_work_allowed',
199
        'published',
200
        'internal_only',
201
        'team_size',
202
        'work_env_features',
203
        'fast_vs_steady',
204
        'horizontal_vs_vertical',
205
        'experimental_vs_ongoing',
206
        'citizen_facing_vs_back_office',
207
        'collaborative_vs_independent',
208
        'telework_allowed_frequency_id',
209
        'flexible_hours_frequency_id',
210
        'travel_requirement_id',
211
        'overtime_requirement_id',
212
        'process_number',
213
        'priority_clearance_number',
214
        'loo_issuance_date',
215
        'classification_id',
216
        'classification_level',
217
        'city',
218
        'title',
219
        'dept_impact',
220
        'team_impact',
221
        'hire_impact',
222
        'division',
223
        'education',
224
        'work_env_description',
225
        'culture_summary',
226
        'culture_special',
227
    ];
228
229
    /**
230
     * The attributes that should be visible in arrays.
231
     * In this case, it blocks loaded relations from appearing.
232
     *
233
     * @var array
234
     */
235
    protected $visible = [
236
        'id',
237
        'manager_id',
238
        'chosen_lang',
239
        'term_qty',
240
        'open_date_time',
241
        'close_date_time',
242
        'start_date_time',
243
        'department_id',
244
        'province_id',
245
        'salary_min',
246
        'salary_max',
247
        'noc',
248
        'security_clearance_id',
249
        'language_requirement_id',
250
        'remote_work_allowed',
251
        'published_at',
252
        'published',
253
        'review_requested_at',
254
        'team_size',
255
        'work_env_features',
256
        'fast_vs_steady',
257
        'horizontal_vs_vertical',
258
        'experimental_vs_ongoing',
259
        'citizen_facing_vs_back_office',
260
        'collaborative_vs_independent',
261
        'telework_allowed_frequency_id',
262
        'flexible_hours_frequency_id',
263
        'travel_requirement_id',
264
        'overtime_requirement_id',
265
        'process_number',
266
        'priority_clearance_number',
267
        'loo_issuance_date',
268
        'classification_id',
269
        'classification_level',
270
        'city',
271
        'title',
272
        'dept_impact',
273
        'team_impact',
274
        'hire_impact',
275
        'division',
276
        'education',
277
        'work_env_description',
278
        'culture_summary',
279
        'culture_special',
280
        'job_poster_status_id',
281
        'created_at',
282
    ];
283
284
    /**
285
     * The accessors to append to the model's array form.
286
     *
287
     * @var string[] $appends
288
     */
289
    protected $appends = [
290
        'classification_code',
291
        'classification_message',
292
    ];
293
294
    /**
295
     * Eager loaded relationships by default.
296
     *
297
     * @var string[] $with
298
     */
299
    protected $with = [
300
        'criteria',
301
        'manager'
302
    ];
303
304
    /**
305
     * @var mixed[] $dispatchesEvents
306
     */
307
    protected $dispatchesEvents = [
308
        'saved' => JobSaved::class,
309
    ];
310
311
    // @codeCoverageIgnoreStart
312
    public function department() // phpcs:ignore
313
    {
314
        return $this->belongsTo(\App\Models\Lookup\Department::class);
315
    }
316
317
    public function job_term() // phpcs:ignore
318
    {
319
        return $this->belongsTo(\App\Models\Lookup\JobTerm::class);
320
    }
321
322
    public function language_requirement() // phpcs:ignore
323
    {
324
        return $this->belongsTo(\App\Models\Lookup\LanguageRequirement::class);
325
    }
326
327
    public function manager() // phpcs:ignore
328
    {
329
        return $this->belongsTo(\App\Models\Manager::class);
330
    }
331
332
    public function province() // phpcs:ignore
333
    {
334
        return $this->belongsTo(\App\Models\Lookup\Province::class);
335
    }
336
337
    public function security_clearance() // phpcs:ignore
338
    {
339
        return $this->belongsTo(\App\Models\Lookup\SecurityClearance::class);
340
    }
341
342
    public function criteria() // phpcs:ignore
343
    {
344
        return $this->hasMany(\App\Models\Criteria::class);
345
    }
346
347
    public function hr_advisors() // phpcs:ignore
348
    {
349
        return $this->belongsToMany(
350
            \App\Models\HrAdvisor::class,
351
            'claimed_jobs'
352
        );
353
    }
354
355
    public function job_applications() // phpcs:ignore
356
    {
357
        return $this->hasMany(\App\Models\JobApplication::class);
358
    }
359
360
    public function job_poster_key_tasks() // phpcs:ignore
361
    {
362
        return $this->hasMany(\App\Models\JobPosterKeyTask::class);
363
    }
364
365
    public function job_poster_questions() // phpcs:ignore
366
    {
367
        return $this->hasMany(\App\Models\JobPosterQuestion::class);
368
    }
369
370
    public function telework_allowed_frequency() // phpcs:ignore
371
    {
372
        return $this->belongsTo(\App\Models\Lookup\Frequency::class);
373
    }
374
375
    public function flexible_hours_frequency() // phpcs:ignore
376
    {
377
        return $this->belongsTo(\App\Models\Lookup\Frequency::class);
378
    }
379
380
    public function travel_requirement() // phpcs:ignore
381
    {
382
        return $this->belongsTo(\App\Models\Lookup\TravelRequirement::class);
383
    }
384
385
    public function overtime_requirement() // phpcs:ignore
386
    {
387
        return $this->belongsTo(\App\Models\Lookup\OvertimeRequirement::class);
388
    }
389
390
    public function classification() // phpcs:ignore
391
    {
392
        return $this->belongsTo(\App\Models\Classification::class);
393
    }
394
395
    public function comments() // phpcs:ignore
396
    {
397
        return $this->hasMany(\App\Models\Comment::class);
398
    }
399
400
    public function job_poster_status() // phpcs:ignore
401
    {
402
        return $this->belongsTo(\App\Models\Lookup\JobPosterStatus::class);
403
    }
404
405
    public function job_poster_status_histories() // phpcs:ignore
406
    {
407
        return $this->hasMany(\App\Models\JobPosterStatusHistory::class);
408
    }
409
410
    // @codeCoverageIgnoreEnd
411
    /* Artificial Relations */
412
413
    /**
414
     * Get all of the Job Applications submitted to this
415
     * Job Poster.
416
     *
417
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
418
     */
419
    public function submitted_applications() // phpcs:ignore
420
    {
421
        return $this->hasMany(\App\Models\JobApplication::class)->whereDoesntHave('application_status', function ($query): void {
422
            $query->where('name', 'draft');
423
        });
424
    }
425
426
    /* Overrides */
427
428
    /**
429
     * Retrieve the model for a bound value.
430
     * Seems to be a useful workaround for providing submitted_applications_count
431
     * to any bound routes that receive a jobPoster instance without using the
432
     * withCount property on the model itself.
433
     * See https://github.com/laravel/framework/issues/23957 for more info.
434
     *
435
     * @param mixed $value Value used to retrieve the model instance.
436
     *
437
     * @return \Illuminate\Database\Eloquent\Model|null
438
     */
439
    public function resolveRouteBinding($value) // phpcs:ignore
440
    {
441
        return $this->withCount('submitted_applications')->where('id', $value)->first() ?? abort(404);
0 ignored issues
show
Bug introduced by
The function abort was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

441
        return $this->withCount('submitted_applications')->where('id', $value)->first() ?? /** @scrutinizer ignore-call */ abort(404);
Loading history...
442
    }
443
444
    /**
445
     * Intercept setting the "published" attribute, and set the
446
     * "published_at" timestamp if true.
447
     *
448
     * @param mixed $value Incoming value for the 'published' attribute.
449
     *
450
     * @return void
451
     */
452
    public function setPublishedAttribute($value): void
453
    {
454
        if ($value) {
455
            $this->attributes['published_at'] = new Date();
0 ignored issues
show
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
456
        } else {
457
            $this->attributes['published_at'] = null;
458
        }
459
        if ($value === null) {
460
            $value = false;
461
        }
462
        $this->attributes['published'] = $value;
463
    }
464
465
    /* Methods */
466
467
    public function submitted_applications_count() //phpcs:ignore
468
    {
469
        return $this->submitted_applications()->count();
470
    }
471
472
    /**
473
     * Formatted and localized date and time the Job Poster closes.
474
     *
475
     * @return string[]
476
     */
477
    public function applyBy(): array
478
    {
479
        $localCloseDate = new Date($this->close_date_time); // This initializes the date object in UTC time.
480
        $localCloseDate->setTimezone(new \DateTimeZone(self::TIMEZONE)); // Then set the time zone for display.
481
        $displayDate = [
482
            'date' => $localCloseDate->format(self::DATE_FORMAT[App::getLocale()]),
483
            'time' => $localCloseDate->format(self::TIME_FORMAT[App::getLocale()])
484
        ];
485
486
        if (App::isLocale('fr')) {
487
            $displayDate['time'] = str_replace(['EST', 'EDT'], ['HNE', 'HAE'], $displayDate['time']);
488
        }
489
490
        return $displayDate;
491
    }
492
493
    /**
494
     * Return whether the Job is Open or Closed.
495
     * Used by the Admin Portal JobPosterCrudController.
496
     *
497
     * @return string
498
     */
499
    public function displayStatus(): string
500
    {
501
        return $this->isOpen() ? 'Open' : 'Closed';
502
    }
503
504
    /**
505
     * Check if a Job Poster is open for applications.
506
     *
507
     * @return boolean
508
     */
509
    public function isOpen(): bool
510
    {
511
        return $this->job_poster_status->name === 'published'
512
            && $this->open_date_time !== null
513
            && $this->close_date_time !== null
514
            && $this->open_date_time->isPast()
515
            && $this->close_date_time->isFuture();
516
    }
517
518
    /**
519
     * Check if a Job Poster is closed for applications.
520
     *
521
     * @return boolean
522
     */
523
    public function isClosed(): bool
524
    {
525
        return ($this->job_poster_status->name === 'published'
526
            || $this->job_poster_status->name === 'completed')
527
            && $this->open_date_time !== null
528
            && $this->close_date_time !== null
529
            && $this->open_date_time->isPast()
530
            && $this->close_date_time->isPast();
531
    }
532
533
    /**
534
     * Return true if this job should be visible to hr advisors.
535
     * It should become visible after Manager has requested a review.
536
     *
537
     * @return boolean
538
     */
539
    public function isVisibleToHr()
540
    {
541
        return $this->job_poster_status->name !== 'draft';
542
    }
543
544
    /**
545
     * Check if a Job Poster is open to public view.
546
     *
547
     * @return boolean
548
     */
549
    public function isPublic(): bool
550
    {
551
        return (
552
            $this->job_poster_status->name == 'published'
553
            || $this->job_poster_status_name == 'closed'
554
        )
555
        && $this->open_date_time !== null
556
        && $this->open_date_time->isPast();
557
    }
558
559
    /**
560
     * Check if a Job Poster is theoretically editable.
561
     *
562
     * @return boolean
563
     */
564
    public function isEditable(): bool
565
    {
566
        // Admins, at least, should be able to edit the job up until the public can see it.
567
        return !$this->isPublic();
568
    }
569
570
    /**
571
     * Calculate the remaining time a Job Poster is open.
572
     *
573
     * @return string
574
     */
575
    public function timeRemaining(): string
576
    {
577
        $interval = $this->close_date_time->diff(Date::now());
578
579
        $d = $interval->d;
580
        $h = $interval->h;
581
        $m = $interval->i;
582
        $s = $interval->s;
583
584
        if ($d > 0) {
585
            $unit = 'day';
586
            $count = $d;
587
        } elseif ($h > 0) {
588
            $unit = 'hour';
589
            $count = $h;
590
        } elseif ($m > 0) {
591
            $unit = 'minute';
592
            $count = $m;
593
        } else {
594
            $unit = 'second';
595
            $count = $s;
596
        }
597
598
        $key = "common/time.$unit";
599
600
        return Lang::choice($key, $count);
601
    }
602
603
604
    /**
605
     * The database model stores a foreign id to the classification table,
606
     * but to simplify the API, this model simply returns the key as classification_code.
607
     *
608
     * @return string|null
609
     */
610
    public function getClassificationCodeAttribute()
611
    {
612
        if ($this->classification_id !== null) {
613
            return $this->classification->key;
614
        }
615
        return null;
616
    }
617
618
    /**
619
     *
620
     * Get the full government classification message.
621
     *
622
     * @return string|null
623
     */
624
    public function getClassificationMessageAttribute()
625
    {
626
        if ($this->classification_id !== null && $this->classification_level !== null) {
627
            return $this->classification->key . '-0' . $this->classification_level;
628
        }
629
        return null;
630
    }
631
}
632