Issues (408)

classes/Member.php (4 issues)

1
<?php
2
/**
3
 * Member Class
4
 *
5
 * @package TheyWorkForYou
6
 */
7
8
namespace MySociety\TheyWorkForYou;
9
10
/**
11
 * Member
12
 */
13
14
class Member extends \MEMBER {
15
    /**
16
     * Is Dead
17
     *
18
     * Determine if the member has died or not.
19
     *
20
     * @return boolean If the member is dead or not.
21
     */
22
23
    public function isDead() {
24
25
        $left_house = $this->left_house();
26
27
        if ($left_house) {
28
29
            // This member has left a house, and might be dead. See if they are.
30
31
            // Types of house to test for death.
32
            $house_types = [
33
                HOUSE_TYPE_COMMONS,
34
                HOUSE_TYPE_LORDS,
35
                HOUSE_TYPE_SCOTLAND,
36
                HOUSE_TYPE_NI,
37
                HOUSE_TYPE_WALES,
38
                HOUSE_TYPE_LONDON_ASSEMBLY,
39
            ];
40
41
            foreach ($house_types as $house_type) {
42
43
                if (in_array($house_type, $left_house) and
44
                    $left_house[$house_type]['reason'] and
45
                    $left_house[$house_type]['reason'] == 'Died'
46
                ) {
47
48
                    // This member has left a house because of death.
49
                    return true;
50
                }
51
52
            }
53
54
        }
55
56
        // If we get this far the member hasn't left a house due to death, and
57
        // is presumably alive.
58
        return false;
59
60
    }
61
62
    public function cohortPartyComparisonDirection() {
63
        // Is this MP and their cohort compared against the
64
        // first or last party they have?
65
        // By default, mirroring the partycohort query,
66
        // this is the first party.
67
        // However, this is ignored for the Speaker, and additional
68
        // individual overrides can be added below.
69
        // This makes most sense when a party switch was a long time ago.
70
        // As long as this person is in a cohort of 1 and has a unique set
71
        // of memberships (which is likely for edge cases) it doesn't matter
72 13
        // that their original party is what is used when in the party cohort
73
        // construction query.
74 13
75 13
        $person_id = $this->person_id();
76
77
        $direction = "first";
78 20
        if ($this->party() == "Speaker") {
79
            $direction = "last";
80
        }
81
82
        // MPs who have switched parties but should be compared against their
83
        // current party can go here.
84
        $use_last_party = [10172, 14031, 25873, 10218];
85
86
        if (in_array($person_id, $use_last_party)) {
87
            $direction = "last";
88
        }
89
90
        return $direction;
91 20
    }
92
93 20
    public function currentPartyComparison() {
94 20
        # Simplify the current party when being compared to the original
95 20
        # Stops co-op and labour being seen as different
96
        $party = $this->party;
97
        if ($party == 'Labour/Co-operative') {
98
            $party = 'Labour';
99
        }
100 20
        return $party;
101
    }
102 20
103 1
    public function cohortParty($house = HOUSE_TYPE_COMMONS) {
104
        // The party being compared against for party comparison purposes
105
        // Unless specified by the condition in cohortPartyComparisonDirection
106 20
        // This is the first, not last, party a person has.
107
108
        $person_id = $this->person_id();
109
        $db = new \ParlDB();
110
111
        $cohort_direction = $this->cohortPartyComparisonDirection();
112
113
        if ($cohort_direction == "first") {
114
            $direction = " ASC";
115
        } else {
116
            $direction = " DESC";
117
        }
118
119 20
        $row = $db->query("SELECT party from member
120
        where house = :house
121
        and person_id = :person_id
122
        and party != ''
123
        order by entered_house
124 20
        " . $direction, [":house" => $house,
125 20
            ":person_id" => $person_id])->first();
126
        if ($row) {
127 20
            $party = $row["party"];
128
            if ($party == 'Labour/Co-operative') {
129 20
                $party = 'Labour';
130 20
            } elseif ($party == 'Sinn Féin') {
131
                $party = 'Sinn Fein';
132 20
            }
133
            return slugify($party);
134
        } else {
135 20
            return null;
136
        }
137
138
    }
139
140 20
    /*
141 20
     * Determine if the member is a new member of a house where new is
142 20
     * within the last 6 months.
143 20
     *
144 20
     * @param int house - identifier for the house, defaults to 1 for westminster
0 ignored issues
show
The type MySociety\TheyWorkForYou\house was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
145
     *
146
     * @return boolean
147 20
     */
