Divisions::generateSummary()   A
last analyzed

Complexity

Conditions 6
Paths 32

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 32
nop 1
dl 0
loc 27
rs 9.0777
c 0
b 0
f 0
1
<?php
2
/**
3
 * Policy Positions
4
 *
5
 * @package TheyWorkForYou
6
 */
7
8
namespace MySociety\TheyWorkForYou;
9
10
class Divisions {
11
    /**
12
     * Member
13
     */
14
15
    private $member;
16
17
    /**
18
     * DB handle
19
     */
20
    private $db;
21
22
    private $positions;
23
    private $policies;
24
25
    /**
26
     * Constructor
27
     *
28
     * @param Member   $member   The member to get positions for.
29
     */
30
31
    public function __construct(?Member $member = null) {
32
        $this->member = $member;
33
        $this->positions = null;
34
        $this->policies = new Policies();
35
        $this->db = new \ParlDB();
36
    }
37
38
    public static function getMostRecentDivisionDate() {
39
        $db = new \ParlDB();
40
        $q = $db->query(
41
            "SELECT policy_id, max(division_date) as recent
42
            FROM policydivisionlink
43
                JOIN divisions USING(division_id)
44
            GROUP BY policy_id"
45
        );
46
47
        $policy_maxes = [];
48
        foreach ($q as $row) {
49
            $policy_maxes[$row['policy_id']] = $row['recent'];
50
        }
51
        $policy_maxes['latest'] = $policy_maxes ? max(array_values($policy_maxes)) : '';
52
        return $policy_maxes;
53
    }
54
55
    /**
56
     * @param  int              $number  Number of divisions to return. Optional.
57
     * @param  string|string[]  $houses  House name (eg: "commons") or array of
58
     *                                   house names. Optional.
59
     */
60
    public function getRecentDivisions($number = 20, $houses = null) {
61
        $select = '';
62
        $order = 'ORDER BY division_date DESC, division_number DESC';
63
        $limit = 'LIMIT :count';
64
        $params = [
65
            ':count' => $number,
66
        ];
67
68
        $where = [];
69
        if ($houses) {
70
            if (is_string($houses)) {
71
                $houses = [ $houses ];
72
            }
73
            $where[] = 'house IN ("' . implode('", "', $houses) . '")';
74
        }
75
        if (!$houses || in_array('senedd', $houses)) {
0 ignored issues
show
Bug introduced by
It seems like $houses can also be of type string; however, parameter $haystack of in_array() 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

75
        if (!$houses || in_array('senedd', /** @scrutinizer ignore-type */ $houses)) {
Loading history...
76
            if (LANGUAGE == 'cy') {
0 ignored issues
show
introduced by
The condition MySociety\TheyWorkForYou\LANGUAGE == 'cy' is always false.
Loading history...
77
                $where[] = "divisions.division_id NOT LIKE '%-en-%'";
78
            } else {
79
                $where[] = "divisions.division_id NOT LIKE '%-cy-%'";
80
            }
81
        }
82
        $where = 'WHERE ' . join(' AND ', $where);
83
84
        if ($this->member) {
85
            $select = "SELECT divisions.*, vote FROM divisions
86
                LEFT JOIN persondivisionvotes ON divisions.division_id=persondivisionvotes.division_id AND person_id=:person_id";
87
            $params[':person_id'] = $this->member->person_id;
88
        } else {
89
            $select = "SELECT * FROM divisions";
90
        }
91
92
        $q = $this->db->query(
93
            sprintf("%s %s %s %s", $select, $where, $order, $limit),
94
            $params
95
        );
96
97
        $divisions = [];
98
        foreach ($q as $division) {
99
            $data = $this->getParliamentDivisionDetails($division);
100
101
            $mp_vote = '';
102
            if (array_key_exists('vote', $division)) {
103
                if ($division['vote'] == 'aye') {
104
                    $mp_vote = 'voted in favour';
105
                } elseif ($division['vote'] == 'tellaye') {
106
                    $mp_vote = 'voted (as a teller) in favour';
107
                } elseif ($division['vote'] == 'no') {
108
                    $mp_vote = 'voted against';
109
                } elseif ($division['vote'] == 'tellno') {
110
                    $mp_vote = 'voted (as a teller) against';
111
                } elseif ($division['vote'] == 'absent') {
112
                    $mp_vote = ' was absent';
113
                } elseif ($division['vote'] == 'both') {
114
                    $mp_vote = ' abstained';
115
                }
116
            }
117
            $data['mp_vote'] = $mp_vote;
118
            $house = Utility\House::division_house_name_to_number($division['house']);
119
            $data['members'] = \MySociety\TheyWorkForYou\Utility\House::house_to_members($house);
120
            $divisions[] = $data;
121
        }
122
123
        return ['divisions' => $divisions];
124
    }
125
126
    /**
127
     * @param  int              $number  Number of divisions to return. Optional.
128
     * @param  int|int[]        $majors  Major types (e.g. 1) or array of
129
     *                                   major types. Optional.
130
     */
131
    public function getRecentDebatesWithDivisions($number = 20, $majors = null) {
132
        global $hansardmajors;
133
134
        if (!is_array($majors)) {
135
            $majors = [$majors];
136
        }
137
138
        $where = '';
139
        if (count($majors) > 0) {
140
            $where = 'AND h.major IN (' . implode(', ', $majors) . ')';
141
        }
142
143
        # Fetch any division speech, its subsection gid for the link, and
144
        # section/subsection bodies to construct a debate title
145
        $q = $this->db->query(
146
            "SELECT min(eps.body) as section_body, min(epss.body) as subsection_body,
147
                min(ss.gid) as debate_gid, min(h.gid) AS gid, min(h.hdate) as hdate,
148
                min(h.major) as major, count(h.gid) AS c
149
            FROM hansard h, hansard ss, epobject eps, epobject epss
150
            WHERE h.section_id = eps.epobject_id
151
                AND h.subsection_id = epss.epobject_id
152
                AND h.subsection_id = ss.epobject_id
153
                AND h.htype=14
154
            $where
155
            GROUP BY h.subsection_id
156
            ORDER BY h.hdate DESC, h.hpos DESC
157
            LIMIT :count",
158
            [':count' => $number]
159
        );
160
161
        $debates = [];
162
        foreach ($q as $debate) {
163
            $debate_gid = fix_gid_from_db($debate['debate_gid']);
164
            $anchor = '';
165
            if ($debate['c'] == 1) {
166
                $anchor = '#g' . gid_to_anchor(fix_gid_from_db($debate['gid']));
167
            }
168
            $url = new Url($hansardmajors[$debate['major']]['page']);
169
            $url->insert(['gid' => $debate_gid]);
170
            $debates[] = [
171
                'url' => $url->generate() . $anchor,
172
                'title' => "$debate[section_body] : $debate[subsection_body]",
173
                'date' => $debate['hdate'],
174
            ];
175
        }
176
177
        return $debates;
178
    }
179
180
    public function getRecentDivisionsForPolicies($policies, $number = 20) {
181
        $args = [':number' => $number];
182
183
        $quoted = [];
184
        foreach ($policies as $policy) {
185
            $quoted[] = $this->db->quote($policy);
186
        }
187
        $policies_str = implode(',', $quoted);
188
189
        $q = $this->db->query(
190
            "SELECT divisions.*
191
            FROM policydivisionlink
192
                JOIN divisions USING(division_id)
193
            WHERE policy_id in ($policies_str)
194
            GROUP BY division_id
195
            ORDER by division_date DESC LIMIT :number",
196
            $args
197
        );
198
199
        $divisions = [];
200
        foreach ($q as $row) {
201
            $divisions[] = $this->getParliamentDivisionDetails($row);
202
        }
203
204
        return $divisions;
205
    }
206
207
    /**
208
     *
209
     * Get a list of division votes related to a policy
210
     *
211
     * Returns an array with one key ( the policyID ) containing a hash
212
     * with a policy_id key and a divisions key which contains an array
213
     * with details of all the divisions.
214
     *
215
     * Each division is a hash with the following fields:
216
     *    division_id, date, vote, gid, url, text, strong
217
     *
218
     * @param int|null $policyId The ID of the policy to get divisions for
219
     */
220
221
    public function getMemberDivisionsForPolicy($policyID = null) {
222
        $where_extra = '';
223
        $args = [':person_id' => $this->member->person_id];
224
        if ($policyID) {
225
            $where_extra = 'AND policy_id = :policy_id';
226
            $args[':policy_id'] = $policyID;
227
        }
228
        $q = $this->db->query(
229
            "SELECT policy_id, division_id, division_title, yes_text, no_text, division_date, division_number, vote, gid, strength, alignment
230
            FROM policydivisionlink JOIN persondivisionvotes USING(division_id)
231
                JOIN divisions USING(division_id)
232
            WHERE person_id = :person_id AND direction <> 'abstention' $where_extra
233
            ORDER by policy_id, division_date DESC",
234
            $args
235
        );
236
        # possibly add another query here to get related policies that use the same votes
237
        return $this->divisionsByPolicy($q);
238
    }
239
    public function getDivisionByGid($gid) {
240
        $args = [
241
            ':gid' => $gid,
242
        ];
243
        $q = $this->db->query("SELECT * FROM divisions WHERE gid = :gid", $args)->first();
244
245
        if (!$q) {
246
            return false;
247
        }
248
249
        return $this->_division_data($q);
250
    }
251
252
    public function getDivisionResults($division_id) {
253
        $args = [
254
            ':division_id' => $division_id,
255
        ];
256
        $q = $this->db->query("SELECT * FROM divisions WHERE division_id = :division_id", $args)->first();
257
258
        if (!$q) {
259
            return false;
260
        }
261
262
        return $this->_division_data($q);
263
264
    }
265
266
    private function _division_data($row) {
267
268
        $details = $this->getParliamentDivisionDetails($row);
269
270
        $house = $row['house'];
271
        $args['division_id'] = $row['division_id'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$args was never initialized. Although not strictly required by PHP, it is generally a good practice to add $args = array(); before regardless.
Loading history...
272
        $args['division_date'] = $row['division_date'];
273
        $args['house'] = \MySociety\TheyWorkForYou\Utility\House::division_house_name_to_number($house);
274
275
        $q = $this->db->query(
276
            "SELECT pdv.person_id, vote, proxy, title, given_name, family_name, lordofname, party
277
            FROM persondivisionvotes AS pdv JOIN person_names AS pn ON (pdv.person_id = pn.person_id)
278
            JOIN member AS m ON (pdv.person_id = m.person_id)
279
            WHERE division_id = :division_id
280
            AND house = :house AND entered_house <= :division_date AND left_house >= :division_date
281
            AND start_date <= :division_date AND end_date >= :division_date
282
            ORDER by family_name",
283
            $args
284
        );
285
286
        $votes = [
287
            'yes_votes' => [],
288
            'no_votes' => [],
289
            'absent_votes' => [],
290
            'both_votes' => [],
291
        ];
292
293
        $party_breakdown = [
294
            'yes_votes' => [],
295
            'no_votes' => [],
296
            'absent_votes' => [],
297
            'both_votes' => [],
298
        ];
299
300
        # Sort Lords specially
301
        $data = $q->fetchAll();
302
        if ($args['house'] == HOUSE_TYPE_LORDS) {
303
            uasort($data, 'by_peer_name');
304
        }
305
306
        foreach ($data as $vote) {
307
            $detail = [
308
                'person_id' => $vote['person_id'],
309
                'name' => ucfirst(member_full_name(
310
                    $args['house'],
311
                    $vote['title'],
312
                    $vote['given_name'],
313
                    $vote['family_name'],
314
                    $vote['lordofname']
315
                )),
316
                'party' => $vote['party'],
317
                'proxy' => false,
318
                'teller' => false,
319
            ];
320
321
            if (strpos($vote['vote'], 'tell') !== false) {
322
                $detail['teller'] = true;
323
            }
324
325
            if ($vote['proxy']) {
326
                $q = $this->db->query(
327
                    "SELECT title, given_name, family_name, lordofname
328
                    FROM person_names AS pn
329
                    WHERE person_id = :person_id
330
                    AND start_date <= :division_date AND end_date >= :division_date",
331
                    [ ':person_id' => $vote['proxy'], ':division_date' => $row['division_date'] ]
332
                )->first();
333
                $detail['proxy'] = ucfirst(member_full_name(
334
                    HOUSE_TYPE_COMMONS,
335
                    $q['title'],
336
                    $q['given_name'],
337
                    $q['family_name'],
338
                    $q['lordofname']
339
                ));
340
            }
341
342
            if ($vote['vote'] == 'aye' or $vote['vote'] == 'tellaye') {
343
                $votes['yes_votes'][] = $detail;
344
                @$party_breakdown['yes_votes'][$detail['party']]++;
345
            } elseif ($vote['vote'] == 'no' or $vote['vote'] == 'tellno') {
346
                $votes['no_votes'][] = $detail;
347
                @$party_breakdown['no_votes'][$detail['party']]++;
348
            } elseif ($vote['vote'] == 'absent') {
349
                $votes['absent_votes'][] = $detail;
350
                @$party_breakdown['absent_votes'][$detail['party']]++;
351
            } elseif ($vote['vote'] == 'both') {
352
                $votes['both_votes'][] = $detail;
353
                @$party_breakdown['both_votes'][$detail['party']]++;
354
            }
355
        }
356
357
        foreach ($votes as $vote => $count) { // array('yes_votes', 'no_votes', 'absent_votes', 'both_votes') as $vote) {
358
            $votes[$vote . '_by_party'] = $votes[$vote];
359
            usort($votes[$vote . '_by_party'], function ($a, $b) {
360
                return $a['party'] > $b['party'];
361
            });
362
        }
363
364
        foreach ($party_breakdown as $vote => $parties) {
365
            $summary = [];
366
            foreach ($parties as $party => $count) {
367
                array_push($summary, "$party: $count");
368
            }
369
370
            sort($summary);
371
            $party_breakdown[$vote] = implode(', ', $summary);
372
        }
373
374
        $details = array_merge($details, $votes);
375
        $details['party_breakdown'] = $party_breakdown;
376
        $details['members'] = \MySociety\TheyWorkForYou\Utility\House::house_to_members($args['house']);
377
        $details['house'] = $house;
378
        $details['house_number'] = $args['house'];
379
380
        return $details;
381
    }
382
383
    public function getDivisionResultsForMember($division_id, $person_id) {
384
        $args = [
385
            ':division_id' => $division_id,
386
            ':person_id' => $person_id,
387
        ];
388
        $q = $this->db->query(
389
            "SELECT division_id, division_title, yes_text, no_text, division_date, division_number, gid, vote
390
            FROM divisions JOIN persondivisionvotes USING(division_id)
391
            WHERE division_id = :division_id AND person_id = :person_id",
392
            $args
393
        )->first();
394
395
        // if the vote was before or after the MP was in Parliament
396
        // then there won't be a row
397
        if (!$q) {
398
            return false;
399
        }
400
401
        $details = $this->getDivisionDetails($q);
402
        return $details;
403
    }
404
405
    public function generateSummary($votes) {
406
        $max = $votes['max'];
407
        $min = $votes['min'];
408
409
        $actions = [
410
            $votes['for'] . ' ' . make_plural('vote', $votes['for']) . ' for',
411
            $votes['against'] . ' ' . make_plural('vote', $votes['against']) . ' against',
412
        ];
413
414
        if ($votes['agreements_for']) {
415
            $actions[] = $votes['agreements_for'] . ' ' . make_plural('agreement', $votes['agreements_for']) . ' for';
416
        }
417
418
        if ($votes['agreements_against']) {
419
            $actions[] = $votes['agreements_against'] . ' ' . make_plural('agreement', $votes['agreements_against']) . ' against';
420
        }
421
422
        if ($votes['both']) {
423
            $actions[] = $votes['both'] . ' ' . make_plural('abstention', $votes['both']);
424
        }
425
        if ($votes['absent']) {
426
            $actions[] = $votes['absent'] . ' ' . make_plural('absence', $votes['absent']);
427
        }
428
        if ($max == $min) {
429
            return join(', ', $actions) . ', in ' . $max;
430
        } else {
431
            return join(', ', $actions) . ', between ' . $min . '&ndash;' . $max;
432
        }
433
    }
434
435
    /**
436
     *
437
     * Get all the divisions a member has voted in keyed by policy
438
     *
439
     * Returns an array with keys for each policyID, each of these contains
440
     * the same structure as getMemberDivisionsForPolicy
441
     *
442
     */
443
444
    public function getAllMemberDivisionsByPolicy() {
445
        $policies = $this->getMemberDivisionsForPolicy();
446
        return Utility\Shuffle::keyValue($policies);
447
    }
448
449
450
    /**
451
     * Get the last n votes for a member
452
     *
453
     * @param $number int - How many divisions to return. Defaults to 20
454
     * @param $context string - The context of the page the results are being presented in.
455
     *    This affects the summary details and can either be 'Parliament' in which case the
456
     *    overall vote for all MPs is returned, plus additional information on how the MP passed
457
     *    in to the constructor voted, or the default of 'MP' which is just the vote of the
458
     *    MP passed in to the constructor.
459
     *
460
     * Returns an array of divisions
461
     */
462
    public function getRecentMemberDivisions($number = 20) {
463
        $args = [':person_id' => $this->member->person_id, ':number' => $number];
464
        $q = $this->db->query(
465
            "SELECT *
466
            FROM persondivisionvotes
467
                JOIN divisions USING(division_id)
468
            WHERE person_id = :person_id
469
            ORDER by division_date DESC, division_number DESC, division_id DESC LIMIT :number",
470
            $args
471
        );
472
473
        $divisions = [];
474
        foreach ($q as $row) {
475
            $divisions[] = $this->getDivisionDetails($row);
476
        }
477
478
        return $divisions;
479
    }
480
481
    private function constructYesNoVoteDescription($direction, $title, $short_text) {
482
        $text = ' ' ;
483
        if ($short_text) {
484
            $text .= sprintf(gettext('voted %s'), $short_text);
485
        } else {
486
            $text .= sprintf(gettext('voted %s on <em>%s</em>'), $direction, $title);
487
        }
488
489
        return $text;
490
    }
491
492
    private function constructVoteDescription($vote, $yes_text, $no_text, $division_title) {
493
        /*
494
         * for most post 2010 votes we have nice single sentence summaries of
495
         * what voting for or against means so we use that if it's there, however
496
         * we don't have anything nice for people being absent or for pre 2010
497
         * votes so we need to generate some text using the title of the division
498
         */
499
500
        switch (strtolower($vote)) {
501
            case 'yes':
502
            case 'aye':
503
                $description = $this->constructYesNoVoteDescription('yes', $division_title, $yes_text);
504
                break;
505
            case 'no':
506
                $description = $this->constructYesNoVoteDescription('no', $division_title, $no_text);
507
                break;
508
            case 'absent':
509
                $description = ' was absent for a vote on <em>' . $division_title . '</em>';
510
                break;
511
            case 'both':
512
                $description = ' abstained on a vote on <em>' . $division_title . '</em>';
513
                break;
514
            case 'tellyes':
515
            case 'tellno':
516
            case 'tellaye':
517
                $description = ' acted as teller for a vote on <em>' . $division_title . '</em>';
518
                break;
519
            default:
520
                $description = $division_title;
521
        }
522
523
        return $description;
524
    }
525
526
    private function getBasicDivisionDetails($row, $vote) {
527
        $yes_text = $row['yes_text'];
528
        $no_text = $row['no_text'];
529
530
        $division = [
531
            'division_id' => $row['division_id'],
532
            'date' => $row['division_date'],
533
            'gid' => fix_gid_from_db($row['gid']),
534
            'number' => $row['division_number'],
535
            'text' => $this->constructVoteDescription($vote, $yes_text, $no_text, $row['division_title']),
536
            'has_description' => $yes_text && $no_text,
537
            'vote' => $vote,
538
        ];
539
540
        if ($row['gid']) {
541
            $division['debate_url'] = $this->divisionUrlFromGid($row['gid']);
542
        }
543
544
        # Policy-related information
545
546
        # So one option is just to query for it here
547
        # we want to add an array of policies aside the current policy
548
        # and if they have the same or different direction as thie current division
549
        # in the row
550
551
        # fetch related policies from database
552
        $q = $this->db->query(
553
            "SELECT policy_id, direction, strength
554
            FROM policydivisionlink
555
            WHERE division_id = :division_id",
556
            [':division_id' => $row['division_id']]
557
        );
558
        $division['related_policies'] = [];
559
560
        $policy_lookup = $this->policies->getPolicies();
561
        foreach ($q as $policy) {
562
            $division['related_policies'][] = [
563
                'policy_id' => $policy['policy_id'],
564
                'policy_title' => preg_replace('#</?a[^>]*>#', '', $policy_lookup[$policy['policy_id']]),
565
                'direction' => $policy['direction'],
566
                'strong' => strtolower($policy["strength"]) === "strong",
567
            ];
568
        }
569
570
        return $division;
571
    }
572
573
    private function getDivisionDetails($row) {
574
        return $this->getBasicDivisionDetails($row, $row['vote']);
575
    }
576
577
    private function getParliamentDivisionDetails($row) {
578
        $division = $this->getBasicDivisionDetails($row, $row['majority_vote']);
579
580
        $division['division_title'] = $row['division_title'];
581
        $division['for'] = $row['yes_total'];
582
        $division['against'] = $row['no_total'];
583
        $division['both'] = $row['both_total'];
584
        $division['absent'] = $row['absent_total'];
585
586
        return $division;
587
    }
588
589
    private function divisionsByPolicy($q) {
590
        $policies = [];
591
592
        # iterate through each division, and adds it to an array of policies
593
        # if there is only one policy being queried, it will be an array of 1
594
        foreach ($q as $row) {
595
            $policy_id = $row['policy_id'];
596
597
            # if this policy hasn't come up yet, create the key for it
598
            if (!array_key_exists($policy_id, $policies)) {
599
                $policies[$policy_id] = [
600
                    'policy_id' => $policy_id,
601
                    'divisions' => [],
602
                ];
603
                $policies[$policy_id]['desc'] = $this->policies->getPolicies()[$policy_id];
604
                $policies[$policy_id]['header'] = $this->policies->getPolicyDetails($policy_id);
605
                if ($this->positions) {
606
                    $policies[$policy_id]['position'] = $this->positions->positionsById[$policy_id];
607
                }
608
            }
609
610
611
            $division = $this->getDivisionDetails($row);
612
613
            $policies[$policy_id]['divisions'][] = $division;
614
        };
615
616
        return $policies;
617
    }
618
619
    private function divisionUrlFromGid($gid) {
620
        global $hansardmajors;
621
622
        $gid = get_canonical_gid($gid);
623
624
        $q = $this->db->query("SELECT gid, major FROM hansard WHERE epobject_id = ( SELECT subsection_id FROM hansard WHERE gid = :gid )", [ ':gid' => $gid ])->first();
625
        if (!$q) {
626
            return '';
627
        }
628
        $parent_gid = fix_gid_from_db($q['gid']);
629
        $url = new Url($hansardmajors[$q['major']]['page']);
630
        $url->insert(['gid' => $parent_gid]);
631
        return $url->generate() . '#g' . gid_to_anchor(fix_gid_from_db($gid));
632
    }
633
}
634