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

DemocracyClub / WhoCanIVoteFor / e1a02576-fed3-4822-b3d7-83768ed61fb1

09 Oct 2025 04:45PM UTC coverage: 58.669% (+0.1%) from 58.523%
e1a02576-fed3-4822-b3d7-83768ed61fb1

Pull #2299

circleci

chris48s
register markdown_subset filter, use it for candidate statements
Pull Request #2299: WIP render good markdown, but not bad markdown

11 of 11 new or added lines in 1 file covered. (100.0%)

268 existing lines in 11 files now uncovered.

2927 of 4989 relevant lines covered (58.67%)

0.59 hits per line

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

50.31
/wcivf/apps/elections/import_helpers.py
1
import contextlib
1✔
2
import re
1✔
3
import sys
1✔
4
from urllib.parse import urlencode
1✔
5

6
from django.conf import settings
1✔
7
from django.db import transaction
1✔
8
from django.utils import timezone
1✔
9
from elections.helpers import EEHelper, JsonPaginator
1✔
10
from elections.models import Election, Post, PostElection, VotingSystem
1✔
11
from parties.models import Party
1✔
12
from people.models import Person, PersonPost
1✔
13

14

15
def time_function_length(func):
1✔
16
    """
17
    Decorator to time how long an import function takes. Intended to
18
    help with debugging lambda timeouts.
19
    """
20

21
    def wraps(*args, **kwargs):
1✔
22
        start = timezone.now()
1✔
23
        sys.stdout.write(f"Starting {func.__name__}\n")
1✔
24
        result = func(*args, **kwargs)
1✔
25
        end = timezone.now()
1✔
26
        sys.stdout.write(f"{func.__name__} took {end - start} to complete\n")
1✔
27
        return result
1✔
28

29
    return wraps
1✔
30

31

32
class YNRElectionImporter:
1✔
33
    """
34
    Takes a JSON object from YNR and creates or updates an election object
35
    from it.
36

37
    Manages caching, and updating metadata from EE
38

39
    """
40

41
    def __init__(self, ee_helper=None):
1✔
42
        if not ee_helper:
1✔
43
            ee_helper = EEHelper()
×
44
        self.ee_helper = ee_helper
1✔
45
        self.election_cache = {}
1✔
46

47
    def ballot_order(self, ballot_dict):
1✔
48
        charisma_map = {
×
49
            "ref": {"default": 100},
50
            "parl": {"default": 90},
51
            "europarl": {"default": 80},
52
            "mayor": {"default": 70, "local-authority": 65},
53
            "nia": {"default": 60},
54
            "gla": {"default": 60, "a": 55},
55
            "naw": {"default": 60, "r": 55},
56
            "senedd": {"default": 60, "r": 65, "c": 60},
57
            "sp": {"default": 60, "r": 55},
58
            "pcc": {"default": 70},
59
            "local": {"default": 40},
60
        }
61
        modifier = 0
×
62
        ballot_paper_id = ballot_dict["ballot_paper_id"]
×
63

64
        # Look up the dict of possible weights for this election type
65
        weights = charisma_map.get(
×
66
            ballot_paper_id.split(".")[0], {"default": 30}
67
        )
68

69
        organisation_type = ballot_paper_id.split(".")[0]
×
70
        default_weight_for_election_type = weights.get("default")
×
71
        base_charisma = weights.get(
×
72
            organisation_type, default_weight_for_election_type
73
        )
74

75
        # Look up `r` and `a` subtypes
76
        subtype = re.match(r"^[^.]+\.([ar])\.", ballot_paper_id)
×
77
        if subtype:
×
78
            base_charisma = weights.get(subtype.group(1), base_charisma)
×
79

80
        # by-elections are slightly less charismatic than scheduled elections
81
        if ".by." in ballot_paper_id:
×
82
            modifier += 1
×
83

84
        return base_charisma - modifier
×
85

86
    def update_or_create_from_ballot_dict(self, ballot_dict):
1✔
87
        created = False
×
88
        slug = ballot_dict["election"]["election_id"]
×
89

90
        election_weight = self.ballot_order(ballot_dict)
×
91
        if slug not in self.election_cache:
×
92
            election_type = slug.split(".")[0]
×
93

