• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DemocracyClub / WhoCanIVoteFor / 78615e59-2602-4373-b4ce-ad20cc5b761e

07 Apr 2025 01:45PM UTC coverage: 58.389% (+0.1%) from 58.293%
78615e59-2602-4373-b4ce-ad20cc5b761e

Pull #2232

circleci

awdem
update PostcodeiCalView to new error handling
Pull Request #2232: handle invalid postcode errors and api 500s differently

31 of 31 new or added lines in 4 files covered. (100.0%)

89 existing lines in 6 files now uncovered.

2812 of 4816 relevant lines covered (58.39%)

0.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.62
/wcivf/apps/elections/views/mixins.py
1
import json
1✔
2
from datetime import date, datetime
1✔
3
from typing import Optional
1✔
4
from urllib.parse import urlencode
1✔
5

6
from core.utils import LastWord
1✔
7
from django.conf import settings
1✔
8
from django.core.cache import cache
1✔
9
from django.db.models import Case, Count, F, IntegerField, Prefetch, When
1✔
10
from django.db.models.functions import Coalesce
1✔
11
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
1✔
12
from django.urls import reverse
1✔
13
from django.views import View
1✔
14
from elections.constants import (
1✔
15
    PEOPLE_FOR_BALLOT_KEY_FMT,
16
    UPDATED_SLUGS,
17
)
18
from elections.devs_dc_client import DevsDCAPIException, DevsDCClient
1✔
19
from hustings.models import Husting
1✔
20
from leaflets.models import Leaflet
1✔
21
from uk_election_timetables.calendars import Country
1✔
22
from uk_election_timetables.election import TimetableEvent
1✔
23
from uk_election_timetables.election_ids import from_election_id
1✔
24

25
DEVS_DC_CLIENT = DevsDCClient()
1✔
26

27

28
class PostcodeToPostsMixin(object):
1✔
29
    def get(self, request, *args, **kwargs):
1✔
30
        from ..models import InvalidPostcodeError
1✔
31

32
        try:
1✔
33
            context = self.get_context_data(**kwargs)
1✔
34
        except (InvalidPostcodeError, DevsDCAPIException) as exception:
1✔
35
            querystring = self.gen_querystring_from_exception(exception)
1✔
36
            return HttpResponseRedirect(f"/?{querystring}")
1✔
37

38
        return self.render_to_response(context)
1✔
39

40
    def gen_querystring_from_exception(self, exception):
1✔
41
        params = {}
1✔
42

43
        if (
1✔
44
            hasattr(exception, "response")
45
            and exception.response.status_code >= 500
46
        ):
47
            params["api_response_error"] = 1
1✔
48
        else:
49
            params["invalid_postcode"] = 1
1✔
50

51
        params["postcode"] = self.postcode
1✔
52
        return urlencode(params)
1✔
53

54
    def postcode_to_ballots(self, postcode, uprn=None, compact=False):
1✔
55
        kwargs = {"postcode": postcode}
1✔
56
        if uprn:
1✔
57
            kwargs["uprn"] = uprn
1✔
58

59
        results_json = DEVS_DC_CLIENT.make_request(**kwargs)
1✔
60
        all_ballots = []
1✔
61
        ret = {
1✔
62
            "address_picker": results_json["address_picker"],
63
            "polling_station": {},
64
            "electoral_services": results_json["electoral_services"],
65
            "registration": results_json["registration"],
66
            "postcode_location": json.dumps(
67
                results_json.get("postcode_location", "")
68
            ),
69
        }
70

71
        if ret["address_picker"]:
1✔
72
            ret["addresses"] = results_json["addresses"]
1✔
73
            return ret
1✔
74

75
        for election_date in results_json.get("dates"):
1✔
76
            for ballot in election_date.get("ballots", []):
1✔
77
                all_ballots.append(ballot["ballot_paper_id"])
1✔
78
            if election_date["polling_station"]["polling_station_known"]:
1✔
79
                ret["polling_station_known"] = True
1✔
80
                ret["polling_station"] = election_date["polling_station"]
1✔
81

82
        from ..models import PostElection
1✔
83

84
        pes = PostElection.objects.filter(ballot_paper_id__in=all_ballots)
1✔
85
        pes = pes.annotate(
1✔
86
            past_date=Case(
87
                When(election__election_date__lt=date.today(), then=1),
88
                When(election__election_date__gte=date.today(), then=0),
89
                output_field=IntegerField(),
90
            )
91
        )
92
        # majority of ballots will have 0 so do this now to help reduce
93
        # unnecessary DB queries later on
94
        pes = pes.annotate(
1✔
95
            num_parish_councils=Count("parish_councils"),
96
        )
97
        pes = pes.select_related("post")
1✔
98
        pes = pes.select_related("election")
1✔
99
        pes = pes.select_related("election__voting_system")
1✔
100
        pes = pes.select_related("referendum")
1✔
101

102
        pes = pes.prefetch_related(
1✔
103
            Prefetch("husting_set", queryset=Husting.objects.published())
104
        )
105
        pes = pes.order_by(
1✔
106
            "past_date", "election__election_date", "-election__election_weight"
107
        )
108
        ret["ballots"] = pes
1✔
109
        return ret
1✔
110

111

112
class PostelectionsToPeopleMixin(object):
1✔
113
    def people_for_ballot(self, postelection, compact=False):
