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

DemocracyClub / yournextrepresentative / e2a380f2-d6da-44d6-848a-253f97bdcafa

23 Apr 2025 03:25PM UTC coverage: 68.185% (+0.002%) from 68.183%
e2a380f2-d6da-44d6-848a-253f97bdcafa

Pull #2515

circleci

awdem
add validation to person form birth_date field
Pull Request #2515: add validation to person form birth_date field

1606 of 2886 branches covered (55.65%)

Branch coverage included in aggregate %.

4 of 5 new or added lines in 1 file covered. (80.0%)

13 existing lines in 1 file now uncovered.

7586 of 10595 relevant lines covered (71.6%)

0.72 hits per line

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

90.64
/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 popolo.models import Membership
1✔
19
from search.utils import search_person_by_name
1✔
20

21

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

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

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

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

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

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

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

58

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

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

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

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

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

100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

236

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

249

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

271
    def clean_biography(self):
1✔
272
        bio = self.cleaned_data["biography"]
1✔
273
        if bio.find("\r"):
1!
274
            bio = bio.replace("\r", "")
1✔
275
        # Reduce > 2 newlines to 2 newlines
276
        return "\n\n".join(
1✔
277
            [line.strip() for line in bio.split("\n\n") if line.strip()]
278
        )
279

280
    def clean_birth_date(self):
1✔
281
        bd = self.cleaned_data["birth_date"]
1✔
282
        if bd and not bd > "1900":
1!
NEW
UNCOV
283
            raise ValidationError("Please enter a valid year of birth")
×
284
        return bd
1✔
285

286

287
class QuickAddSinglePersonForm(PopulatePartiesMixin, NameOnlyPersonForm):
1✔
288
    source = forms.CharField(required=True)
1✔
289
    party = PartyIdentifierField()
1✔
290
    previous_party_affiliations = PreviousPartyAffiliationsField()
1✔
291

292
    def __init__(self, **kwargs):
1✔
293
        previous_party_affiliations_choices = kwargs.pop(
1✔
294
            "previous_party_affiliations_choices", []
295
        )
296
        super().__init__(**kwargs)
1✔
297
        self.fields[
1✔
298
            "previous_party_affiliations"
299
        ].choices = previous_party_affiliations_choices
300

301
    def has_changed(self, *args, **kwargs):
1✔
302
        if "name" not in self.changed_data:
1✔
303
            return False
1✔
304
        return super().has_changed(*args, **kwargs)
1✔
305

306
    def clean(self):
1✔
307
        if (
1!
308
            not self.cleaned_data["ballot"].is_welsh_run
309
            and self.cleaned_data["previous_party_affiliations"]
310
        ):
UNCOV
311
            raise ValidationError(
×
312
                "Previous party affiliations are invalid for this ballot"
313
            )
314
        return super().clean()
1✔
315

316

317
class ReviewSinglePersonNameOnlyForm(forms.Form):
1✔
318
    def __init__(self, *args, **kwargs):
1✔
319
        kwargs.pop("party_choices", None)
1✔
320
        super().__init__(*args, **kwargs)
1✔
321

322
    name = StrippedCharField(
1✔
323
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
324
    )
325

326

327
class ReviewBulkAddByPartyForm(ReviewSinglePersonNameOnlyForm):
1✔
328
    biography = StrippedCharField(
1✔
329
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
330
    )
331
    gender = StrippedCharField(
1✔
332
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
333
    )
334
    birth_date = forms.CharField(
1✔
335
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
336
    )
337
    person_identifiers = forms.CharField(
1✔
338
        required=False,
339
        widget=forms.HiddenInput(
340
            attrs={"readonly": "readonly"},
341
        ),
342
    )
343

344

345
class ReviewSinglePersonForm(ReviewSinglePersonNameOnlyForm):
1✔
346
    source = forms.CharField(
1✔
347
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
348
    )