94
            election, created = Election.objects.update_or_create(
×
95
                slug=slug,
96
                election_type=election_type,
97
                defaults={
98
                    "election_date": ballot_dict["election"]["election_date"],
99
                    "name": ballot_dict["election"]["name"].strip(),
100
                    "current": ballot_dict["election"]["current"],
101
                    "election_weight": election_weight,
102
                    "uses_lists": ballot_dict["election"]["party_lists_in_use"],
103
                },
104
            )
105
            self.import_metadata_from_ee(election)
×
106
            self.election_cache[election.slug] = election
×
107
        return self.election_cache[slug]
×
108

109
    def import_metadata_from_ee(self, election):
1✔
110
        """
111
        There are various things we don't have in YNR, have in EE and want here
112

113
        This means grabbing the data from EE directly
114
        """
115
        ee_data = self.ee_helper.get_data(election.slug)
×
116
        if ee_data:
×
117
            updated = False
×
118
            metadata = ee_data["metadata"]
×
119
            if metadata:
×
120
                election.metadata = metadata
×
121
                updated = True
×
122

123
            description = ee_data["explanation"]
×
124
            if description:
×
125
                election.description = description
×
126
                updated = True
×
127

128
            requires_voter_id = ee_data["requires_voter_id"]
×
129
            if requires_voter_id:
×
130
                election.requires_voter_id = requires_voter_id
×
131
                updated = True
×
132

133
            cancellation_reason = ee_data["cancellation_reason"]
×
134
            if cancellation_reason:
×
135
                election.cancellation_reason = cancellation_reason
×
136
                updated = True
×
137

138
            by_election_reason = ee_data["by_election_reason"]
×
139
            if by_election_reason:
×
140
                election.by_election_reason = by_election_reason
×
141
                updated = True
×
142

143
            voting_system = ee_data["voting_system"]
×
144
            if voting_system:
×
145
                election.voting_system = VotingSystem.objects.update_or_create(
×
146
                    slug=voting_system["slug"],
147
                    defaults={"name": voting_system["name"]},
148
                )[0]
149
                updated = True
×
150

151
            if updated:
×
152
                election.save()
×
153

154

155
class YNRPostImporter:
1✔
156
    def __init__(self, ee_helper=None):
1✔
157
        if not ee_helper:
1✔
158
            ee_helper = EEHelper()
1✔
159
        self.ee_helper = ee_helper
1✔
160
        self.post_cache = {}
1✔
161

162
    def update_or_create_from_ballot_dict(self, ballot_dict):
1✔
163
        # fall back to slug here as some temp ballots don't have an ID set
164
        post_id = ballot_dict["post"]["id"] or ballot_dict["post"]["slug"]
1✔
165
        if not post_id:
1✔
166
            # if no id to use return None to indicate to skip this ballot
167
            return None
×
168

169
        if post_id not in self.post_cache:
1✔
170
            post, _ = Post.objects.update_or_create(
1✔
171
                ynr_id=post_id,
172
                defaults={"label": ballot_dict["post"]["label"]},
173
            )
174
            self.post_cache[post_id] = post
1✔
175
        return self.post_cache[post_id]
1✔
176

177

178
class YNRBallotImporter:
1✔
179
    """
180
    Class for populating local election and ballot models in this
181
    project from YNR.
182

183
    The class sets up everything needed for show a ballot, including elections,
184
    posts, voting systems, and the person information that show's on a ballot.
185
    (name, candidacy data)
186

187
    """
188

189
    def __init__(
1✔
190
        self,
191
        force_update=False,
192
        stdout=sys.stdout,
193
        current_only=False,
194
        exclude_candidacies=False,
195
        force_metadata=False,
196
        recently_updated=False,
197
        base_url=None,
198
        api_key=None,
199
        default_params=None,
200
    ):
201
        self.stdout = stdout
1✔
202
        self.ee_helper = EEHelper()
1✔
203
        self.voting_systems = {}
1✔
204
        self.election_importer = YNRElectionImporter(self.ee_helper)
1✔
205
        self.post_importer = YNRPostImporter(self.ee_helper)
1✔
206
        self.force_update = force_update
1✔
207
        self.current_only = current_only
1✔
208
        self.exclude_candidacies = exclude_candidacies
1✔
209
        self.force_metadata = force_metadata
1✔
210
        self.recently_updated = recently_updated
1✔
211
        self.base_url = base_url or settings.YNR_BASE
1✔
212
        self.api_key = api_key or settings.YNR_API_KEY
1✔
213
        self.default_params = default_params or {"page_size": 200}
1✔
214

215
    @time_function_length
1✔
216
    def get_paginator(self, page1):
1✔
217
        return JsonPaginator(page1, self.stdout)
×
218

219
    @time_function_length
1✔
220
    def get_last_updated(self):
1✔
221
        try:
×
222
            return PostElection.objects.last_updated_in_ynr().ynr_modified
×
223
        except PostElection.DoesNotExist:
×
224
            # default before changes were added to YNR
225
            return timezone.datetime(2021, 10, 27, tzinfo=timezone.utc)
×
226

227
    @property
1✔
228
    def should_prewarm_ee_cache(self):
1✔
229
        """
230
        Always if current_only, otherwise check if params or is a
231
        recent updates only
232
        """
233
        if self.current_only:
1✔
234
            return True
1✔
235

236
        return not any([self.params, self.recently_updated])
1✔
237

238
    @property
1✔
239
    def is_full_import(self):
1✔
240
        """
241
        Check if any flags or paras
242
        """
243
        return not any([self.recently_updated, self.current_only, self.params])
1✔
244

245
    @time_function_length
1✔
246
    def build_params(self, params):
1✔
247
        """
248
        Build up params based on flages initialised with or return an
249
        empty dict
250
        """
251
        params = params or {}
1✔
252

253
        if self.api_key:
1✔
254
            params["auth_token"] = self.api_key
1✔
255

256
        if self.current_only:
1✔
257
            params["current"] = True
1✔
258

259
        if self.recently_updated:
1✔
260
            params["last_updated"] = self.get_last_updated().isoformat()
1✔
261

262
        if params:
1✔
263
            params.update(self.default_params)
1✔
264

265
        return params
1✔
266

267
    @property
1✔
268
    def import_url(self):
1✔
269
        """
270
        Use cached data if a full import, unless base_url is using locahost
271
        """
272
        if self.is_full_import and not self.base_url.startswith(
1✔
273
            "http://localhost"
274
        ):
275
            return (
1✔
276
                f"{self.base_url}/media/cached-api/latest/ballots-000001.json"
277
            )
278

279
        querystring = urlencode(self.params)
1✔
280
        return f"{self.base_url}/api/next/ballots/?{querystring}"
1✔
281

282
    @property
1✔
283
    def should_run_post_ballot_import_tasks(self):
1✔
284
        """
285
        Don't try to do things like add replaced
286
        ballots if we've filtered the ballots.
287
        This is because there's a high chance we've not
288
        got all the ballots we need yet.
289
        """
290
        return any([self.is_full_import, self.current_only])
1✔
291

292
    def do_import(self, params=None):
1✔
293
        self.params = self.build_params(params=params)
1✔
294

295
        if self.should_prewarm_ee_cache:
1✔
296
            self.ee_helper.prewarm_cache(current=not self.force_metadata)
1✔
297

298
        pages = self.get_paginator(self.import_url)
1✔
299
        for page in pages:
1✔
300
            self.add_ballots(page)
1✔
301

302
        if self.should_run_post_ballot_import_tasks:
1✔
303
            self.attach_cancelled_ballot_info()
1✔
304

305
        if self.recently_updated:
1✔
306
            self.check_for_ee_updates()
×
307

308
        self.delete_orphan_posts()
1✔
309

310
    @time_function_length
1✔
311
    def delete_orphan_posts(self):
1✔
312
        """
313
        This method cleans orphan posts.
314
        This typically gets called at the end of the import process.
315
        """
316
        return Post.objects.filter(postelection=None).delete()
1✔
317

318
    def add_replaced_ballot(self, ballot, replaced_ballot_id):
1✔
319
        """
320
        Takes a ballot object and a ballot_paper_id for a ballot that
321
        has been replaced. If the replaced ballot is found, adds this
322
        relationship. Explicity return True or False to represent if
323
        the lookup was a success and help with testing.
324
        """
325
        if not replaced_ballot_id:
1✔
326
            return False
1✔
327

328
        try:
1✔
329
            replaced_ballot = PostElection.objects.get(
1✔
330
                ballot_paper_id=replaced_ballot_id,
331
            )
332
        except PostElection.DoesNotExist:
1✔
333
            return False
1✔
334

335
        ballot.replaces.add(replaced_ballot)
1✔
336
        return True
1✔
337

338
    @time_function_length
1✔
339
    @transaction.atomic()
1✔
340
    def add_ballots(self, results):
1✔
341
        for ballot_dict in results["results"]:
1✔
342
            print(ballot_dict["ballot_paper_id"])
1✔
343

344
            election = self.election_importer.update_or_create_from_ballot_dict(
1✔
345
                ballot_dict
346
            )
347

348
            post = self.post_importer.update_or_create_from_ballot_dict(
1✔
349
                ballot_dict
350
            )
351
            if not post:
1✔
352
                # cant create a ballot without a post so skip to the next one
353
                continue
×
354

355
            defaults = {
1✔
356
                "election": election,
357
                "post": post,
358
                "winner_count": ballot_dict["winner_count"] or 1,
359
                "cancelled": ballot_dict["cancelled"],
360
                "locked": ballot_dict["candidates_locked"],
361
            }
362

363
            if (
1✔
364
                ballot_dict["candidates_locked"] or ballot_dict["cancelled"]
365
            ) and ballot_dict["winner_count"]:
366
                defaults["contested"] = not ballot_dict["uncontested"]
×
367

368
            # only update this when using the recently_updated flag as otherwise
369
            # the timestamp will only be the modifed timestamp on the ballot
370
            # see BallotSerializer.get_last_updated in YNR
371
            if self.recently_updated:
1✔
372
                defaults["ynr_modified"] = ballot_dict["last_updated"]
1✔
373

374
            if ballot_dict.get("results"):
1✔
375
                results_defaults = {
×
376
                    "ballot_papers_issued": ballot_dict["results"][
377
                        "num_turnout_reported"
378
                    ],
379
                    "electorate": ballot_dict["results"]["total_electorate"],
380
                    "turnout": ballot_dict["results"]["turnout_percentage"],
381
                    "spoilt_ballots": ballot_dict["results"][
382
                        "num_spoilt_ballots"
383
                    ],
384
                }
385

386
                defaults = {**defaults, **results_defaults}
×
387

388
            ballot, created = PostElection.objects.update_or_create(
1✔
389
                ballot_paper_id=ballot_dict["ballot_paper_id"],
390
                defaults=defaults,
391
            )
392

393
            if self.recently_updated:
1✔
394
                # we can do this as the older ballot will be known.
395
                # if the ballot is does not replace another ballot,
396
                # nothing happens
397
                self.add_replaced_ballot(
1✔
398
                    ballot=ballot,
399
                    replaced_ballot_id=ballot_dict.get("replaces"),
400
                )
401

402
            if ballot.election.current or self.force_metadata:
1✔
403
                self.import_metadata_from_ee(ballot)
1✔
404

405
            if not self.exclude_candidacies:
1✔
406
                # Now set the nominations up for this ballot
407
                # First, remove any old candidates, this is to flush out candidates
408
                # that have changed. We just delete the `person_post`
409
                # (`membership` in YNR), not the person profile.
410
                ballot.personpost_set.all().delete()
1✔
411
                for candidate in ballot_dict["candidacies"]:
1✔
412
                    person, person_created = Person.objects.update_or_create(
1✔
413
                        ynr_id=candidate["person"]["id"],
414
                        defaults={"name": candidate["person"]["name"]},
415
                    )
416
                    result = candidate["result"] or {}
1✔
417
                    # if we dont have a result, get the "elected" value from
418
                    # the main candidacy data
419
                    elected = result.get("elected", candidate["elected"])
1✔
420
                    person_post = PersonPost.objects.create(
1✔
421
                        post_election=ballot,
422
                        person=person,
423
                        party_id=candidate["party"]["legacy_slug"],
424
                        party_name=candidate["party_name"],
425
                        party_description_text=candidate[
426
                            "party_description_text"
427
                        ],
428
                        list_position=candidate["party_list_position"],
429
                        deselected=candidate.get("deselected", False),
430
                        deselected_source=candidate.get("deselected_source"),
431
                        elected=elected,
432
                        votes_cast=result.get("num_ballots", None),
433
                        post=ballot.post,
434
                        election=ballot.election,
435
                    )
436
                    for party in candidate.get(
1✔
437
                        "previous_party_affiliations", []
438
                    ):
