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

DemocracyClub / yournextrepresentative / 354f6bd3-acc2-44c9-8298-67e1291fc3d4

26 Mar 2026 02:08PM UTC coverage: 74.635% (+0.08%) from 74.553%
354f6bd3-acc2-44c9-8298-67e1291fc3d4

push

circleci

web-flow
Merge pull request #2681 from DemocracyClub/remove-existing-from-bulk-add-flow

Remove existing candidates from bulk add flow

873 of 1226 branches covered (71.21%)

Branch coverage included in aggregate %.

167 of 173 new or added lines in 6 files covered. (96.53%)

8 existing lines in 1 file now uncovered.

7875 of 10495 relevant lines covered (75.04%)

0.75 hits per line

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

90.3
/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 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

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

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

188

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

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

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

208

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

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

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

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

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

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

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