Failed Conditions
Branch master (3ce7e2)
by Nick
14:43
created

Divisions::getRecentMemberDivisions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 23
rs 9.0856
1
<?php
2
/**
3
 * Policy Positions
4
 *
5
 * @package TheyWorkForYou
6
 */
7
8
namespace MySociety\TheyWorkForYou;
9
10
class Divisions {
11
12
    /**
13
     * Member
14
     */
15
16
    private $member;
17
18
    /**
19
     * DB handle
20
     */
21
    private $db;
22
23
    private $positions;
24
    private $policies;
25
26
    /**
27
     * Constructor
28
     *
29
     * @param Member   $member   The member to get positions for.
30
     */
31
32
    public function __construct(Member $member = NULL, PolicyPositions $positions = NULL, Policies $policies = NULL)
33
    {
34
        $this->member = $member;
35
        $this->positions = $positions;
36
        $this->policies = $policies;
37
        $this->db = new \ParlDB;
38
    }
39
40
    public static function getMostRecentDivisionDate() {
41
      $db = new \ParlDB;
42
      $q = $db->query(
43
        "SELECT policy_id, max(division_date) as recent from policydivisions GROUP BY policy_id"
44
      );
45
46
      $policy_maxes = array();
47
      $row_count = $q->rows();
48
      for ($n = 0; $n < $row_count; $n++) {
49
        $policy_maxes[$q->field($n, 'policy_id')] = $q->field( $n, 'recent' );
50
      }
51
      $policy_maxes['latest'] = $policy_maxes ? max(array_values($policy_maxes)) : '';
52
      return $policy_maxes;
53
    }
54
55
    public function getRecentDivisions($number = 20) {
56
        // Grab distinct divisions as sometimes we have the same division for more than one policy
57
        // and we don't want to display it twice
58
        $q = $this->db->query(
59
          "SELECT distinct division_id, division_title, yes_text, no_text, division_date, division_number, gid, direction, majority_vote,
60
          yes_total, no_total, absent_total, both_total
61
          FROM policydivisions ORDER BY division_date DESC, division_number DESC LIMIT :count",
62
            array(
63
                ':count' => $number
64
            )
65
        );
66
67
        $divisions = array();
68
        foreach ($q->data as $division) {
69
            $divisions[] = $this->getParliamentDivisionDetails($division);
70
        }
71
72
        return array('divisions' => $divisions);
73
    }
74
75
    public function getRecentDivisionsForPolicies($policies, $number = 20) {
76
        $args = array(':number' => $number);
77
78
        $quoted = array();
79
        foreach ($policies as $policy) {
80
            $quoted[] = $this->db->quote($policy);
81
        }
82
        $policies_str = implode(',', $quoted);
83
84
        $q = $this->db->query(
85
            "SELECT division_id, division_title, yes_text, no_text, division_date, division_number, gid, direction, majority_vote,
86
            yes_total, no_total, absent_total, both_total
87
            FROM policydivisions
88
            WHERE policy_id in ($policies_str)
89
            GROUP BY division_id
90
            ORDER by division_date DESC LIMIT :number",
91
            $args
92
        );
93
94
        $divisions = array();
95
        $row_count = $q->rows();
96
        for ($n = 0; $n < $row_count; $n++) {
97
          $divisions[] = $this->getParliamentDivisionDetails($q->row($n));
98
        }
99
100
        return $divisions;
101
    }
102
103
    /**
104
     *
105
     * Get a list of division votes related to a policy
106
     *
107
     * Returns an array with one key ( the policyID ) containing a hash
108
     * with a policy_id key and a divisions key which contains an array
109
     * with details of all the divisions.
110
     *
111
     * Each division is a hash with the following fields:
112
     *    division_id, date, vote, gid, url, text, strong
113
     *
114
     * @param $policyId The ID of the policy to get divisions for
0 ignored issues
show
Bug introduced by
The type MySociety\TheyWorkForYou\The 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...
115
     */
116
117
    public function getMemberDivisionsForPolicy($policyID = null) {
118
        $where_extra = '';
119
        $args = array(':person_id' => $this->member->person_id);
120
        if ( $policyID ) {
121
            $where_extra = 'AND policy_id = :policy_id';
122
            $args[':policy_id'] = $policyID;
123
        }
124
        $q = $this->db->query(
125
            "SELECT policy_id, division_id, division_title, yes_text, no_text, division_date, division_number, vote, gid, direction
126
            FROM policydivisions JOIN persondivisionvotes USING(division_id)
127
            WHERE person_id = :person_id AND direction <> 'abstention' $where_extra
128
            ORDER by policy_id, division_date",
129
            $args
130
        );
131
132
        return $this->divisionsByPolicy($q);
133
    }
134
135
    public function getMemberDivisionDetails() {
136
        $args = array(':person_id' => $this->member->person_id);
137
138
        $policy_divisions = array();
139
140
        $q = $this->db->query(
141
            "SELECT policy_id, policy_vote, vote, count(division_id) as total,
142
            max(year(division_date)) as latest, min(year(division_date)) as earliest
143
            FROM policydivisions JOIN persondivisionvotes USING(division_id)
144
            WHERE person_id = :person_id AND direction <> 'abstention'
145
            GROUP BY policy_id, policy_vote, vote",
146
            $args
147
        );
148
149
        $row_count = $q->rows();
150
        for ($n = 0; $n < $row_count; $n++) {
151
          $policy_id = $q->field($n, 'policy_id');
152
153
          if (!array_key_exists($policy_id, $policy_divisions)) {
154
            $summary = array(
155
              'max' => $q->field($n, 'latest'),
156
              'min' => $q->field($n, 'earliest'),
157
              'total' => $q->field($n, 'total'),
158
              'for' => 0, 'against' => 0, 'absent' => 0, 'both' => 0, 'tell' => 0
159
            );
160
161
            $policy_divisions[$policy_id] = $summary;
162
          }
163
164
          $summary = $policy_divisions[$policy_id];
165
166
          $summary['total'] += $q->field($n, 'total');
167
          if ($summary['max'] < $q->field($n, 'latest')) {
168
              $summary['max'] = $q->field($n, 'latest');
169
          }
170
          if ($summary['min'] > $q->field($n, 'latest')) {
171
              $summary['min'] = $q->field($n, 'latest');
172
          }
173
174
          $vote = $q->field($n, 'vote');
175
          $policy_vote = str_replace('3', '', $q->field($n, 'policy_vote'));
176
          if ( $vote == 'absent' ) {
177
              $summary['absent'] += $q->field($n, 'total');
178
          } else if ( $vote == 'both' ) {
179
              $summary['both'] += $q->field($n, 'total');
180
          } else if ( strpos($vote, 'tell') !== FALSE ) {
181
              $summary['tell'] += $q->field($n, 'total');
182
          } else if ( $policy_vote == $vote ) {
183
              $summary['for'] += $q->field($n, 'total');
184
          } else if ( $policy_vote != $vote ) {
185
              $summary['against'] += $q->field($n, 'total');
186
          }
187
188
          $policy_divisions[$policy_id] = $summary;
189
        }
190
191
        return $policy_divisions;
192
    }
193
194
    public function getDivisionResults($division_id) {
195
        $args = array(
196
            ':division_id' => $division_id
197
        );
198
        $q = $this->db->query(
199
            "SELECT division_id, division_title, yes_text, no_text, division_date, division_number, gid, direction,
200
            yes_total, no_total, absent_total, both_total, majority_vote
201
            FROM policydivisions
202
            WHERE division_id = :division_id",
203
            $args
204
        );
205
206
        if ($q->rows == 0) {
207
            return false;
208
        }
209
210
        $details = $this->getParliamentDivisionDetails($q->row(0));
211
        $details['division_title'] = $q->row(0)['division_title'];
212
213
        $q = $this->db->query(
214
            "SELECT person_id, vote, given_name, family_name
215
            FROM persondivisionvotes JOIN person_names USING(person_id)
216
            WHERE division_id = :division_id
217
            ORDER by family_name",
218
            $args
219
        );
220
221
        $votes = array(
222
          'yes_votes' => array(),
223
          'no_votes' => array(),
224
          'absent_votes' => array(),
225
          'both_votes' => array()
226
        );
227
228
        foreach ($q->data as $vote) {
229
            $detail = array(
230
              'person_id' => $vote['person_id'],
231
              'name' => $vote['given_name'] . ' ' . $vote['family_name'],
232
              'teller' => false
233
            );
234
235
            if (strpos($vote['vote'], 'tell') !== FALSE) {
236
                $detail['teller'] = true;
237
            }
238
239
            if ($vote['vote'] == 'aye' or $vote['vote'] == 'tellaye') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
240
              $votes['yes_votes'][] = $detail;
241
            } else if ($vote['vote'] == 'no' or $vote['vote'] == 'tellno') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
242
              $votes['no_votes'][] = $detail;
243
            } else if ($vote['vote'] == 'absent') {
244
              $votes['absent_votes'][] = $detail;
245
            } else if ($vote['vote'] == 'both') {
246
              $votes['both_votes'][] = $detail;
247
            }
248
249
        }
