Passed
Push — dependabot/npm_and_yarn/dev/st... ( 790070...2dbd00 )
by
unknown
17:44 queued 12:22
created

JobPoster   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 526
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
wmc 60
eloc 227
c 4
b 0
f 1
dl 0
loc 526
rs 3.6

33 Methods

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

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