• 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

73.64
/wcivf/apps/api/views.py
1
import abc
1✔
2

3
from api import serializers
1✔
4
from api.serializers import VotingSystemSerializer
1✔
5
from core.helpers import clean_postcode
1✔
6
from django.conf import settings
1✔
7
from django.utils.http import urlencode
1✔
8
from elections.models import InvalidPostcodeError, PostElection
1✔
9
from elections.views import mixins
1✔
10
from hustings.api.serializers import HustingSerializer
1✔
11
from people.models import Person
1✔
12
from rest_framework import viewsets
1✔
13
from rest_framework.exceptions import APIException
1✔
14
from rest_framework.response import Response
1✔
15
from rest_framework.views import APIView
1✔
16

17

18
class PostcodeNotProvided(APIException):
1✔
19
    status_code = 400
1✔
20
    default_detail = "postcode is a required GET parameter"
1✔
21
    default_code = "postcode_required"
1✔
22

23

24
class InvalidPostcode(APIException):
1✔
25
    status_code = 400
1✔
26
    default_detail = "Could not find postcode"
1✔
27
    default_code = "postcode_invalid"
1✔
28

29

30
class BallotIdsNotProvided(APIException):
1✔
31
    status_code = 400
1✔
32
    default_detail = "ballot_ids is a required GET parameter"
1✔
33
    default_code = "ballot_ids_required"
1✔
34

35

36
class PersonViewSet(viewsets.ModelViewSet):
1✔
37
    http_method_names = ["get", "head"]
1✔
38
    queryset = Person.objects.all()
1✔
39
    serializer_class = serializers.PersonSerializer
1✔
40

41

42
class BaseCandidatesAndElectionsViewSet(
1✔
43
    viewsets.ViewSet, mixins.PostelectionsToPeopleMixin, metaclass=abc.ABCMeta
44
):
45
    http_method_names = ["get", "head"]
1✔
46

47
    @abc.abstractmethod
1✔
48
    def get_ballots(self, request):
1✔
UNCOV
49
        pass
×
50

51
    def add_hustings(self, postelection: PostElection):
1✔
52
        hustings = None
1✔
53
        hustings_qs = postelection.husting_set.published()
1✔
54
        if hustings_qs:
1✔
UNCOV
55
            hustings = HustingSerializer(
×
56
                hustings_qs, many=True, read_only=True
57
            ).data
58
        return hustings
1✔
59

60
    def list(self, request, *args, **kwargs):
1✔
61
        results = []
1✔
62

63
        ballots = self.get_ballots(request)
1✔
64
        postelections = ballots["ballots"].select_related("voting_system")
1✔
65

66
        for postelection in postelections:
1✔
67
            candidates = []
1✔
68
            personposts = self.people_for_ballot(postelection, compact=True)
1✔
69
            for personpost in personposts:
1✔
70
                candidates.append(
1✔
71
                    serializers.PersonPostSerializer(
72
                        personpost,
73
                        context={
74
                            "request": request,
75
                            "postelection": postelection,
76
                        },
77
                    ).data
78
                )
79

80
            election = {
1✔
81
                "ballot_paper_id": postelection.ballot_paper_id,
82
                "absolute_url": self.request.build_absolute_uri(
83
                    postelection.get_absolute_url()
84
                ),
85
                "election_date": postelection.election.election_date,
86
                "election_name": postelection.election.nice_election_name,
87
                "election_id": postelection.election.slug,
88
                "post": {
89
                    "post_name": postelection.post.label,
90
                    "post_slug": postelection.post.ynr_id,
91
                },
92
                "cancelled": postelection.cancelled,
93
                "ballot_locked": postelection.locked,
94
                "replaced_by": postelection.replaced_by,
95
                "candidates": candidates,
96
                "voting_system": VotingSystemSerializer(
97
                    postelection.voting_system
98
                ).data,
99
                "requires_voter_id": postelection.requires_voter_id,
100
                "postal_voting_requirements": postelection.get_postal_voting_requirements,
101
                "seats_contested": postelection.winner_count,
102
                "organisation_type": postelection.post.organization_type,
103
                "hustings": self.add_hustings(postelection),
104
                "last_updated": getattr(
105
                    postelection, "last_updated", postelection.modified
106
                ),
107
            }