250
251
        $details = array_merge($details, $votes);
252
253
        return $details;
254
    }
255
256
    public function getDivisionResultsForMember($division_id, $person_id) {
257
        $args = array(
258
            ':division_id' => $division_id,
259
            ':person_id' => $person_id
260
        );
261
        $q = $this->db->query(
262
            "SELECT division_id, division_title, yes_text, no_text, division_date, division_number, gid, direction, vote
263
            FROM policydivisions JOIN persondivisionvotes USING(division_id)
264
            WHERE division_id = :division_id AND person_id = :person_id",
265
            $args
266
        );
267
268
        // if the vote was before or after the MP was in Parliament
269
        // then there won't be a row
270
        if ($q->rows == 0) {
271
            return false;
272
        }
273
274
        $details = $this->getDivisionDetails($q->row(0));
275
        return $details;
276
    }
277
278
    public function generateSummary($votes) {
279
        $max = $votes['max'];
280
        $min = $votes['min'];
281
282
        $actions = array(
283
            $votes['for'] . ' ' . make_plural('vote', $votes['for']) . ' for',
284
            $votes['against'] . ' ' . make_plural('vote', $votes['against']) . ' against'
285
        );
286
287
        if ( $votes['both'] ) {
288
            $actions[] = $votes['both'] . ' ' . make_plural('abstention', $votes['both']);
289
        }
290
        if ( $votes['absent'] ) {
291
            $actions[] = $votes['absent'] . ' ' . make_plural('absence', $votes['absent']);
292
        }
293
        if ($max == $min) {
294
            return join(', ', $actions) . ', in ' . $max;
295
        } else {
296
            return join(', ', $actions) . ', between ' . $min . '&ndash;' . $max;
297
        }
298
    }
