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

DemocracyClub / yournextrepresentative / ffd7f197-e0dd-46d6-bc54-72369ada5785

18 Feb 2026 02:26AM UTC coverage: 70.844% (+0.003%) from 70.841%
ffd7f197-e0dd-46d6-bc54-72369ada5785

push

circleci

mattpep
Add filtered event notification

863 of 1294 branches covered (66.69%)

Branch coverage included in aggregate %.

7780 of 10906 relevant lines covered (71.34%)

0.71 hits per line

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

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

25

26
class BaseBulkAddFormSet(forms.BaseFormSet):
1✔
27
    renderer = None
1✔
28

29
    def __init__(self, *args, **kwargs):
1✔
30
        if "source" in kwargs:
1✔
31
            self.source = kwargs["source"]
1✔
32
            del kwargs["source"]
1✔
33

34
        if "ballot" in kwargs:
1✔
35
            self.ballot = kwargs["ballot"]
1✔
36
            del kwargs["ballot"]
1✔
37

38
        super().__init__(*args, **kwargs)
1✔
39

40
    def add_fields(self, form, index):
1✔
41
        super().add_fields(form, index)
1✔
42
        form.initial["ballot"] = self.ballot.ballot_paper_id
1✔
43

44
        if hasattr(self, "source"):
1✔
45
            form.fields["source"].initial = self.source
1✔
46
            form.fields["source"].widget = forms.HiddenInput()
1✔
47

48
    def clean(self):
1✔
49
        if (
1✔
50
            not self.initial_form_count()
51
            and self.ballot.membership_set.exists()
52
        ):
53
            # No extra forms exist, meaning no new people were added
54
            return super().clean()
1✔
55

56
        # Check if any forms have data
57
        has_data = any(
1✔
58
            form.is_valid()
59
            and form.cleaned_data
60
            and not form.cleaned_data.get("DELETE", False)
61
            for form in self.forms
62
        )
63

64
        if not has_data and not self.ballot.membership_set.exists():
1✔
65
            raise ValidationError("At least one person required on this ballot")
1✔
66

67
        return super().clean()
1✔
68

69

70
class BulkAddFormSet(BaseBulkAddFormSet):
1✔
71
    def __init__(self, *args, **kwargs):
1✔
72
        super().__init__(*args, **kwargs)
1✔
73
        self.parties = Party.objects.register(
1✔
74
            self.ballot.post.party_set.slug.upper()
75
        ).default_party_choices(extra_party_ids=self.initial_party_ids)
76
        self.previous_party_affiliations_choices = (
1✔
77
            self.get_previous_party_affiliations_choices()
78
        )
79

80
    def get_form_kwargs(self, index):
1✔
81
        kwargs = super().get_form_kwargs(index)
1✔
82
        kwargs["party_choices"] = self.parties
1✔
83
        kwargs[
1✔
84
            "previous_party_affiliations_choices"
85
        ] = self.previous_party_affiliations_choices
86
        return kwargs
1✔
87

88
    @property
1✔
89
    def initial_party_ids(self):
1✔
90
        """
91
        Returns a list of any party ID's that are included in initial data
92
        """
93
        if self.initial is None:
1✔
94
            return []
1✔
95
        return [d.get("party")[0].split("__")[0] for d in self.initial]
1✔
96

97
    def get_previous_party_affiliations_choices(self):
1✔
98
        """
99
        Return choices for previous_party_affilations field. By getting these on
100
        the formset and passing to the form, it saves the query for every
101
        individual form
102
        """
103
        if not self.ballot.is_welsh_run:
1✔
104
            return []
1✔
105

106
        parties = Party.objects.register("GB").active_in_last_year(
1✔
107
            date=self.ballot.election.election_date
108
        )
109
        return parties.values_list("ec_id", "name")
1✔
110

111

112
class BaseBulkAddReviewFormSet(BaseBulkAddFormSet):
1✔
113
    def suggested_people(
1✔
114
        self,
115
        person_name,
116
        new_party,
117
        new_election,
118
        new_name,
119
    ):
120
        if person_name:
1✔
121
            org_id = (
1✔
122
                new_election.organization.pk
123
                if new_election and new_election.organization
124
                else None
125
            )
126
            annotations = {
1✔
127
                "new_party": Value(new_party, output_field=CharField()),
128
                "new_organisation": Value(
129
                    org_id,
130
                    output_field=IntegerField(),
131
                ),
132
                "new_name": Value(new_name, output_field=CharField()),
133
            }
134