108
            if postelection.replaced_by:
1✔
UNCOV
109
                election[
×
110
                    "replaced_by"
111
                ] = postelection.replaced_by.ballot_paper_id
112
            else:
113
                election["replaced_by"] = None
1✔
114

115
            results.append(election)
1✔
116
        return Response(results)
1✔
117

118

119
class CandidatesAndElectionsForPostcodeViewSet(
1✔
120
    BaseCandidatesAndElectionsViewSet, mixins.PostcodeToPostsMixin
121
):
122
    def get_ballots(self, request):
1✔
123
        postcode = request.GET.get("postcode", None)
1✔
124
        if not postcode:
1✔
125
            raise PostcodeNotProvided()
1✔
126
        postcode = clean_postcode(postcode)
1✔
127
        try:
1✔
128
            return self.postcode_to_ballots(postcode, compact=True)
1✔
UNCOV
129
        except InvalidPostcodeError:
×
130
            raise InvalidPostcode()
×
131

132

133
class CandidatesAndElectionsForBallots(BaseCandidatesAndElectionsViewSet):
1✔
134
    def get_ballots(self, request):
1✔
135
        ballot_ids_str = request.GET.get("ballot_ids", None)
1✔
136
        modified_gt = request.GET.get("modified_gt", None)
1✔
137
        current = request.GET.get("current", None)
1✔
138
        if not any((ballot_ids_str, modified_gt)):
1✔
UNCOV
139
            raise BallotIdsNotProvided
×
140

141
        pes = PostElection.objects.all().select_related(
1✔
142
            "post",
143
            "election",
144
            "election__voting_system",
145
        )
146
        pes = pes.prefetch_related("husting_set")
1✔
147
        if ballot_ids_str:
1✔
148
            if "," in ballot_ids_str:
1✔
UNCOV
149
                ballot_ids_lst = ballot_ids_str.split(",")
×
150
                ballot_ids_lst = [b.strip() for b in ballot_ids_lst]
×
151
            else:
152
                ballot_ids_lst = [ballot_ids_str]
1✔
153

154
            pes = pes.filter(ballot_paper_id__in=ballot_ids_lst)
1✔
155
        if current:
1✔
UNCOV
156
            pes = pes.filter(election__current=True)
×
157
        ordering = ["election__election_date", "election__election_weight"]
1✔
158

159
        if modified_gt:
1✔
160
            pes = pes.modified_gt_with_related(date=modified_gt)
1✔
161
        else:
162
            pes = pes.order_by(*ordering)
1✔
163
        return {"ballots": pes[:100]}
1✔
164

165

166
class LastUpdatedView(APIView):
1✔
167
    def get(self, request):
1✔
168
        """
169
        Returns the current timestamps used by the people and ballot recently
170
        updated importers
171
        """
UNCOV
172
        data = {
×
173
            "ballot_timestamp": None,
174
            "person_timestamp": None,
175
            "ballot_last_updated_url": None,
176
            "person_last_updated_url": None,
177
        }
UNCOV
178
        api_base_url = f"{settings.YNR_BASE}/api/next/"
×
179
        try:
×
180
            ts = PostElection.objects.last_updated_in_ynr().ynr_modified
×
181
            data["ballot_timestamp"] = ts.isoformat()
×
182
            params = {"last_updated": ts.isoformat()}
×
183
            if settings.YNR_API_KEY:
×
184
                params.update({"auth_token": settings.YNR_API_KEY})
×
185
            qs = urlencode(params)
×
186
            data["ballot_last_updated_url"] = f"{api_base_url}ballots/?{qs}"
×
187
        except PostElection.DoesNotExist:
×
188
            pass
×
189

UNCOV
190
        try:
×
191
            ts = Person.objects.latest().last_updated.isoformat()
×
192
            data["person_timestamp"] = ts
×
193
            qs = urlencode({"last_updated": ts})
×
194
            data["person_last_updated_url"] = f"{api_base_url}people/?{qs}"
×
195
        except Person.DoesNotExist:
×
196
            pass
×
197

UNCOV
198
        return Response(data=data)
×
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