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

DemocracyClub / yournextrepresentative / d0421b27-f043-41ee-a5ee-c3a83bf3c576

14 Nov 2025 11:13AM UTC coverage: 70.14% (+0.04%) from 70.101%
d0421b27-f043-41ee-a5ee-c3a83bf3c576

Pull #2613

circleci

chris48s
fix last bulk add test

I've fixed this from 2 angles
1. Election.organization is nullable,
   so the code should handle that case
2. The test is simulating an general election,
   so the election and ballot should all be
   assigned to House of Commons as the org
Pull Request #2613: WIP django 5

820 of 1282 branches covered (63.96%)

Branch coverage included in aggregate %.

76 of 81 new or added lines in 13 files covered. (93.83%)

2 existing lines in 1 file now uncovered.

7648 of 10791 relevant lines covered (70.87%)

0.71 hits per line

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

89.31
/ynr/apps/bulk_adding/fields.py
1
import json
1✔
2

3
from django import forms
1✔
4
from django.core.exceptions import ValidationError
1✔
5
from django.core.validators import URLValidator, validate_email
1✔
6
from django.template.loader import render_to_string
1✔
7
from django.utils.safestring import SafeText, mark_safe
1✔
8
from people.helpers import (
1✔
9
    clean_instagram_url,
10
    clean_linkedin_url,
11
    clean_mastodon_username,
12
    clean_twitter_username,
13
    clean_wikidata_id,
14
)
15
from people.models import PersonIdentifier
1✔
16

17
HTTP_IDENTIFIERS = [
1✔
18
    "homepage_url",
19
    "facebook_personal_url",
20
    "party_ppc_page_url",
21
    "linkedin_url",
22
    "facebook_page_url",
23
    "wikipedia_url",
24
    "blue_sky_url",
25
    "threads_url",
26
    "other_url",
27
    "tiktok_url",
28
]
29

30

31
def clean_email(email):
1✔
32
    validate_email(email)
1✔
33
    if email.lower().endswith("parliament.uk"):
1✔
34
        raise ValueError(
×
35
            "MPs are locked out of their email addresses during a general election. Please find a different email address."
36
        )
37
    return email
1✔
38

39

40
class PersonIdentifierWidget(forms.MultiWidget):
1✔
41
    template_name = "bulk_add/widgets/person_identifier_multiwidget.html"
1✔
42

43
    def __init__(self, *args, **kwargs):
1✔
44
        super().__init__(
1✔
45
            widgets=[
46
                forms.TextInput(),
47
                forms.Select(
48
                    choices=[("", "Select an option")]
49
                    + PersonIdentifier.objects.select_choices()[1:],
50
                ),
51
            ],
52
            **kwargs,
53
        )
54

55
    def decompress(self, value):
1✔
56
        if value:
1✔
57
            pid_type, pid = next(iter(value.items()))
×
58
            return [pid, pid_type]
×
59
        return [None, None]
1✔
60

61

62
class PersonIdentifierWidgetSet(forms.MultiWidget):
1✔
63
    def __init__(self, *args, **kwargs):
1✔
64
        super().__init__(
1✔
65
            widgets=[
66
                PersonIdentifierWidget(),
67
                PersonIdentifierWidget(),
68
                PersonIdentifierWidget(),
69
            ],
70
            **kwargs,
71
        )
72

73
    def decompress(self, value):
1✔
74
        if value:
1✔
75
            return [{k: v} for k, v in value.items()]
×
76
        return [None, None, None]
1✔
77

78

79
class PersonIdentifierField(forms.MultiValueField):
1✔
80
    def __init__(self, **kwargs):
1✔
81
        fields = (
1✔
82
            forms.CharField(
83
                required=True,
84
                error_messages={
85
                    "incomplete": "Please enter a social media link",
86
                },
87
            ),
88
            forms.ChoiceField(
89
                required=True,
90
                error_messages={
91
                    "incomplete": "Please select a link type",
92
                },
93
                choices=[("", "Select an option")]
94
                + PersonIdentifier.objects.select_choices()[1:],
95
            ),
96
        )
97
        widget = PersonIdentifierWidget()
1✔
98

99
        super().__init__(
1✔
100
            fields=fields,
101
            require_all_fields=False,
102
            widget=widget,
103
            **kwargs,
104
        )
105

106
    def compress(self, data_list):
1✔
107
        if not data_list:
1✔
108
            return None
1✔
109
        pid = data_list[0]
1✔
110
        pid_type = data_list[1]
1✔
111
        # Validate URLs:
112
        if pid_type in HTTP_IDENTIFIERS:
1✔
113
            # Add https schema if missing
