Passed
Push — task/user-api-endpoint ( 6e4649...f0b85b )
by Chris
04:32
created

JobPoster   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 247
dl 0
loc 539
rs 4.5599
c 0
b 0
f 0

31 Methods

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

423
        return $this->withCount('submitted_applications')->where('id', $value)->first() ?? /** @scrutinizer ignore-call */ abort(404);
Loading history...
424
    }
425
426
    /**
427
     * Intercept setting the "published" attribute, and set the
428
     * "published_at" timestamp if true.
429
     *
430
     * @param mixed $value Incoming value for the 'published' attribute.
431
     *
432
     * @return void
433
     */
434
    public function setPublishedAttribute($value): void
435
    {
436
        if ($value) {
437
            $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...
438
        } else {
439
            $this->attributes['published_at'] = null;
440
        }
441
        if ($value === null) {
442
            $value = false;
443
        }
444
        $this->attributes['published'] = $value;
445
    }
446
447
    /* Methods */
448
449
    public function submitted_applications_count() //phpcs:ignore
450
    {
451
        return $this->submitted_applications()->count();
452
    }
453
454
    /**
455
     * Formatted and localized date and time the Job Poster closes.
456
     *
457
     * @return string[]
458
     */
459
    public function applyBy(): array
460
    {
461
        $localCloseDate = new Date($this->close_date_time); // This initializes the date object in UTC time.
462
        $localCloseDate->setTimezone(new \DateTimeZone(self::TIMEZONE)); // Then set the time zone for display.
463
        $displayDate = [
464
            'date' => $localCloseDate->format(self::DATE_FORMAT[App::getLocale()]),
465
            'time' => $localCloseDate->format(self::TIME_FORMAT[App::getLocale()])
466
        ];
467
468
        if (App::isLocale('fr')) {
469
            $displayDate['time'] = str_replace(['EST', 'EDT'], ['HNE', 'HAE'], $displayDate['time']);
470
        }
471
472
        return $displayDate;
473
    }
474
475
    /**
476
     * Return whether the Job is Open or Closed.
477
     * Used by the Admin Portal JobPosterCrudController.
478
     *
479
     * @return string
480
     */
481
    public function displayStatus(): string
482
    {
483
        return $this->isOpen() ? 'Open' : 'Closed';
484
    }
485
486
    /**
487
     * Check if a Job Poster is open for applications.
488
     *
489
     * @return boolean
490
     */
491
    public function isOpen(): bool
492
    {
493
        return $this->published
494
            && $this->open_date_time !== null
495
            && $this->close_date_time !== null
496
            && $this->open_date_time->isPast()
497
            && $this->close_date_time->isFuture();
498
    }
499
500
    /**
501
     * Check if a Job Poster is closed for applications.
502
     *
503
     * @return boolean
504
     */
505
    public function isClosed(): bool
506
    {
507
        return $this->published
508
            && $this->open_date_time !== null
509
            && $this->close_date_time !== null
510
            && $this->open_date_time->isPast()
511
            && $this->close_date_time->isPast();
512
    }
513
514
    /**
515
     * Calculate the remaining time a Job Poster is open.
516
     *
517
     * @return string
518
     */
519
    public function timeRemaining(): string
520
    {
521
        $interval = $this->close_date_time->diff(Date::now());
522
523
        $d = $interval->d;
524
        $h = $interval->h;
525
        $m = $interval->i;
526
        $s = $interval->s;
527
528
        if ($d > 0) {
529
            $unit = 'day';
530
            $count = $d;
531
        } elseif ($h > 0) {
532
            $unit = 'hour';
533
            $count = $h;
534
        } elseif ($m > 0) {
535
            $unit = 'minute';
536
            $count = $m;
537
        } else {
538
            $unit = 'second';
539
            $count = $s;
540
        }
541
542
        $key = "common/time.$unit";
543
544
        return Lang::choice($key, $count);
545
    }
546
547
    /**
548
     * Return the current status for the Job Poster.
549
     * Possible values are "draft", "submitted", "published" and "closed".
550
     *
551
     * @return string
552
     */
553
    public function status(): string
554
    {
555
        $status = 'draft';
556
        if ($this->isOpen()) {
557
            $status = 'published';
558
        } elseif ($this->isClosed()) {
559
            $status = 'closed';
560
        } elseif ($this->review_requested_at !== null) {
561
            $status = 'submitted';
562
        } else {
563
            $status = 'draft';
564
        }
565
566
        return $status;
567
    }
568
569
    /**
570
     * FIXME:
571
     * Return a calculated job status id.
572
     * For now these ids represent the following:
573
     *    1 = Draft
574
     *    2 = Review requested
575
     *    3 = Approved
576
     *    4 = Open
577
     *    5 = Closed
578
     * These statuses needs an immenent refactoring, so I'm not going to create
579
     * a lookup table for them yet.
580
     * TODO: When this is rebuilt, make sure to change matching JobStatus code in
581
     *    resources\assets\js\models\lookupConstants.ts
582
     *
583
     * @return integer
584
     */
585
    public function getJobStatusIdAttribute()
586
    {
587
        $now = new Date();
588
        if ($this->review_requested_at === null) {
589
            return 1; // Draft.
590
        } elseif ($this->published_at === null) {
591
            return 2; // Review requested, but not approved.
592
        } elseif ($this->open_date_time === null || $this->open_date_time >$now) {
593
            return 3; // Approved, but not open.
594
        } elseif ($this->close_date_time === null || $this->close_date_time > $now) {
595
            // Approved and currently open.
596
            return 4; // Open.
597
        } else {
598
            // Published and close date has passed.
599
            return 5; // Closed.
600
        }
601
    }
602
603
    /**
604
     * Return true if this job should be visible to hr advisors.
605
     * It should become visible after Manager has requested a review.
606
     *
607
     * @return boolean
608
     */
609
    public function isVisibleToHr()
610
    {
611
        return $this->job_status_id !== 1;
612
    }
613
614
    /**
615
     * The database model stores a foreign id to the classification table,
616
     * but to simplify the API, this model simply returns the key as classification_code.
617
     *
618
     * @return string|null
619
     */
620
    public function getClassificationCodeAttribute()
621
    {
622
        if ($this->classification_id !== null) {
623
            return $this->classification->key;
624
        }
625
        return null;
626
    }
627
628
    /**
629
     *
630
     * Get the full government classification message.
631
     *
632
     * @return string|null
633
     */
634
    public function getClassificationMessageAttribute()
635
    {
636
        if ($this->classification_id !== null && $this->classification_level !== null) {
637
            return $this->classification->key . '-0' . $this->classification_level;
638
        }
639
        return null;
640
    }
641
}
642