148
149
150
    public function isNew($house = HOUSE_TYPE_COMMONS) {
151
        $date_entered = $this->getEntryDate($house);
152
153
        if ($date_entered) {
154
            $date_entered = new \DateTime($date_entered);
155
            $now = new \DateTime();
156
157
            $diff = $date_entered->diff($now);
158
            if ($diff->y == 0 && $diff->m <= 6) {
159
                return true;
160
            }
161
        }
162
163
        return false;
164 1
    }
165 1
166
    /*
167 1
     * Get the date the person first entered a house. May not be for their current seat.
168 1
     *
169 1
     * @param int house - identifier for the house, defaults to 1 for westminster
170
     *
171 1
     * @return string - blank if no entry date for that house otherwise in YYYY-MM-DD format
172 1
     */
173 1
174
    public function getEntryDate($house = HOUSE_TYPE_COMMONS) {
175
        $date_entered = '';
176
177 1
        $entered_house = $this->entered_house($house);
178
179
        if ($entered_house) {
180
            $date_entered = $entered_house['date'];
181
        }
182
183
        return $date_entered;
184
    }
185
186
    /*
187
     * Get the date the person last left the house.
188 2
     *
189 2
     * @param int house - identifier for the house, defaults to 1 for westminster
190
     *
191 2
     * @return string - 9999-12-31 if they are still in that house otherwise in YYYY-MM-DD format
192
     */
193 2
194 2
    public function getLeftDate($house = HOUSE_TYPE_COMMONS) {
195
        $date_left = '';
196
197 2
        $left_house = $this->left_house($house);
198
199
        if ($left_house) {
200
            $date_left = $left_house['date'];
201
        }
202
203
        return $date_left;
204
    }