299
300
    /**
301
     *
302
     * Get all the divisions a member has voted in keyed by policy
303
     *
304
     * Returns an array with keys for each policyID, each of these contains
305
     * the same structure as getMemberDivisionsForPolicy
306
     *
307
     */
308
309
    public function getAllMemberDivisionsByPolicy() {
310
        return $this->getMemberDivisionsForPolicy();
311
    }
312
313
314
    /**
315
     * Get the last n votes for a member
316
     *
317
     * @param $number int - How many divisions to return. Defaults to 20
318
     * @param $context string - The context of the page the results are being presented in.
319
     *    This affects the summary details and can either be 'Parliament' in which case the
320
     *    overall vote for all MPs is returned, plus additional information on how the MP passed
321
     *    in to the constructor voted, or the default of 'MP' which is just the vote of the
322
     *    MP passed in to the constructor.
323
     *
324
     * Returns an array of divisions
325
     */
326
    public function getRecentMemberDivisions($number = 20, $context = 'MP') {
327
        $args = array(':person_id' => $this->member->person_id, ':number' => $number);
328
        $q = $this->db->query(
329
            "SELECT division_id, division_title, yes_text, no_text, division_date, division_number, vote, gid, direction,
330
            yes_total, no_total, absent_total, both_total, majority_vote
331
            FROM policydivisions JOIN persondivisionvotes USING(division_id)
332
            WHERE person_id = :person_id
333
            GROUP BY division_id
334
            ORDER by division_date DESC, division_id DESC LIMIT :number",
335
            $args
336
        );
337
338
        $divisions = array();
339
        $row_count = $q->rows();
340
        for ($n = 0; $n < $row_count; $n++) {
341
          if ($context == 'Parliament') {
342
              $divisions[] = $this->getParliamentDivisionDetails($q->row($n));
343
          } else {
344
              $divisions[] = $this->getDivisionDetails($q->row($n));
345
          }
346
        }
347
348
        return $divisions;
349
    }
350
351
352
    private function constructYesNoVoteDescription($direction, $title, $short_text) {
353
        $text = ' voted ';
354
        if ( $short_text ) {
355
            $text .= $short_text;
356
        } else {
357
            $text .= "$direction on <em>$title</em>";
358
        }
359
360
        return $text;
361
    }
362
363
364
    private function constructVoteDescription($vote, $yes_text, $no_text, $division_title) {
365
        /*
366
         * for most post 2010 votes we have nice single sentence summaries of
367
         * what voting for or against means so we use that if it's there, however
368
         * we don't have anything nice for people being absent or for pre 2010
369
         * votes so we need to generate some text using the title of the division
370
         */
371
372
        switch ( strtolower($vote) ) {
373
            case 'yes':
374
            case 'aye':
375
                $description = $this->constructYesNoVoteDescription('yes', $division_title, $yes_text);
376
                break;
377
            case 'no':
378
                $description = $this->constructYesNoVoteDescription('no', $division_title, $no_text);
379
                break;
380
            case 'absent':
381
                $description = ' was absent for a vote on <em>' . $division_title . '</em>';
382
                break;
383
            case 'both':
384
                $description = ' abstained on a vote on <em>' . $division_title . '</em>';
385
                break;
386
            case 'tellyes':
387
            case 'tellno':
388
            case 'tellaye':
389
                $description = ' acted as teller for a vote on <em>' . $division_title . '</em>';
390
                break;
391
            default:
392
                $description = $division_title;
393
        }
394
395
        return $description;
396
    }