439
                        # if the previous party affiliation is the
440
                        # same as the party on the candidacy skip it
441
                        party_id = party["legacy_slug"]
1✔
442
                        if party_id == person_post.party_id:
1✔
443
                            continue
×
444

445
                        try:
1✔
446
                            party = Party.objects.get(party_id=party_id)
1✔
447
                        except Party.DoesNotExist:
×
448
                            continue
×
449
                        person_post.previous_party_affiliations.add(party)
1✔
450

451
            if created:
1✔
UNCOV
452
                self.stdout.write(
×
453
                    "Added new ballot: {0}".format(ballot.ballot_paper_id)
454
                )
455

456
    def import_metadata_from_ee(self, ballot):
1✔
457
        # First, grab the data from EE
458

UNCOV
459
        self.set_territory(ballot)
×
UNCOV
460
        self.set_voting_system(ballot)
×
UNCOV
461
        self.set_metadata(ballot)
×
UNCOV
462
        self.set_requires_voter_id(ballot)
×
463
        self.set_cancellation_reason(ballot)
×
464
        self.set_by_election_reason(ballot)
×
465
        self.set_organisation_type(ballot)
×
466
        self.set_division_type(ballot)
×
467
        ballot.save()
×
468

469
    def set_territory(self, ballot):
1✔
470
        if ballot.post.territory and not self.force_update:
×
471
            return
×
UNCOV
472
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
473
        # If an election has no divisions, try the org territory code
474
        # Otherwise use the division territory code
475
        if ee_data["division"] is None:
×
476
            territory = ee_data["organisation"].get("territory_code", "-")
×
477
        else:
UNCOV
478
            territory = ee_data["division"].get("territory_code")
×
479

480
        ballot.post.territory = territory
×
UNCOV
481
        ballot.post.save()
×
482

483
    def set_voting_system(self, ballot):
1✔
484
        if ballot.voting_system_id and not self.force_update:
×
485
            return
×
UNCOV
486
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
UNCOV
487
        if ee_data and "voting_system" in ee_data:
×
488
            voting_system_slug = ee_data["voting_system"]["slug"]
×
489
            if voting_system_slug not in self.voting_systems:
×
490
                voting_system = VotingSystem.objects.update_or_create(
×
491
                    slug=voting_system_slug,
492
                    defaults={"description": ee_data["voting_system"]["name"]},
493
                )[0]
494
                self.voting_systems[voting_system_slug] = voting_system
×
495

UNCOV
496
            ballot.voting_system = self.voting_systems[voting_system_slug]
×
UNCOV
497
            ballot.save()
×
498

499
    def set_metadata(self, ballot):
1✔
500
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
501
        if ee_data:
×
UNCOV
502
            ballot.metadata = ee_data["metadata"]
×
503

504
    def set_requires_voter_id(self, ballot):
1✔
505
        if ballot.requires_voter_id and not self.force_update:
×
506
            return
×
UNCOV
507
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
UNCOV
508
        if ee_data:
×
509
            ballot.requires_voter_id = ee_data["requires_voter_id"]
×
510
            ballot.save()
×
511

512
    def set_cancellation_reason(self, ballot):
1✔
513
        if ballot.cancellation_reason and not self.force_update:
×
514
            return
×
UNCOV
515
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
UNCOV
516
        if ee_data:
×
517
            ballot.cancellation_reason = ee_data["cancellation_reason"]
×
518
            ballot.save()
×
519

520
    def set_by_election_reason(self, ballot):
1✔
521
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
522
        if ee_data:
×
UNCOV
523
            ballot.by_election_reason = ee_data["by_election_reason"]
×
UNCOV
524
            ballot.save()
×
525

526
    def set_organisation_type(self, ballot):
1✔
527
        if ballot.post.organization_type and not self.force_update:
×
528
            return
×
UNCOV
529
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
×
UNCOV
530
        if ee_data:
×
531
            ballot.post.organization_type = ee_data["organisation"][
×
532
                "organisation_type"
533
            ]
534
            ballot.post.save()
×
535

536
    def set_division_type(self, ballot):
1✔
537
        """
538
        Attempts to set the division_type field from EveryElection
539
        """
540
        if ballot.post.division_type and not self.force_update:
1✔
541
            return
1✔
542

