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

DemocracyClub / yournextrepresentative / 331362b5-3b39-45b1-a177-b4ab59a18cd4

16 Mar 2026 07:54PM UTC coverage: 74.474% (-0.04%) from 74.511%
331362b5-3b39-45b1-a177-b4ab59a18cd4

Pull #2681

circleci

symroe
WIP confirmation page
Pull Request #2681: Remove existing candidates from bulk add flow

864 of 1220 branches covered (70.82%)

Branch coverage included in aggregate %.

96 of 100 new or added lines in 5 files covered. (96.0%)

22 existing lines in 2 files now uncovered.

7880 of 10521 relevant lines covered (74.9%)

0.75 hits per line

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

90.37
/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.forms.models import ModelChoiceIteratorValue
1✔
7
from django.template.loader import render_to_string
1✔
8
from django.utils.safestring import mark_safe
1✔
9
from people.helpers import (
1✔
10
    clean_instagram_url,
11
    clean_linkedin_url,
12
    clean_mastodon_username,
13
    clean_twitter_username,
14
    clean_wikidata_id,
15
)
16
from people.models import PersonIdentifier
1✔
17

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

31

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

40

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

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

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

62

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

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

79

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

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

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

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

136
        return {pid_type: pid}
1✔
137

138

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

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

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

168

169
class PersonSuggestionRadioSelect(forms.RadioSelect):
1✔
170
    def create_option(
1✔
171
        self, name, value, label, selected, index, subindex=None, attrs=None
172
    ):
173
        option = super().create_option(
1✔
174
            name, value, label, selected, index, subindex=subindex, attrs=attrs
175
        )
176
        if value == "_new":
1✔
177
            return option
1✔
178

179
        option["instance"] = value.instance
1✔
180
        option["other_names"] = value.instance.other_names.all()
1✔
181
        option["previous_candidacies"] = self.get_previous_candidacies(
1✔
182
            value.instance
183
        )
184
        return option
1✔
185

186
    def get_previous_candidacies(self, person):
1✔
187
        return person.memberships.all()[:3]
1✔
188

189

190
class PersonSuggestionChoiceIterator:
1✔
191
    def __init__(self, field):
1✔
192
        self.field = field
1✔
193

194
    def __iter__(self):
1✔
195
        field = self.field
1✔
196
        yield (
1✔
197
            "_new",
198
            mark_safe(f'Add a new profile "{field.new_name}"'),
199
        )
200

201
        for person in field.suggestions:
1✔
202
            value = ModelChoiceIteratorValue(person.pk, person)
1✔
203
            label = render_to_string(
1✔
204
                "bulk_add/widgets/person_suggestion_choice_label.html",
205
                {"object": person},
206
            )
207
            yield (value, mark_safe(label))
1✔
208

209

210
class PersonSuggestionField(forms.ChoiceField):
1✔
211
    """
212
    For use on the review page to show each suggested person as a radio field.
213
    """
214

215
    widget = None  # set when instantiating if you want
1✔
216

217
    def __init__(self, *args, suggestions, new_name=None, **kwargs):
1✔
218
        self.suggestions = list(suggestions)
1✔
219
        self.new_name = new_name
1✔
220
        self._person_map = {
1✔
221
            str(person.pk): person for person in self.suggestions
222
        }
223

224
        kwargs["choices"] = PersonSuggestionChoiceIterator(self)
1✔
225
        super().__init__(*args, **kwargs)
1✔
226

227
        self.initial = "_new"
1✔
228
        if self.suggestions:
1✔
229
            top = self.suggestions[0]
1✔
230
            if getattr(top, "on_ballot", False) and getattr(
1✔
231
                top, "same_party", False
232
            ):
233
                self.initial = str(top.pk)
1✔
234

235
    def valid_value(self, value):
1✔
236
        return value == "_new" or str(value) in self._person_map
1✔
237

238
    def clean(self, value):
1✔
239
        value = super().clean(value)
1✔
240
        if value == "_new":
1✔
241
            return value
1✔
242
        try:
1✔
243
            return self._person_map[str(value)].pk
1✔
NEW
244
        except KeyError:
×
NEW
245
            raise forms.ValidationError("Select a valid choice.")
×
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