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

DemocracyClub / yournextrepresentative / 8ebbd407-0726-42cb-87d6-d74b133eb099

23 Apr 2025 04:13PM UTC coverage: 68.199% (+0.02%) from 68.181%
8ebbd407-0726-42cb-87d6-d74b133eb099

Pull #2515

circleci

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

1608 of 2888 branches covered (55.68%)

Branch coverage included in aggregate %.

8 of 9 new or added lines in 1 file covered. (88.89%)

7 existing lines in 1 file now uncovered.

7590 of 10599 relevant lines covered (71.61%)

0.72 hits per line

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

90.82
/ynr/apps/bulk_adding/forms.py
1
from datetime import date
1✔
2

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

23

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

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

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

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

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

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

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

60

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

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

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

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

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

102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

238

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

251

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

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

282
    def clean_birth_date(self):
1✔
283
        bd = self.cleaned_data["birth_date"]
1✔
284
        if bd:
1✔
285
            current_year = date.today().year
1✔
286
            min_year = str(current_year - 18)
1✔
287
            if not ("1900" < bd <= min_year):
1!
NEW
288
                raise ValidationError("Please enter a valid year of birth")
×
289
        return bd
1✔
290

291

292
class QuickAddSinglePersonForm(PopulatePartiesMixin, NameOnlyPersonForm):
1✔
293
    source = forms.CharField(required=True)
1✔
294
    party = PartyIdentifierField()
1✔
295
    previous_party_affiliations = PreviousPartyAffiliationsField()
1✔
296

297
    def __init__(self, **kwargs):
1✔
298
        previous_party_affiliations_choices = kwargs.pop(
1✔
299
            "previous_party_affiliations_choices", []
300
        )
301
        super().__init__(**kwargs)
1✔
302
        self.fields[
1✔
303
            "previous_party_affiliations"
304
        ].choices = previous_party_affiliations_choices
305

306
    def has_changed(self, *args, **kwargs):
1✔
307
        if "name" not in self.changed_data:
1✔
308
            return False
1✔
309
        return super().has_changed(*args, **kwargs)
1✔
310

311
    def clean(self):
1✔
312
        if (
1!
313
            not self.cleaned_data["ballot"].is_welsh_run
314
            and self.cleaned_data["previous_party_affiliations"]
315
        ):
316
            raise ValidationError(
×
317
                "Previous party affiliations are invalid for this ballot"
318
            )
319
        return super().clean()
1✔
320

321

322
class ReviewSinglePersonNameOnlyForm(forms.Form):
1✔
323
    def __init__(self, *args, **kwargs):
1✔
324
        kwargs.pop("party_choices", None)
1✔
325
        super().__init__(*args, **kwargs)
1✔
326

327
    name = StrippedCharField(
1✔
328
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
329
    )
330

331

332
class ReviewBulkAddByPartyForm(ReviewSinglePersonNameOnlyForm):
1✔
333
    biography = StrippedCharField(
1✔
334
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
335
    )
336
    gender = StrippedCharField(
1✔
337
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
338
    )
339
    birth_date = forms.CharField(
1✔
340
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
341
    )
342
    person_identifiers = forms.CharField(
1✔
343
        required=False,
344
        widget=forms.HiddenInput(
345
            attrs={"readonly": "readonly"},
346
        ),
347
    )
348

349

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

366

367
BulkAddFormSetFactory = forms.formset_factory(
1✔
368
    QuickAddSinglePersonForm, extra=15, formset=BulkAddFormSet, can_delete=True
369
)
370

371

372
BulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
373
    ReviewSinglePersonNameOnlyForm, extra=0, formset=BaseBulkAddReviewFormSet
374
)
375

376
BulkAddReviewFormSet = forms.formset_factory(
1✔
377
    ReviewSinglePersonForm, extra=0, formset=BaseBulkAddReviewFormSet
378
)
379

380

381
class BaseBulkAddByPartyFormset(forms.BaseFormSet):
1✔
382
    def __init__(self, *args, **kwargs):
1✔
383
        self.ballot = kwargs["ballot"]
1✔
384
        kwargs["prefix"] = self.ballot.pk
1✔
385
        self.extra = self.ballot.winner_count
1✔
386
        del kwargs["ballot"]
1✔
387

388
        super().__init__(*args, **kwargs)
1✔
389

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

401
        if self.ballot.candidates_locked:
1!
402
            for name, field in form.fields.items():
×
403
                field.disabled = True
×
404
                form.fields[name] = field
×
405

406

407
BulkAddByPartyFormset = forms.formset_factory(
1✔
408
    BulkAddByPartyForm,
409
    extra=6,
410
    formset=BaseBulkAddByPartyFormset,
411
    can_delete=False,
412
)
413

414

415
class PartyBulkAddReviewFormSet(BaseBulkAddReviewFormSet):
1✔
416
    def __init__(self, *args, **kwargs):
1✔
417
        self.ballot = kwargs["ballot"]
1✔
418
        kwargs["prefix"] = self.ballot.pk
1✔
419
        self.extra = self.ballot.winner_count
1✔
420
        del kwargs["ballot"]
1✔
421

422
        super().__init__(*args, **kwargs)
1✔
423

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

437
    def add_fields(self, form, index):
1✔
438
        super().add_fields(form, index)
1✔
439
        if self.ballot.election.party_lists_in_use:
1!
440
            form.fields["party_list_position"] = forms.IntegerField(
×
441
                min_value=1, required=False, widget=forms.HiddenInput()
442
            )
443

444

445
PartyBulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
446
    ReviewBulkAddByPartyForm, extra=0, formset=PartyBulkAddReviewFormSet
447
)
448

449

450
class SelectPartyForm(forms.Form):
1✔
451
    def __init__(self, *args, **kwargs):
1✔
452
        election = kwargs.pop("election")
1✔
453
        super().__init__(*args, **kwargs)
1✔
454

455
        self.election = election
1✔
456
        party_set_qs = (
1✔
457
            election.ballot_set.all()
458
            .order_by("post__party_set")
459
            .values_list("post__party_set__slug", flat=True)
460
        )
461

462
        registers = {p.upper() for p in party_set_qs}
1✔
463
        for register in registers:
1✔
464
            choices = Party.objects.register(register).party_choices(
1✔
465
                include_description_ids=True
466
            )
467
            field = PartyIdentifierField(
1✔
468
                label=f"{register} parties", choices=choices
469
            )
470

471
            field.fields[0].choices = choices
1✔
472
            field.widget.attrs["data-party-register"] = register
1✔
473
            field.widget.attrs["register"] = register
1✔
474
            self.fields[f"party_{register}"] = field
1✔
475

476
    def clean(self):
1✔
477
        form_data = self.cleaned_data
1✔
478
        if len([v for v in form_data.values() if v]) != 1:
1✔
479
            self.cleaned_data = {}
1✔
480
            raise forms.ValidationError("Select one and only one party")
1✔
481

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

484

485
class AddByPartyForm(forms.Form):
1✔
486
    source = forms.CharField(required=True)
1✔
487

488

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