543
        ee_data = self.ee_helper.get_data(ballot.ballot_paper_id)
1✔
544

545
        if not ee_data or not ee_data["division"]:
1✔
546
            return
1✔
547

548
        ballot.post.division_type = ee_data["division"].get("division_type")
1✔
549
        # ensures the division_type is valid, or will raise a ValidationError
550
        ballot.post.full_clean()
1✔
551
        ballot.post.save()
1✔
552

553
    def get_replacement_ballot(self, ballot_id):
1✔
UNCOV
554
        replacement_ballot = None
×
UNCOV
555
        ee_data = self.ee_helper.get_data(ballot_id)
×
UNCOV
556
        if ee_data:
×
UNCOV
557
            replacement_ballot_id = ee_data["replaced_by"]
×
558
            if replacement_ballot_id:
×
559
                with contextlib.suppress(PostElection.DoesNotExist):
×
560
                    replacement_ballot = PostElection.objects.get(
×
561
                        ballot_paper_id=replacement_ballot_id
562
                    )
563

564
        return replacement_ballot
×
565

566
    def attach_cancelled_ballot_info(self):
1✔
567
        # we need to do this as a post-process instead of in the manager
568
        # because if we're going to link 2 PostElection objects together
569
        # we need to be sure that both of them already exist in our DB
UNCOV
570
        cancelled_ballots = PostElection.objects.filter(cancelled=True)
×
UNCOV
571
        if self.current_only:
×
UNCOV
572
            cancelled_ballots = cancelled_ballots.filter(election__current=True)
×
UNCOV
573
        for cb in cancelled_ballots:
×
574
            cb.replaced_by = self.get_replacement_ballot(cb.ballot_paper_id)
×
575
            # Always get metadata, even if we might have it already.
576
            # This is because is self.force_update is False, it might not have
577
            # been imported already
578
            self.set_metadata(cb)
×
UNCOV
579
            cb.save()
×
580

581
    def check_for_ee_updates(self):
1✔
582
        print("checking for recently updated EE")
×
583
        elections_with_children = set()
×
UNCOV
584
        for election_id in self.ee_helper.iter_recently_modified_election_ids():
×
UNCOV
585
            print(election_id)
×
586
            if self.ee_helper.ee_cache[election_id]["group_type"]:
×
587
                # Only the ones with group_type == "organisation" match Election objects in WCIVF directly
588
                if (
×
589
                    self.ee_helper.ee_cache[election_id]["group_type"]
590
                    == "organisation"
591
                ):
592
                    election = Election.objects.get(slug=election_id)
×
UNCOV
593
                    print(
×
594
                        f"importing metadata from EE for Election: {election}"
595
                    )
596
                    self.election_importer.import_metadata_from_ee(election)
×
597
                # for those that any other group_type, we need to get the children and try to match an Election
598
                else:
UNCOV
599
                    elections_with_children.add(election_id)
×
600
            # if there is no group_type, the election_id is for a PostElection object, and possibly
601
            # an Election object as well as is the case with pcc.<region>.2024-05-02, for example
602
            # it's possible the Election update is covered in the previous else block,
603
            # but try here as well to be sure
604
            else:
UNCOV
605
                ballot = PostElection.objects.get(ballot_paper_id=election_id)
×
UNCOV
606
                print(f"importing metadata from EE for PostElection: {ballot}")
×
UNCOV
607
                self.import_metadata_from_ee(ballot)
×
UNCOV
608
                try:
×
609
                    election = Election.objects.get(slug=election_id)
×
610
                    print(
×
611
                        f"importing metadata from EE for Election: {election}"
612
                    )
613
                    self.election_importer.import_metadata_from_ee(election)
×
614
                except Election.DoesNotExist:
×
UNCOV
615
                    print(f"Election {election_id} not found in WCIVF")
×
UNCOV
616
                    continue
×
617
            for election_id in elections_with_children:
×
618
                try:
×
619
                    election = Election.objects.get(slug=election_id)
×
620
                    print(
×
621
                        f"importing metadata from EE for Election: {election}"
622
                    )
623
                    self.election_importer.import_metadata_from_ee(election)
×
624
                except Election.DoesNotExist:
×
UNCOV
625
                    print(f"Election {election_id} not found in WCIVF")
×
UNCOV
626
                    continue
×
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

© 2025 Coveralls, Inc