205
206
207
    public function getEUStance() {
208
        if (array_key_exists('eu_ref_stance', $this->extra_info())) {
209
            return $this->extra_info()['eu_ref_stance'];
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
    * Image
217
    *
218
    * Return a URL for the member's image.
219
    *
220
    * @return string The URL of the member's image.
221
    */
222
223
    public function image() {
224
225
        $is_lord = $this->house(HOUSE_TYPE_LORDS);
226
        if ($is_lord) {
227
            [$image, $size] = Utility\Member::findMemberImage($this->person_id(), false, 'lord');
228
        } else {
229
            [$image, $size] = Utility\Member::findMemberImage($this->person_id(), false, true);
230
        }
231
232
        // We can determine if the image exists or not by testing if size is set
233
        if ($size !== null) {
234
            $exists = true;
235
        } else {
236
            $exists = false;
237
        }
238
239
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('url' => $i...e, 'exists' => $exists) returns the type array<string,boolean|null|string> which is incompatible with the documented return type string.
Loading history...
240
            'url' => $image,
241
            'size' => $size,
242
            'exists' => $exists,
243
        ];
244
245
    }
246
247
    public function getMostRecentMembership() {
248
        $departures = $this->left_house();
249
250
        usort(
251
            $departures,
0 ignored issues
show
It seems like $departures can also be of type null; however, parameter $array of usort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

251
            /** @scrutinizer ignore-type */ $departures,
Loading history...
252
            function ($a, $b) {
253
                if ($a['date'] == $b['date']) {
254
                    return 0;
255
                } elseif ($a['date'] < $b['date']) {
256
                    return -1;
257
                } else {
258
                    return 1;
259
                }
260
            }
261
        );
262
263
        $latest_membership = array_slice($departures, -1)[0];
264
        $latest_membership['current'] = ($latest_membership['date'] == '9999-12-31');
265
        $latest_entrance = $this->entered_house($latest_membership['house']);
266
        $latest_membership['start_date'] = $latest_entrance['date'];
267
        $latest_membership['end_date'] = $latest_membership['date'];
268
        $latest_membership['rep_name'] = $this->getRepNameForHouse($latest_membership['house']);
269
270
        return $latest_membership;
271
    }
272
273
    /**
274
    * Offices
275
    *
276
    * Return an array of Office objects held (or previously held) by the member.
277
    *
278
    * @param string $include_only  Restrict the list to include only "previous" or "current" offices.
279
    * @param bool   $ignore_committees Ignore offices that appear to be committee memberships.
280
    * @param bool   $committees_only Only return committee memberships.
281
    *
282
    * @return array An array of Office objects.
283
    */
284
285
    public function offices($include_only = null, $ignore_committees = false, $committees_only = false) {
286
287
        $out = [];
288
289
        if (array_key_exists('office', $this->extra_info())) {
290
            $office = $this->extra_info();
291
            $office = $office['office'];
292
293
            foreach ($office as $row) {
294
                if ($officeObject = $this->getOfficeObject($include_only, $ignore_committees, $committees_only, $row)) {
295
                    $out[] = $officeObject;
296
                }
297
            }
298
        }
299
300
        return $out;
301
302
    }
303
304
    private function getOfficeObject($include_only, $ignore_committees, $committees_only, $row) {
305
        if (!$this->includeOffice($include_only, $row['to_date'])) {
306
            return null;
307
        }
308
        if ($ignore_committees && strpos($row['moffice_id'], 'Committee')) {
309
            return null;
310
        }
311
312
        if ($committees_only && !strpos($row['moffice_id'], 'Committee')) {
313
            return null;
314
        }
315
316
        $officeObject = new Office();
317
        $officeObject->title = prettify_office($row['position'], $row['dept']);
318
        $officeObject->position = $row['position'];
319
        $officeObject->dept = $row['dept'];
320
        $officeObject->from_date = $row['from_date'];
321
        $officeObject->to_date = $row['to_date'];
322
        $officeObject->source = $row['source'];
323
        return $officeObject;
324
    }
325
326
    private function includeOffice($include_only, $to_date) {
327
        $include_office = true;
328
329
        // If we should only include previous offices, and the to date is in the future, suppress this office.
330
        if ($include_only == 'previous' and $to_date == '9999-12-31') {
331
            $include_office = false;
332
        }
333
334
        // If we should only include previous offices, and the to date is in the past, suppress this office.
335
        if ($include_only == 'current' and $to_date != '9999-12-31') {
336
            $include_office = false;
337
        }
338
339
        return $include_office;
340
    }
341
342
    /**
343
    * Get Other Parties String
344
    *
345
    * Return a readable list of party changes for this member.
346
    *
347
    * @return string|null A readable list of the party changes for this member.
348
    */
349
350
    public function getOtherPartiesString() {
351
352
        if (!empty($this->other_parties) && $this->party != 'Speaker' && $this->party != 'Deputy Speaker') {
353
            $output = 'Party was ';
354
            $other_parties = [];
355
            foreach ($this->other_parties as $r) {
356
                $other_parties[] = $r['from'] . ' until ' . format_date($r['date'], SHORTDATEFORMAT);
357
            }
358
            $output .= join('; ', $other_parties);
359
            return $output;
360
        } else {
361
            return null;
362
        }
363
364
    }
365
366
    /**
367
    * Get Other Constituencies String
368
    *
369
    * Return a readable list of other constituencies for this member.
370
    *
371
    * @return string|null A readable list of the other constituencies for this member.
372
    */
373
374
    public function getOtherConstituenciesString() {
375
376
        if ($this->other_constituencies) {
377
            return 'Also represented ' . join('; ', array_keys($this->other_constituencies));
378
        } else {
379
            return null;
380
        }
381
382
    }
383
384
    /**
385
    * Get Entered/Left Strings
386
    *
387
    * Return an array of readable strings covering when people entered or left
388
    * various houses. Returns an array since it's possible for a member to have
389
    * done several of these things.
390
    *
391
    * @return array An array of strings of when this member entered or left houses.
392
    */
393
    public function getEnterLeaveStrings() {
394
        $output = [];
395
396
        $output[] = $this->entered_house_line(HOUSE_TYPE_LORDS, gettext('House of Lords'));
397
398
        if (isset($this->left_house[HOUSE_TYPE_COMMONS]) && isset($this->entered_house[HOUSE_TYPE_LORDS])) {
399
            $string = '<strong>';
400 1
            $string .= sprintf(gettext('Previously MP for %s until %s'), $this->left_house[HOUSE_TYPE_COMMONS]['constituency'], $this->left_house[HOUSE_TYPE_COMMONS]['date_pretty']);
401 1
            $string .= '</strong>';
402
            if ($this->left_house[HOUSE_TYPE_COMMONS]['reason']) {
403 1
                $string .= ' &mdash; ' . $this->left_house[HOUSE_TYPE_COMMONS]['reason'];
404
            }
405 1
            $output[] = $string;
406
        }
407
408
        $output[] = $this->left_house_line(HOUSE_TYPE_LORDS, gettext('House of Lords'));
409
410
        if (isset($this->extra_info['lordbio'])) {
411
            $output[] = '<strong>Positions held at time of appointment:</strong> ' . $this->extra_info['lordbio'] .
412
                ' <small>(from <a href="' .
413
                $this->extra_info['lordbio_from'] . '">Number 10 press release</a>)</small>';
414
        }
415 1
416
        $output[] = $this->entered_house_line(HOUSE_TYPE_COMMONS, gettext('House of Commons'));
417 1
418
        # If they became a Lord, we handled this above.
419
        if ($this->house(HOUSE_TYPE_COMMONS) && !$this->current_member(HOUSE_TYPE_COMMONS) && !$this->house(HOUSE_TYPE_LORDS)) {
420
            $output[] = $this->left_house_line(HOUSE_TYPE_COMMONS, gettext('House of Commons'));
421
        }
422
423 1
        $output[] = $this->entered_house_line(HOUSE_TYPE_NI, gettext('Assembly'));
424
        $output[] = $this->left_house_line(HOUSE_TYPE_NI, gettext('Assembly'));
425
        $output[] = $this->entered_house_line(HOUSE_TYPE_SCOTLAND, gettext('Scottish Parliament'));
426 1
        $output[] = $this->left_house_line(HOUSE_TYPE_SCOTLAND, gettext('Scottish Parliament'));
427
        $output[] = $this->entered_house_line(HOUSE_TYPE_WALES, gettext('Senedd'));
428
        $output[] = $this->left_house_line(HOUSE_TYPE_WALES, gettext('Senedd'));
429
        $output[] = $this->entered_house_line(HOUSE_TYPE_LONDON_ASSEMBLY, gettext('London Assembly'));
430 1
        $output[] = $this->left_house_line(HOUSE_TYPE_LONDON_ASSEMBLY, gettext('London Assembly'));
431 1
432 1
        $output = array_values(array_filter($output));
433 1
        return $output;
434 1
    }
435 1
436 1
    private function entered_house_line($house, $house_name) {
437 1
        if (isset($this->entered_house[$house]['date'])) {
438
            $string = "<strong>";
439 1
            if (strlen($this->entered_house[$house]['date_pretty']) == 4) {
440 1
                $string .= sprintf(gettext("Entered the %s in %s"), $house_name, $this->entered_house[$house]['date_pretty']);
441
            } else {
442
                $string .= sprintf(gettext("Entered the %s on %s"), $house_name, $this->entered_house[$house]['date_pretty']);
443 1
            }
444 1
            $string .= '</strong>';
445 1
            if ($this->entered_house[$house]['reason']) {
446 1
                $string .= ' &mdash; ' . $this->entered_house[$house]['reason'];
447
            }
448
            return $string;
449 1
        }
450
    }
451 1
452 1
    private function left_house_line($house, $house_name) {
453 1
        if ($this->house($house) && !$this->current_member($house)) {
454
            $string = "<strong>";
455 1
            if (strlen($this->left_house[$house]['date_pretty']) == 4) {
456
                $string .= sprintf(gettext("Left the %s in %s"), $house_name, $this->left_house[$house]['date_pretty']);
457 1
            } else {
458
                $string .= sprintf(gettext("Left the %s on %s"), $house_name, $this->left_house[$house]['date_pretty']);
459 1
            }
460 1
            $string .= '</strong>';
461 1
            if ($this->left_house[$house]['reason']) {
462 1
                $string .= ' &mdash; ' . $this->left_house[$house]['reason'];
463
            }
464
            return $string;
465 1
        }
466
    }
467 1
468 1
    public function getPartyPolicyDiffs($partyCohort, $policiesList, $positions) {
469 1
        $policy_diffs = [];
470
        $party_positions = $partyCohort->getAllPolicyPositions($policiesList);
471 1
472
        if (!$party_positions) {
473 1
            return $policy_diffs;
474
        }
475
476
        foreach ($positions->positionsById as $policy_id => $details) {
477
            if ($details['has_strong'] && $details['score'] != -1 && isset($party_positions[$policy_id])) {
478
                $mp_score = $details['score'];
479
                $party_position = $party_positions[$policy_id];
480
                $party_score = $party_position['score'];
481
                $year_min = substr($party_position['date_min'], 0, 4);
482
                $year_max = substr($party_position['date_max'], 0, 4);
483
                if ($year_min == $year_max) {
484
                    $party_voting_summary = sprintf("%d votes, in %d", $party_position['divisions'], $year_min);
485
                } else {
486
                    $party_voting_summary = sprintf("%d votes, between %d–%d", $party_position['divisions'], $year_min, $year_max);
487
                }
488
489
                $score_diff = $this->calculatePolicyDiffScore($mp_score, $party_score);
490
491
                $policy_diffs[$policy_id] = [
492
                    'policy_text' => $details['policy'],
493
                    'score_difference' => $score_diff,
494
                    'person_position' => $details['position'],
495
                    'summary' => $details['summary'],
496
                    'party_position' => $party_position['position'],
497
                    'party_voting_summary' => $party_voting_summary,
498
                ];
499
            }
500
        }
501
502
        uasort($policy_diffs, function ($a, $b) {
503
            return $b['score_difference'] - $a['score_difference'];
504
        });
505
506
        return $policy_diffs;
507
    }
508
509
    private function calculatePolicyDiffScore($mp_score, $party_score) {
510
        $score_diff = abs($mp_score - $party_score);
511
        // if they are on opposite sides of mixture of for and against
512
        if (
513
            ($mp_score < 0.4 && $party_score > 0.6) ||
514
            ($mp_score > 0.6 && $party_score < 0.4)
515
        ) {
516
            $score_diff += 2;
517
            // if on is mixture of for and against and one is for/against
518
        } elseif (
519
            ($mp_score > 0.4 && $mp_score < 0.6 && ($party_score > 0.6 || $party_score < 0.4)) ||
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: ($mp_score > 0.4 && $mp_... 0.6 || $mp_score < 0.4, Probably Intended Meaning: $mp_score > 0.4 && $mp_s...0.6 || $mp_score < 0.4)
Loading history...
520
            ($party_score > 0.4 && $party_score < 0.6 && ($mp_score > 0.6 || $mp_score < 0.4))
521
        ) {
522
            $score_diff += 1;
523
        }
524
525
        return $score_diff;
526
    }
527
528
    public static function getRegionalList($postcode, $house, $type) {
529
        $db = new \ParlDB();
530
531
        $mreg = [];
532
        $constituencies = \MySociety\TheyWorkForYou\Utility\Postcode::postcodeToConstituencies($postcode);
533
        if (isset($constituencies[$type])) {
534
            $cons_name = $constituencies[$type];
535
            $query_base = "SELECT member.person_id, title, lordofname, given_name, family_name, constituency, house
536
                FROM member, person_names
537
                WHERE
538
                member.person_id = person_names.person_id
539 1
                AND person_names.type = 'name'
540 1
                AND constituency = :cons_name
541
                AND house = :house
542 1
                AND left_house >= start_date
543 1
                AND left_house <= end_date";
544 1
            $q = $db->query(
545 1
                "$query_base AND left_reason = 'still_in_office'",
546 1
                [
547
                    ':house' => $house,
548
                    ':cons_name' => $cons_name,
549
                ]
550
            );
551
            if (!$q->rows() && ($dissolution = Dissolution::db())) {
552
                $q = $db->query(
553
                    "$query_base AND $dissolution[query]",
554
                    [
555 1
                        ':house' => $house,
556
                        ':cons_name' => $cons_name,
557 1
                    ] + $dissolution['params']
558 1
                );
559
            }
560
561 1
            foreach ($q as $row) {
562
                $name = member_full_name($house, $row['title'], $row['given_name'], $row['family_name'], $row['lordofname']);
563
                $mreg[] = [
564
                    'person_id' => $row['person_id'],
565
                    'name' => $name,
566
                    'house' => $row['house'],
567
                    'constituency' => gettext($row['constituency']),
568
                ];
569
            }
570 1
        }
571 1
572 1
        return $mreg;
573 1
    }
574 1
575 1
    public static function getRepNameForHouse($house) {
576 1
        switch ($house) {
577
            case HOUSE_TYPE_COMMONS:
578
                $name = 'MP';
579
                break;
580
            case HOUSE_TYPE_LORDS:
581 1
                $name = 'Peer';
582
                break;
583
            case HOUSE_TYPE_NI:
584
                $name = 'MLA';
585
                break;
586
            case HOUSE_TYPE_SCOTLAND:
587
                $name = 'MSP';
588
                break;
589
            case HOUSE_TYPE_WALES:
590
                $name = 'MS';
591
                break;
592
            case HOUSE_TYPE_LONDON_ASSEMBLY:
593
                $name = 'London Assembly Member';
594
                break;
595
            case HOUSE_TYPE_ROYAL:
596
                $name = 'Member of royalty';
597
                break;
598
            default:
599
                $name = '';
600
        }
601
        return $name;
602
    }
603
604
}
605