135
            qs = (
1✔
136
                search_person_by_name(person_name, synonym=True)
137
                .prefetch_related(
138
                    Prefetch(
139
                        "memberships",
140
                        queryset=Membership.objects.select_related(
141
                            "party",
142
                            "ballot",
143
                            "ballot__election",
144
                            "ballot__election__organization",
145
                        ),
146
                    ),
147
                )
148
                .annotate(**annotations)
149
            )
150
            return qs[:5]
1✔
151
        return None
×
152

153
    def add_fields(self, form, index):
1✔
154
        super().add_fields(form, index)
1✔
155
        if not form["name"].value():
1✔
156
            return
1✔
157
        suggestions = self.suggested_people(
1✔
158
            form["name"].value(),
159
            new_party=form.initial.get("party"),
160
            new_election=self.ballot.election,
161
            new_name=form.initial.get("name"),
162
        )
163
        form.fields["select_person"] = PersonSuggestionModelChoiceField(
1✔
164
            queryset=suggestions,
165
            widget=PersonSuggestionRadioSelect,
166
        )
167

168
        form.fields["select_person"].choices = [
1✔
169
            (
170
                "_new",
171
                SafeText(f'Add a new profile "{form.initial.get("name")}"'),
172
            )
173
        ] + list(form.fields["select_person"].choices)
174
        form.fields["select_person"].initial = "_new"
1✔
175

176
        form.fields["party"] = forms.CharField(
1✔
177
            widget=forms.HiddenInput(
178
                attrs={"readonly": "readonly", "class": "party-select"}
179
            ),
180
            required=False,
181
        )
182
        form.fields["sopn_last_name"] = StrippedCharField(
1✔
183
            widget=forms.HiddenInput(attrs={"readonly": "readonly"}),
184
            required=False,
185
        )
186
        form.fields["sopn_first_names"] = StrippedCharField(
1✔
187
            widget=forms.HiddenInput(attrs={"readonly": "readonly"}),
188
            required=False,
189
        )
190

191
    def clean(self):
1✔
192
        errors = []
1✔
193
        if not hasattr(self, "ballot"):
1✔
194
            return super().clean()
×
195

196
        if self.ballot.candidates_locked:
1✔
197
            raise ValidationError(
1✔
198
                "Candidates have already been locked for this ballot"
199
            )
200
        for form in self.forms:
1✔
201
            if not form.is_valid():
1✔
202
                continue
×
203
            form_data = form.cleaned_data
1✔
204
            if (
1✔
205
                "select_person" in form_data
206
                and form_data["select_person"] == "_new"
207
            ):
208
                continue
1✔
209
            qs = (
1✔
210
                Membership.objects.filter(ballot__election=self.ballot.election)
211
                .filter(person_id=form_data.get("select_person"))
212
                .exclude(ballot=self.ballot)
213
                .exclude(ballot__candidates_locked=True)
214
            )
215
            if qs.exists():
1✔
216
                errors.append(
1✔
217
                    forms.ValidationError(
218
                        "'{}' is marked as standing in another ballot for this "
219
                        "election. Check you're entering the correct "
220
                        "information for {}".format(
221
                            form_data["name"], self.ballot.post.label
222
                        )
223
                    )
224
                )
225
        if errors:
1✔
226
            raise forms.ValidationError(errors)
1✔
227
        return super().clean()
1✔
228

229

230
class NameOnlyPersonForm(forms.Form):
1✔
231
    name = StrippedCharField(
1✔
232
        label="Name (style: Ali McKay Smith not SMITH Ali McKay)",
233
        required=True,
234
        widget=forms.TextInput(
235
            attrs={"class": "person_name", "spellcheck": "false"}
236
        ),
237
    )
238
    ballot = ValidBallotField(
1✔
239
        widget=BallotInputWidget(attrs={"type": "hidden"})
240
    )
241
    sopn_last_name = StrippedCharField(
1✔
242
        widget=forms.HiddenInput(attrs={"readonly": "readonly"}),
243
        required=False,
244
    )
245
    sopn_first_names = StrippedCharField(
1✔
246
        widget=forms.HiddenInput(attrs={"readonly": "readonly"}),
247
        required=False,
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
    renderer = None
1✔
372

373
    def __init__(self, *args, **kwargs):
1✔
374
        self.ballot = kwargs["ballot"]
1✔
375
        kwargs["prefix"] = self.ballot.pk
1✔
376
        self.extra = self.ballot.winner_count
1✔
377
        del kwargs["ballot"]
1✔
378

379
        super().__init__(*args, **kwargs)
1✔
380

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

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

397

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

405

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

413
        super().__init__(*args, **kwargs)
1✔
414

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

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

435

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

440

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

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

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

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

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

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

475

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

479

480
class DeleteRawPeopleForm(forms.Form):
1✔
481
    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