114
            if not pid.startswith("http"):
1✔
115
                pid = f"https://{pid}"
×
116
            URLValidator()(pid)
1✔
117

118
        # Validate pid:
119
        try:
1✔
120
            if pid_type == "instagram_url":
1✔
121
                clean_instagram_url(pid)
×
122
            elif pid_type == "linkedin_url":
1✔
123
                clean_linkedin_url(pid)
1✔
124
            elif pid_type == "mastodon_url":
1✔
125
                clean_mastodon_username(pid)
×
126
            elif pid_type == "twitter_url":
1✔
127
                clean_twitter_username(pid)
×
128
            elif pid_type == "wikidata_id":
1✔
129
                clean_wikidata_id(pid)
×
130
            elif pid_type == "email":
1✔
131
                clean_email(pid)
1✔
132
        except ValueError as e:
×
133
            raise ValidationError(e)
×
134

135
        return {pid_type: pid}
1✔
136

137

138
class PersonIdentifierFieldSet(forms.MultiValueField):
1✔
139
    def __init__(self, **kwargs):
1✔
140
        fields = (
1✔
141
            PersonIdentifierField(
142
                required=False,
143
            ),
144
            PersonIdentifierField(
145
                required=False,
146
            ),
147
            PersonIdentifierField(
148
                required=False,
149
            ),
150
        )
151
        widget = PersonIdentifierWidgetSet()
1✔
152

153
        super().__init__(
1✔
154
            fields=fields,
155
            require_all_fields=False,
156
            widget=widget,
157
            **kwargs,
158
        )
159

160
    def compress(self, data_list):
1✔
161
        person_identifiers = {}
1✔
162
        for pi in data_list:
1✔
163
            if pi:
1✔
164
                person_identifiers.update(pi)
1✔
165
        return json.dumps(person_identifiers)
1✔
166

167

168
class PersonSuggestionRadioSelect(forms.RadioSelect):
1✔
169
    def create_option(
1✔
170
        self, name, value, label, selected, index, subindex=None, attrs=None
171
    ):
172
        option = super().create_option(
1✔
173
            name, value, label, selected, index, subindex=subindex, attrs=attrs
174
        )
175
        if value == "_new":
1✔
176
            return option
1✔
177
        option["instance"] = value.instance
1✔
178
        option["other_names"] = value.instance.other_names.all()
1✔
179
        option["previous_candidacies"] = self.get_previous_candidacies(
1✔
180
            value.instance
181
        )
182
        return option
1✔
183

184
    def get_previous_candidacies(self, person):
1✔
185
        previous = []
1✔
186
        candidacies = (
1✔
187
            person.memberships.select_related(
188
                "ballot__post", "ballot__election", "party"
189
            )
190
            .select_related("ballot__sopn")
191
            .order_by("-ballot__election__election_date")[:3]
192
        )
193

194
        for candidacy in candidacies:
1✔
195
            party = candidacy.party
1✔
196
            party_str = f"{party.name}"
1✔
197
            if person.new_party == party.ec_id:
1✔
NEW
198
                party_str = f"<strong>{party.name}</strong>"
×
199

200
            election = candidacy.ballot.election
1✔
201
            election_str = f"{election.name}"
1✔
202
            if person.new_organisation == election.organization.pk:
1✔
203
                election_str = f"<strong>{election.name}</strong>"
1✔
204

205
            text = """{election}: {post} – {party}""".format(
1✔
206
                post=candidacy.ballot.post.short_label,
207
                election=election_str,
208
                party=party_str,
209
            )
210
            sopn = candidacy.ballot.officialdocument_set.first()
1✔
211
            if sopn:
1✔
NEW
212
                text += ' (<a href="{0}">SOPN</a>)'.format(
×
213
                    sopn.get_absolute_url()
214
                )
215
            previous.append(SafeText(text))
1✔
216
        return previous
1✔
217

218

219
class PersonSuggestionModelChoiceField(forms.ModelChoiceField):
1✔
220
    """
221
    For use on the review page to show each suggested person as a radio field.
222
    """
223

224
    def label_from_instance(self, obj):
1✔
225
        # Render using a template fragment per object
226
        html = render_to_string(
1✔
227
            "bulk_add/widgets/person_suggestion_choice_label.html",
228
            {"object": obj},
229
        )
230
        return mark_safe(html)
1✔
231

232
    def to_python(self, value):
1✔
233
        if value == "_new":
1✔
234
            return value
1✔
235
        for model in self.queryset:
1✔
236
            if str(model.pk) == value:
1✔
237
                return model.pk
1✔
NEW
238
        return super().to_python(value)
×
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

© 2026 Coveralls, Inc