397
398
    private function getBasicDivisionDetails($row, $vote) {
399
        $division = array();
400
401
        $direction = $row['direction'];
402
        if ( strpos( $direction, 'strong') !== FALSE ) {
403
            $division['strong'] = TRUE;
404
        } else {
405
            $division['strong'] = FALSE;
406
        }
407
408
        $division['division_id'] = $row['division_id'];
409
        $division['date'] = $row['division_date'];
410
        $division['gid'] = fix_gid_from_db($row['gid']);
411
        $division['url'] = $this->divisionUrlFromGid($row['gid']);
412
        $division['direction'] = $direction;
413
        $division['number'] = $row['division_number'];
414
415
        $yes_text = $row['yes_text'];
416
        $no_text = $row['no_text'];
417
        $division_title = $row['division_title'];
418
        $division['text'] = $this->constructVoteDescription($vote, $yes_text, $no_text, $division_title);
419
420
        $division['has_description'] = false;
421
        if ($yes_text && $no_text) {
422
            $division['has_description'] = true;
423
        }
424
425
        if ($row['gid']) {
426
            $division['debate_url'] = $this->divisionUrlFromGid($row['gid']);
427
        }
428
429
        return $division;
430
    }
431
432
    private function getDivisionDetails($row) {
433
        $vote = $row['vote'];
434
        $division = $this->getBasicDivisionDetails($row, $vote);
435
436
        $division['vote'] = $vote;
437
438
        return $division;
439
    }
440
441
    private function getParliamentDivisionDetails($row) {
442
        $vote = $row['majority_vote'];
443
        $division = $this->getBasicDivisionDetails($row, $vote);
444
445
        $division['mp_vote'] = '';
446
        if (array_key_exists('vote', $row)) {
447
          $mp_vote = ' was absent';
448
          if ($row['vote'] == 'aye') {
449
              $mp_vote = 'voted in favour';
450
          } else if ($row['vote'] == 'no') {
451
              $mp_vote = 'voted against';
452
          }
453
          $division['mp_vote'] = $mp_vote;
454
        }
455
        $division['division_title'] = $row['division_title'];
456
        $division['vote'] = $vote;
457
458
        $division['summary'] = $row['yes_total'] . ' for, ' . $row['no_total'] . ' against, ' . $row['absent_total'] . ' absent';
459
        $division['for'] = $row['yes_total'];
460
        $division['against'] = $row['no_total'];
461
        $division['both'] = $row['both_total'];
462
        $division['absent'] = $row['absent_total'];
463
464
        return $division;
465
    }
466
467
    private function divisionsByPolicy($q) {
468
        $policies = array();
469
470
        $row_count = $q->rows();
471
        for ($n = 0; $n < $row_count; $n++) {
472
            $policy_id = $q->field($n, 'policy_id');
473
474
            if ( !array_key_exists($policy_id, $policies) ) {
475
                $policies[$policy_id] = array(
476
                    'policy_id' => $policy_id,
477
                    'weak_count' => 0,
478
                    'divisions' => array()
479
                );
480
                if ( $this->policies ) {
481
                    $policies[$policy_id]['desc'] = $this->policies->getPolicies()[$policy_id];
482
                    $policies[$policy_id]['header'] = $this->policies->getPolicyDetails($policy_id);
483
                }
484
                if ( $this->positions ) {
485
                    $policies[$policy_id]['position'] = $this->positions->positionsById[$policy_id];
486
                }
487
            }
488
489
            $division = $this->getDivisionDetails($q->row($n));
490
491
            if ( !$division['strong'] ) {
492
                $policies[$policy_id]['weak_count']++;
493
            }
494
495
            $policies[$policy_id]['divisions'][] = $division;
496
        };
497
498
        return $policies;
499
    }
500
501
    private function divisionUrlFromGid($gid) {
502
        global $hansardmajors;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
503
504
        $gid = get_canonical_gid($gid);
505
506
        $q = $this->db->query("SELECT gid, major FROM hansard WHERE epobject_id = ( SELECT subsection_id FROM hansard WHERE gid = :gid )", array( ':gid' => $gid ));
507
        $parent_gid = $q->field(0, 'gid');
508
        if ( !$parent_gid ) {
509
            return '';
510
        }
511
        $parent_gid = fix_gid_from_db($parent_gid);
512
        $url = new Url($hansardmajors[$q->field(0, 'major')]['page']);
513
        $url->insert(array('gid' => $parent_gid));
514
        return $url->generate();
515
    }
516
}
517