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

DemocracyClub / yournextrepresentative / b01e35fe-a3b8-4e22-a335-99b1ef3e2b75

25 Jul 2025 02:43PM UTC coverage: 67.202% (+4.0%) from 63.202%
b01e35fe-a3b8-4e22-a335-99b1ef3e2b75

push

circleci

mattpep
ci: Run hurl tests post-deploy

1604 of 2949 branches covered (54.39%)

Branch coverage included in aggregate %.

7600 of 10747 relevant lines covered (70.72%)

0.71 hits per line

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

91.38
/ynr/apps/bulk_adding/forms.py
1
from bulk_adding.fields import PersonIdentifierFieldSet
1✔
2
from django import forms
1✔
3
from django.core.exceptions import ValidationError
1✔
4
from django.db.models import Prefetch
1✔
5
from django.utils.safestring import SafeText, mark_safe
1✔
6
from elections.models import Election
1✔
7
from parties.forms import (
1✔
8
    PartyIdentifierField,
9
    PopulatePartiesMixin,
10
    PreviousPartyAffiliationsField,
11
)
12
from parties.models import Party, PartyDescription
1✔
13
from people.forms.fields import (
1✔
14
    BallotInputWidget,
15
    StrippedCharField,
16
    ValidBallotField,
17
)
18
from people.helpers import clean_biography, clean_birth_date
1✔
19
from popolo.models import Membership
1✔
20
from search.utils import search_person_by_name
1✔
21

22

23
class BaseBulkAddFormSet(forms.BaseFormSet):
1✔
24
    def __init__(self, *args, **kwargs):
1✔
25
        if "source" in kwargs:
1✔
26
            self.source = kwargs["source"]
1✔
27
            del kwargs["source"]
1✔
28

29
        if "ballot" in kwargs:
1✔
30
            self.ballot = kwargs["ballot"]
1✔
31
            del kwargs["ballot"]
1✔
32

33
        super().__init__(*args, **kwargs)
1✔
34

35
    def add_fields(self, form, index):
1✔
36
        super().add_fields(form, index)
1✔
37
        form.initial["ballot"] = self.ballot.ballot_paper_id
1✔
38

39
        if hasattr(self, "source"):
1✔
40
            form.fields["source"].initial = self.source
1✔
41
            form.fields["source"].widget = forms.HiddenInput()
1✔
42

43
    def clean(self):
1✔
44
        if (
1✔
45
            not self.initial_form_count()
46
            and self.ballot.membership_set.exists()
47
        ):
48
            # No extra forms exist, meaning no new people were added
49
            return super().clean()
1✔
50
        if (
1✔
51
            hasattr(self, "cleaned_data")
52
            and not any(self.cleaned_data)
53
            and not self.ballot.membership_set.exists()
54
        ):
55
            raise ValidationError("At least one person required on this ballot")
1✔
56

57
        return super().clean()
1✔
58

59

60
class BulkAddFormSet(BaseBulkAddFormSet):
1✔
61
    def __init__(self, *args, **kwargs):
1✔
62
        super().__init__(*args, **kwargs)
1✔
63
        self.parties = Party.objects.register(
1✔
64
            self.ballot.post.party_set.slug.upper()
65
        ).default_party_choices(extra_party_ids=self.initial_party_ids)
66
        self.previous_party_affiliations_choices = (
1✔
67
            self.get_previous_party_affiliations_choices()
68
        )
69

70
    def get_form_kwargs(self, index):
1✔
71
        kwargs = super().get_form_kwargs(index)
1✔
72
        kwargs["party_choices"] = self.parties
1✔
73
        kwargs[
1✔
74
            "previous_party_affiliations_choices"
75
        ] = self.previous_party_affiliations_choices
76
        return kwargs
1✔
77

78
    @property
1✔
79
    def initial_party_ids(self):
1✔
80
        """
81
        Returns a list of any party ID's that are included in initial data
82
        """
83
        if self.initial is None:
1✔
84
            return []
1✔
85
        return [d.get("party")[0].split("__")[0] for d in self.initial]
1✔
86

87
    def get_previous_party_affiliations_choices(self):
1✔
88
        """
89
        Return choices for previous_party_affilations field. By getting these on
90
        the formset and passing to the form, it saves the query for every
91
        individual form
92
        """
93
        if not self.ballot.is_welsh_run:
1✔
94
            return []
1✔
95

96
        parties = Party.objects.register("GB").active_in_last_year(
1✔
97
            date=self.ballot.election.election_date
98
        )
99
        return parties.values_list("ec_id", "name")
1✔
100

101

102
class BaseBulkAddReviewFormSet(BaseBulkAddFormSet):
1✔
103
    def suggested_people(self, person_name):
1✔
104
        if person_name:
1!
105
            qs = search_person_by_name(
1✔
106
                person_name, synonym=True
107
            ).prefetch_related(
108
                Prefetch(
109
                    "memberships",
110
                    queryset=Membership.objects.select_related(
111
                        "party",
112
                        "ballot",
113
                        "ballot__election",
114
                        "ballot__election__organization",
115
                    ),
116
                ),
117
            )
118
            return qs[:5]
1✔
119
        return None
×
120

121
    def format_value(
1✔
122
        self,
123
        suggestion,
124
        new_party=None,
125
        new_election: Election = None,
126
        new_name=None,
127
    ):
128
        """
129
        Turn the whole form in to a value string
130
        """
131
        name = suggestion.name
1✔
132
        if name == new_name:
1✔
133
            name = mark_safe(f"<strong>{name}</strong>")
1✔
134
        suggestion_dict = {"name": name, "object": suggestion}
1✔
135

136
        candidacies = (
1✔
137
            suggestion.memberships.select_related(
138
                "ballot__post", "ballot__election", "party"
139
            )
140
            .select_related("ballot__sopn")
141
            .order_by("-ballot__election__election_date")[:3]
142
        )
143

144
        if candidacies:
1!
145
            suggestion_dict["previous_candidacies"] = []
1✔
146

147
        for candidacy in candidacies:
1✔
148
            party = candidacy.party
1✔
149
            party_str = f"{party.name}"
1✔
150
            if new_party == party.ec_id:
1!
151
                party_str = f"<strong>{party.name}</strong>"
×
152

153
            election = candidacy.ballot.election
1✔
154
            election_str = f"{election.name}"
1✔
155
            if new_election.organization == election.organization:
1✔
156
                election_str = f"<strong>{election.name}</strong>"
1✔
157

158
            text = """{election}: {post} – {party}""".format(
1✔
159
                post=candidacy.ballot.post.short_label,
160
                election=election_str,
161
                party=party_str,
162
            )
163
            sopn = candidacy.ballot.officialdocument_set.first()
1✔
164
            if sopn:
1!
165
                text += ' (<a href="{0}">SOPN</a>)'.format(
×
166
                    sopn.get_absolute_url()
167
                )
168
            suggestion_dict["previous_candidacies"].append(SafeText(text))
1✔
169

170
        return [suggestion.pk, suggestion_dict]
1✔
171

172
    def add_fields(self, form, index):
1✔
173
        super().add_fields(form, index)
1✔
174
        if not form["name"].value():
1✔
175
            return
1✔
176
        suggestions = self.suggested_people(form["name"].value())
1✔
177

178
        CHOICES = [("_new", "Add new person")]
1✔
179
        if suggestions:
1✔
180
            CHOICES += [
1✔
181
                self.format_value(
182
                    suggestion,
183
                    new_party=form.initial.get("party"),
184
                    new_election=self.ballot.election,
185
                    new_name=form.initial.get("name"),
186
                )
187
                for suggestion in suggestions
188
            ]
189
        form.fields["select_person"] = forms.ChoiceField(
1✔
190
            choices=CHOICES, widget=forms.RadioSelect()
191
        )
192
        form.fields["select_person"].initial = "_new"
1✔
193

194
        form.fields["party"] = forms.CharField(
1✔
195
            widget=forms.HiddenInput(
196
                attrs={"readonly": "readonly", "class": "party-select"}
197
            ),
198
            required=False,
199
        )
200

201
    def clean(self):
1✔
202
        errors = []
1✔
203
        if not hasattr(self, "ballot"):
1!
204
            return super().clean()
×
205

206
        if self.ballot.candidates_locked:
1✔
207
            raise ValidationError(
1✔
208
                "Candidates have already been locked for this ballot"
209
            )
210

211
        for form_data in self.cleaned_data:
1✔
212
            if (
1✔
213
                "select_person" in form_data
214
                and form_data["select_person"] == "_new"
215
            ):
216
                continue
1✔
217
            qs = (
1✔
218
                Membership.objects.filter(ballot__election=self.ballot.election)
219
                .filter(person_id=form_data.get("select_person"))
220
                .exclude(ballot=self.ballot)
221
                .exclude(ballot__candidates_locked=True)
222
            )
223
            if qs.exists():
1✔
224
                errors.append(
1✔
225
                    forms.ValidationError(
226
                        "'{}' is marked as standing in another ballot for this "
227
                        "election. Check you're entering the correct "
228
                        "information for {}".format(
229
                            form_data["name"], self.ballot.post.label
230
                        )
231
                    )
232
                )
233
        if errors:
1✔
234
            raise forms.ValidationError(errors)
1✔
235
        return super().clean()
1✔
236

237

238
class NameOnlyPersonForm(forms.Form):
1✔
239
    name = StrippedCharField(
1✔
240
        label="Name (style: Ali McKay Smith not SMITH Ali McKay)",
241
        required=True,
242
        widget=forms.TextInput(
243
            attrs={"class": "person_name", "spellcheck": "false"}
244
        ),
245
    )
246
    ballot = ValidBallotField(
1✔
247
        widget=BallotInputWidget(attrs={"type": "hidden"})
248
    )
249

250

251
class BulkAddByPartyForm(NameOnlyPersonForm):
1✔
252
    biography = StrippedCharField(
1✔
253
        label="Statement to Voters",
254
        required=False,
255
        widget=forms.Textarea(
256
            attrs={"class": "person_biography"},
257
        ),
258
    )
259
    gender = StrippedCharField(
1✔
260
        label="Gender (e.g. “male”, “female”)", required=False
261
    )
262
    birth_date = forms.CharField(
1✔
263
        label="Year of birth (a four digit year)",
264
        required=False,
265
        widget=forms.NumberInput,
266
    )
267
    person_identifiers = PersonIdentifierFieldSet(
1✔
268
        label="Links and social media",
269
        required=False,
270
    )
271

272
    def clean_biography(self):
1✔
273
        bio = self.cleaned_data["biography"]
1✔
274
        return clean_biography(bio)
1✔
275

276
    def clean_birth_date(self):
1✔
277
        bd = self.cleaned_data["birth_date"]
1✔
278
        return clean_birth_date(bd)
1✔
279

280

281
class QuickAddSinglePersonForm(PopulatePartiesMixin, NameOnlyPersonForm):
1✔
282
    source = forms.CharField(required=True)
1✔
283
    party = PartyIdentifierField()
1✔
284
    previous_party_affiliations = PreviousPartyAffiliationsField()
1✔
285

286
    def __init__(self, **kwargs):
1✔
287
        previous_party_affiliations_choices = kwargs.pop(
1✔
288
            "previous_party_affiliations_choices", []
289
        )
290
        super().__init__(**kwargs)
1✔
291
        self.fields[
1✔
292
            "previous_party_affiliations"
293
        ].choices = previous_party_affiliations_choices
294

295
    def has_changed(self, *args, **kwargs):
1✔
296
        if "name" not in self.changed_data:
1✔
297
            return False
1✔
298
        return super().has_changed(*args, **kwargs)
1✔
299

300
    def clean(self):
1✔
301
        if (
1!
302
            not self.cleaned_data["ballot"].is_welsh_run
303
            and self.cleaned_data["previous_party_affiliations"]
304
        ):
305
            raise ValidationError(
×
306
                "Previous party affiliations are invalid for this ballot"
307
            )
308
        return super().clean()
1✔
309

310

311
class ReviewSinglePersonNameOnlyForm(forms.Form):
1✔
312
    def __init__(self, *args, **kwargs):
1✔
313
        kwargs.pop("party_choices", None)
1✔
314
        super().__init__(*args, **kwargs)
1✔
315

316
    name = StrippedCharField(
1✔
317
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
318
    )
319

320

321
class ReviewBulkAddByPartyForm(ReviewSinglePersonNameOnlyForm):
1✔
322
    biography = StrippedCharField(
1✔
323
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
324
    )
325
    gender = StrippedCharField(
1✔
326
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
327
    )
328
    birth_date = forms.CharField(
1✔
329
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
330
    )
331
    person_identifiers = forms.CharField(
1✔
332
        required=False,
333
        widget=forms.HiddenInput(
334
            attrs={"readonly": "readonly"},
335
        ),
336
    )
337

338

339
class ReviewSinglePersonForm(ReviewSinglePersonNameOnlyForm):
1✔
340
    source = forms.CharField(
1✔
341
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
342
    )
343
    party_description = forms.ModelChoiceField(
1✔
344
        required=False,
345
        widget=forms.HiddenInput(),
346
        queryset=PartyDescription.objects.all(),
347
    )
348
    party_description_text = forms.CharField(
1✔
349
        required=False, widget=forms.HiddenInput()
350
    )
351
    previous_party_affiliations = forms.CharField(
1✔
352
        required=False, widget=forms.HiddenInput()
353
    )
354

355

356
BulkAddFormSetFactory = forms.formset_factory(
1✔
357
    QuickAddSinglePersonForm, extra=15, formset=BulkAddFormSet, can_delete=True
358
)
359

360

361
BulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
362
    ReviewSinglePersonNameOnlyForm, extra=0, formset=BaseBulkAddReviewFormSet
363
)
364

365
BulkAddReviewFormSet = forms.formset_factory(
1✔
366
    ReviewSinglePersonForm, extra=0, formset=BaseBulkAddReviewFormSet
367
)
368

369

370
class BaseBulkAddByPartyFormset(forms.BaseFormSet):
1✔
371
    def __init__(self, *args, **kwargs):
1✔
372
        self.ballot = kwargs["ballot"]
1✔
373
        kwargs["prefix"] = self.ballot.pk
1✔
374
        self.extra = self.ballot.winner_count
1✔
375
        del kwargs["ballot"]
1✔
376

377
        super().__init__(*args, **kwargs)
1✔
378

379
    def add_fields(self, form, index):
1✔
380
        super().add_fields(form, index)
1✔
381
        if self.ballot.election.party_lists_in_use:
1!
382
            form.fields["party_list_position"] = forms.IntegerField(
×
383
                label="Position in party list ('1' for first, '2' for second, etc.)",
384
                min_value=1,
385
                required=False,
386
                initial=index + 1,
387
                widget=forms.NumberInput(attrs={"class": "party-position"}),
388
            )
389

390
        if self.ballot.candidates_locked:
1!
391
            for name, field in form.fields.items():
×
392
                field.disabled = True
×
393
                form.fields[name] = field
×
394

395

396
BulkAddByPartyFormset = forms.formset_factory(
1✔
397
    BulkAddByPartyForm,
398
    extra=6,
399
    formset=BaseBulkAddByPartyFormset,
400
    can_delete=False,
401
)
402

403

404
class PartyBulkAddReviewFormSet(BaseBulkAddReviewFormSet):
1✔
405
    def __init__(self, *args, **kwargs):
1✔
406
        self.ballot = kwargs["ballot"]
1✔
407
        kwargs["prefix"] = self.ballot.pk
1✔
408
        self.extra = self.ballot.winner_count
1✔
409
        del kwargs["ballot"]
1✔
410

411
        super().__init__(*args, **kwargs)
1✔
412

413
    def clean(self):
1✔
414
        """
415
        Use this method to prevent users from adding candidates to ballots
416
        that can't have candidates added to them, like locked or cancelled
417
        ballots
418
        """
419
        if self.ballot.candidates_locked:
1!
420
            raise ValidationError("Cannot add candidates to a locked ballot")
×
421
        if self.ballot.cancelled:
1!
422
            raise ValidationError(
×
423
                "Cannot add candidates to a cancelled election"
424
            )
425

426
    def add_fields(self, form, index):
1✔
427
        super().add_fields(form, index)
1✔
428
        if self.ballot.election.party_lists_in_use:
1!
429
            form.fields["party_list_position"] = forms.IntegerField(
×
430
                min_value=1, required=False, widget=forms.HiddenInput()
431
            )
432

433

434
PartyBulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
435
    ReviewBulkAddByPartyForm, extra=0, formset=PartyBulkAddReviewFormSet
436
)
437

438

439
class SelectPartyForm(forms.Form):
1✔
440
    def __init__(self, *args, **kwargs):
1✔
441
        election = kwargs.pop("election")
1✔
442
        super().__init__(*args, **kwargs)
1✔
443

444
        self.election = election
1✔
445
        party_set_qs = (
1✔
446
            election.ballot_set.all()
447
            .order_by("post__party_set")
448
            .values_list("post__party_set__slug", flat=True)
449
        )
450

451
        registers = {p.upper() for p in party_set_qs}
1✔
452
        for register in registers:
1✔
453
            choices = Party.objects.register(register).party_choices(
1✔
454
                include_description_ids=True
455
            )
456
            field = PartyIdentifierField(
1✔
457
                label=f"{register} parties", choices=choices
458
            )
459

460
            field.fields[0].choices = choices
1✔
461
            field.widget.attrs["data-party-register"] = register
1✔
462
            field.widget.attrs["register"] = register
1✔
463
            self.fields[f"party_{register}"] = field
1✔
464

465
    def clean(self):
1✔
466
        form_data = self.cleaned_data
1✔
467
        if len([v for v in form_data.values() if v]) != 1:
1✔
468
            self.cleaned_data = {}
1✔
469
            raise forms.ValidationError("Select one and only one party")
1✔
470

471
        form_data["party"] = [v for v in form_data.values() if v][0]
1✔
472

473

474
class AddByPartyForm(forms.Form):
1✔
475
    source = forms.CharField(required=True)
1✔
476

477

478
class DeleteRawPeopleForm(forms.Form):
1✔
479
    ballot_paper_id = forms.CharField(required=True, widget=forms.HiddenInput)
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

© 2026 Coveralls, Inc