Passed
Pull Request — master (#1751)
by Matthew
34:50
created

Divisions::getMostRecentDivisionDate()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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