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

DemocracyClub / yournextrepresentative / c52643c5-40ed-46fc-9a3d-2683ffe8096e

24 Apr 2025 01:45PM UTC coverage: 68.199% (+0.02%) from 68.181%
c52643c5-40ed-46fc-9a3d-2683ffe8096e

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%)

13 existing lines in 2 files 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 - 19)
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
class QuickAddSinglePersonForm(PopulatePartiesMixin, NameOnlyPersonForm):
292
    source = forms.CharField(required=True)
1✔
293
    party = PartyIdentifierField()
1✔
294
    previous_party_affiliations = PreviousPartyAffiliationsField()
1✔
295

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

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

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

1✔
320

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

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

330

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

348

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

365

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

370

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

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

379

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

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

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

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

×
405

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

413

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

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

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

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

443

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

448

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

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

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

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

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

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

1✔
483

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

1✔
487

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