1✔
114
        key = PEOPLE_FOR_BALLOT_KEY_FMT.format(
1✔
115
            postelection.ballot_paper_id, compact
116
        )
117
        people_for_post = cache.get(key)
1✔
118
        if people_for_post:
1✔
UNCOV
119
            return people_for_post
×
120
        people_for_post = postelection.personpost_set.all()
1✔
121
        people_for_post = people_for_post.annotate(
1✔
122
            last_name=LastWord("person__name")
123
        )
124
        people_for_post = people_for_post.annotate(
1✔
125
            name_for_ordering=Coalesce("person__sort_name", "last_name")
126
        )
127
        if postelection.election.uses_lists:
1✔
UNCOV
128
            order_by = ["party__party_name", "list_position"]
×
129
        else:
130
            order_by = ["name_for_ordering", "person__name"]
1✔
131

132
        people_for_post = people_for_post.order_by(
1✔
133
            F("elected").desc(nulls_last=True),
134
            F("votes_cast").desc(nulls_last=True),
135
            *order_by,
136
        )
137

138
        people_for_post = people_for_post.select_related(
1✔
139
            "post",
140
            "election",
141
            "person",
142
            "party",
143
        )
144
        people_for_post = people_for_post.prefetch_related(
1✔
145
            "previous_party_affiliations"
146
        )
147
        people_for_post = people_for_post.prefetch_related(
1✔
148
            Prefetch(
149
                "person__leaflet_set",
150
                queryset=Leaflet.objects.order_by(
151
                    "date_uploaded_to_electionleaflets"
152
                ),
153
                to_attr="ordered_leaflets",
154
            )
155
        )
156
        if not compact:
1✔
157
            people_for_post = people_for_post.prefetch_related(
1✔
158
                "person__pledges"
159
            )
160
        cache.set(key, people_for_post)
1✔
161
        return people_for_post
1✔
162

163

164
class PollingStationInfoMixin(object):
1✔
165
    def show_polling_card(self, post_elections):
1✔
166
        return any(p.contested and not p.cancelled for p in post_elections)
1✔
167

168
    def get_advance_voting_station_info(self, polling_station: Optional[dict]):
1✔
169
        if not polling_station or not polling_station.get(
1✔
170
            "advance_voting_station"
171
        ):
172
            return None
1✔
UNCOV
173
        advance_voting_station = polling_station["advance_voting_station"]
×
174

UNCOV
175
        last_open_row = advance_voting_station["opening_times"][-1]
×
UNCOV
176
        last_date, last_open, last_close = last_open_row
×
UNCOV
177
        open_in_future = (
×
178
            datetime.combine(
179
                datetime.strptime(last_date, "%Y-%m-%d").date(),
180
                datetime.strptime(last_close, "%H:%M:%S").time(),
181
            )
182
            > datetime.now()
183
        )
UNCOV
184
        advance_voting_station["open_in_future"] = open_in_future
×
UNCOV
185
        return advance_voting_station
×
186

187
    def get_global_registration_card(self, post_elections):
1✔
188
        # City of London local elections have different
189
        # registration rules to every other election
190
        non_city_of_london_ballots = [
1✔
191
            ballot
192
            for ballot in post_elections
193
            if not ballot.election.is_city_of_london_local_election
194
            and not ballot.cancelled
195
        ]
196

197
        if not non_city_of_london_ballots:
1✔
198
            return False
1✔
199
        next_ballot = non_city_of_london_ballots[0]
1✔
200
        election = next_ballot.election
1✔
201
        country = next_ballot.post.territory
1✔
202

203
        if not country:
1✔
204
            country = Country.ENGLAND
1✔
205
        else:
206
            country = {
1✔
207
                "ENG": Country.ENGLAND,
208
                "SCT": Country.SCOTLAND,
209
                "WLS": Country.WALES,
210
                "NIR": Country.NORTHERN_IRELAND,
211
            }.get(country)
212
        election = from_election_id(election_id=election.slug, country=country)
1✔
213
        event = TimetableEvent.REGISTRATION_DEADLINE
1✔
214
        return {
1✔
215
            "show": election.is_before(event),
216
            "registration_deadline": next_ballot.registration_deadline,
217
            "election_date": next_ballot.election.election_date,
218
        }
219

220

221
class LogLookUpMixin(object):
1✔
222
    def log_postcode(self: View, postcode):
1✔
223
        entry = settings.POSTCODE_LOGGER.entry_class(
1✔
224
            postcode=postcode,
225
            dc_product=settings.POSTCODE_LOGGER.dc_product.wcivf,
226
            calls_devs_dc_api=True,
227
            **self.request.session.get("utm_data"),
228
        )
229
        settings.POSTCODE_LOGGER.log(entry)
1✔
230

231

232
class NewSlugsRedirectMixin(object):
1✔
233
    def get_changed_election_slug(self, slug):
1✔
234
        return UPDATED_SLUGS.get(slug, slug)
1✔
235

236
    def get(self, request, *args, **kwargs):
1✔
237
        given_slug = self.kwargs.get(self.pk_url_kwarg)
1✔
238
        updated_slug = self.get_changed_election_slug(given_slug)
1✔
239
        if updated_slug != given_slug:
1✔
UNCOV
240
            return HttpResponsePermanentRedirect(
×
241
                reverse("election_view", kwargs={"election": updated_slug})
242
            )
243

244
        return super().get(request, *args, **kwargs)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc