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

DemocracyClub / yournextrepresentative / 1986a3c2-8f69-41c4-98c1-54f3c6331c9d

pending completion
1986a3c2-8f69-41c4-98c1-54f3c6331c9d

push

circleci

web-flow
Merge pull request #2151 from DemocracyClub/ruff

Ruff

1638 of 2752 branches covered (59.52%)

Branch coverage included in aggregate %.

355 of 355 new or added lines in 114 files covered. (100.0%)

6623 of 9446 relevant lines covered (70.11%)

0.7 hits per line

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

91.58
/ynr/apps/bulk_adding/forms.py
1
from django import forms
1✔
2
from django.core.exceptions import ValidationError
1✔
3
from django.db.models import Prefetch
1✔
4
from django.utils.safestring import SafeText, mark_safe
1✔
5
from elections.models import Election
1✔
6
from official_documents.models import OfficialDocument
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

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
            .prefetch_related(
141
                Prefetch(
142
                    "ballot__officialdocument_set",
143
                    queryset=OfficialDocument.objects.filter(
144
                        document_type=OfficialDocument.NOMINATION_PAPER
145
                    ).order_by("-modified"),
146
                ),
147
            )
148
            .order_by("-ballot__election__election_date")[:3]
149
        )
150

151
        if candidacies:
1!
152
            suggestion_dict["previous_candidacies"] = []
1✔
153

154
        for candidacy in candidacies:
1✔
155
            party = candidacy.party
1✔
156
            party_str = f"{party.name}"
1✔
157
            if new_party == party.ec_id:
1!
158
                party_str = f"<strong>{party.name}</strong>"
×
159

160
            election = candidacy.ballot.election
1✔
161
            election_str = f"{election.name}"
1✔
162
            if new_election.organization == election.organization:
1✔
163
                election_str = f"<strong>{election.name}</strong>"
1✔
164

165
            text = """{election}: {post} – {party}""".format(
1✔
166
                post=candidacy.ballot.post.short_label,
167
                election=election_str,
168
                party=party_str,
169
            )
170
            sopn = candidacy.ballot.officialdocument_set.first()
1✔
171
            if sopn:
1✔
172
                text += ' (<a href="{0}">SOPN</a>)'.format(
1✔
173
                    sopn.get_absolute_url()
174
                )
175
            suggestion_dict["previous_candidacies"].append(SafeText(text))
1✔
176

177
        return [suggestion.pk, suggestion_dict]
1✔
178

179
    def add_fields(self, form, index):
1✔
180
        super().add_fields(form, index)
1✔
181
        if not form["name"].value():
1✔
182
            return
1✔
183
        suggestions = self.suggested_people(form["name"].value())
1✔
184

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

201
        form.fields["party"] = forms.CharField(
1✔
202
            widget=forms.HiddenInput(
203
                attrs={"readonly": "readonly", "class": "party-select"}
204
            ),
205
            required=False,
206
        )
207

208
    def clean(self):
1✔
209
        errors = []
1✔
210
        if not hasattr(self, "ballot"):
1!
211
            return super().clean()
×
212

213
        if self.ballot.candidates_locked:
1✔
214
            raise ValidationError(
1✔
215
                "Candidates have already been locked for this ballot"
216
            )
217

218
        for form_data in self.cleaned_data:
1✔
219

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

245

246
class NameOnlyPersonForm(forms.Form):
1✔
247
    name = StrippedCharField(
1✔
248
        label="Name (style: Ali McKay Smith not SMITH Ali McKay)",
249
        required=True,
250
        widget=forms.TextInput(
251
            attrs={"class": "person_name", "spellcheck": "false"}
252
        ),
253
    )
254
    ballot = ValidBallotField(
1✔
255
        widget=BallotInputWidget(attrs={"type": "hidden"})
256
    )
257

258

259
class QuickAddSinglePersonForm(PopulatePartiesMixin, NameOnlyPersonForm):
1✔
260
    source = forms.CharField(required=True)
1✔
261
    party = PartyIdentifierField()
1✔
262
    previous_party_affiliations = PreviousPartyAffiliationsField()
1✔
263

264
    def __init__(self, **kwargs):
1✔
265
        previous_party_affiliations_choices = kwargs.pop(
1✔
266
            "previous_party_affiliations_choices", []
267
        )
268
        super().__init__(**kwargs)
1✔
269
        self.fields[
1✔
270
            "previous_party_affiliations"
271
        ].choices = previous_party_affiliations_choices
272

273
    def has_changed(self, *args, **kwargs):
1✔
274
        if "name" not in self.changed_data:
1✔
275
            return False
1✔
276
        return super().has_changed(*args, **kwargs)
1✔
277

278
    def clean(self):
1✔
279
        if (
1!
280
            not self.cleaned_data["ballot"].is_welsh_run
281
            and self.cleaned_data["previous_party_affiliations"]
282
        ):
283
            raise ValidationError(
×
284
                "Previous party affiliations are invalid for this ballot"
285
            )
286
        return super().clean()
1✔
287

288

289
class ReviewSinglePersonNameOnlyForm(forms.Form):
1✔
290
    def __init__(self, *args, **kwargs):
1✔
291
        kwargs.pop("party_choices", None)
1✔
292
        super().__init__(*args, **kwargs)
1✔
293

294
    name = StrippedCharField(
1✔
295
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
296
    )
297

298