349
    party_description = forms.ModelChoiceField(
1✔
350
        required=False,
351
        widget=forms.HiddenInput(),
352
        queryset=PartyDescription.objects.all(),
353
    )
354
    party_description_text = forms.CharField(
1✔
355
        required=False, widget=forms.HiddenInput()
356
    )
357
    previous_party_affiliations = forms.CharField(
1✔
358
        required=False, widget=forms.HiddenInput()
359
    )
360

361

362
BulkAddFormSetFactory = forms.formset_factory(
1✔
363
    QuickAddSinglePersonForm, extra=15, formset=BulkAddFormSet, can_delete=True
364
)
365

366

367
BulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
368
    ReviewSinglePersonNameOnlyForm, extra=0, formset=BaseBulkAddReviewFormSet
369
)
370

371
BulkAddReviewFormSet = forms.formset_factory(
1✔
372
    ReviewSinglePersonForm, extra=0, formset=BaseBulkAddReviewFormSet
373
)
374

375

376
class BaseBulkAddByPartyFormset(forms.BaseFormSet):
1✔
377
    def __init__(self, *args, **kwargs):
1✔
378
        self.ballot = kwargs["ballot"]
1✔
379
        kwargs["prefix"] = self.ballot.pk
1✔
380
        self.extra = self.ballot.winner_count
1✔
381
        del kwargs["ballot"]
1✔
382

383
        super().__init__(*args, **kwargs)
1✔
384

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

396
        if self.ballot.candidates_locked:
1!
UNCOV
397
            for name, field in form.fields.items():
×
UNCOV
398
                field.disabled = True
×
UNCOV
399
                form.fields[name] = field
×
400

401

402
BulkAddByPartyFormset = forms.formset_factory(
1✔
403
    BulkAddByPartyForm,
404
    extra=6,
405
    formset=BaseBulkAddByPartyFormset,
406
    can_delete=False,
407
)
408

409

410
class PartyBulkAddReviewFormSet(BaseBulkAddReviewFormSet):
1✔
411
    def __init__(self, *args, **kwargs):
1✔
412
        self.ballot = kwargs["ballot"]
1✔
413
        kwargs["prefix"] = self.ballot.pk
1✔
414
        self.extra = self.ballot.winner_count
1✔
415
        del kwargs["ballot"]
1✔
416

417
        super().__init__(*args, **kwargs)
1✔
418

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

432
    def add_fields(self, form, index):
1✔
433
        super().add_fields(form, index)
1✔
434
        if self.ballot.election.party_lists_in_use:
1!
UNCOV
435
            form.fields["party_list_position"] = forms.IntegerField(
×
436
                min_value=1, required=False, widget=forms.HiddenInput()
437
            )
438

439

440
PartyBulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
441
    ReviewBulkAddByPartyForm, extra=0, formset=PartyBulkAddReviewFormSet
442
)
443

444

445
class SelectPartyForm(forms.Form):
1✔
446
    def __init__(self, *args, **kwargs):
1✔
447
        election = kwargs.pop("election")
1✔
448
        super().__init__(*args, **kwargs)
1✔
449

450
        self.election = election
1✔
451
        party_set_qs = (
1✔
452
            election.ballot_set.all()
453
            .order_by("post__party_set")
454
            .values_list("post__party_set__slug", flat=True)
455
        )
456

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

466
            field.fields[0].choices = choices
1✔
467
            field.widget.attrs["data-party-register"] = register
1✔
468
            field.widget.attrs["register"] = register
1✔
469
            self.fields[f"party_{register}"] = field
1✔
470

471
    def clean(self):
1✔
472
        form_data = self.cleaned_data
1✔
473
        if len([v for v in form_data.values() if v]) != 1:
1✔
474
            self.cleaned_data = {}
1✔
475
            raise forms.ValidationError("Select one and only one party")
1✔
476

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

479

480
class AddByPartyForm(forms.Form):
1✔
481
    source = forms.CharField(required=True)
1✔
482

483

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