299
class ReviewSinglePersonForm(ReviewSinglePersonNameOnlyForm):
1✔
300
    source = forms.CharField(
1✔
301
        required=False, widget=forms.HiddenInput(attrs={"readonly": "readonly"})
302
    )
303
    party_description = forms.ModelChoiceField(
1✔
304
        required=False,
305
        widget=forms.HiddenInput(),
306
        queryset=PartyDescription.objects.all(),
307
    )
308
    party_description_text = forms.CharField(
1✔
309
        required=False, widget=forms.HiddenInput()
310
    )
311
    previous_party_affiliations = forms.CharField(
1✔
312
        required=False, widget=forms.HiddenInput()
313
    )
314

315

316
BulkAddFormSetFactory = forms.formset_factory(
1✔
317
    QuickAddSinglePersonForm, extra=15, formset=BulkAddFormSet, can_delete=True
318
)
319

320

321
BulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
322
    ReviewSinglePersonNameOnlyForm, extra=0, formset=BaseBulkAddReviewFormSet
323
)
324

325
BulkAddReviewFormSet = forms.formset_factory(
1✔
326
    ReviewSinglePersonForm, extra=0, formset=BaseBulkAddReviewFormSet
327
)
328

329

330
class BaseBulkAddByPartyFormset(forms.BaseFormSet):
1✔
331
    def __init__(self, *args, **kwargs):
1✔
332
        self.ballot = kwargs["ballot"]
1✔
333
        kwargs["prefix"] = self.ballot.pk
1✔
334
        self.extra = self.ballot.winner_count
1✔
335
        del kwargs["ballot"]
1✔
336

337
        super().__init__(*args, **kwargs)
1✔
338

339
    def add_fields(self, form, index):
1✔
340
        super().add_fields(form, index)
1✔
341
        if self.ballot.election.party_lists_in_use:
1!
342
            form.fields["party_list_position"] = forms.IntegerField(
×
343
                label="Position in party list ('1' for first, '2' for second, etc.)",
344
                min_value=1,
345
                required=False,
346
                initial=index + 1,
347
                widget=forms.NumberInput(attrs={"class": "party-position"}),
348
            )
349

350
        if self.ballot.candidates_locked:
1!
351
            for name, field in form.fields.items():
×
352
                field.disabled = True
×
353
                form.fields[name] = field
×
354

355

356
BulkAddByPartyFormset = forms.formset_factory(
1✔
357
    NameOnlyPersonForm,
358
    extra=6,
359
    formset=BaseBulkAddByPartyFormset,
360
    can_delete=False,
361
)
362

363

364
class PartyBulkAddReviewFormSet(BaseBulkAddReviewFormSet):
1✔
365
    def __init__(self, *args, **kwargs):
1✔
366
        self.ballot = kwargs["ballot"]
1✔
367
        kwargs["prefix"] = self.ballot.pk
1✔
368
        self.extra = self.ballot.winner_count
1✔
369
        del kwargs["ballot"]
1✔
370

371
        super().__init__(*args, **kwargs)
1✔
372

373
    def clean(self):
1✔
374
        """
375
        Use this method to prevent users from adding candidates to ballots
376
        that can't have candidates added to them, like locked or cancelled
377
        ballots
378
        """
379
        if self.ballot.candidates_locked:
1!
380
            raise ValidationError("Cannot add candidates to a locked ballot")
×
381
        if self.ballot.cancelled:
1!
382
            raise ValidationError(
×
383
                "Cannot add candidates to a cancelled election"
384
            )
385

386
    def add_fields(self, form, index):
1✔
387
        super().add_fields(form, index)
1✔
388
        if self.ballot.election.party_lists_in_use:
1!
389
            form.fields["party_list_position"] = forms.IntegerField(
×
390
                min_value=1, required=False, widget=forms.HiddenInput()
391
            )
392

393

394
PartyBulkAddReviewNameOnlyFormSet = forms.formset_factory(
1✔
395
    ReviewSinglePersonNameOnlyForm, extra=0, formset=PartyBulkAddReviewFormSet
396
)
397

398

399
class SelectPartyForm(forms.Form):
1✔
400
    def __init__(self, *args, **kwargs):
1✔
401

402
        election = kwargs.pop("election")
1✔
403
        super().__init__(*args, **kwargs)
1✔
404

405
        self.election = election
1✔
406
        party_set_qs = (
1✔
407
            election.ballot_set.all()
408
            .order_by("post__party_set")
409
            .values_list("post__party_set__slug", flat=True)
410
        )
411

412
        registers = {p.upper() for p in party_set_qs}
1✔
413
        for register in registers:
1✔
414
            choices = Party.objects.register(register).party_choices(
1✔
415
                include_description_ids=True
416
            )
417
            field = PartyIdentifierField(
1✔
418
                label=f"{register} parties", choices=choices
419
            )
420

421
            field.fields[0].choices = choices
1✔
422
            field.widget.attrs["data-party-register"] = register
1✔
423
            field.widget.attrs["register"] = register
1✔
424
            self.fields[f"party_{register}"] = field
1✔
425

426
    def clean(self):
1✔
427
        form_data = self.cleaned_data
1✔
428
        if len([v for v in form_data.values() if v]) != 1:
1✔
429
            self.cleaned_data = {}
1✔
430
            raise forms.ValidationError("Select one and only one party")
1✔
431

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

434

435
class AddByPartyForm(forms.Form):
1✔
436

437
    source = forms.CharField(required=True)
1✔
438

439

440
class DeleteRawPeopleForm(forms.Form):
1✔
441

442
    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