civics-cdf-validator 1.60.dev2__tar.gz → 1.60.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {civics_cdf_validator-1.60.dev2/civics_cdf_validator.egg-info → civics_cdf_validator-1.60.dev4}/PKG-INFO +1 -1
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/rules.py +111 -21
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/version.py +1 -1
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/CONTRIBUTING.md +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/LICENSE-2.0.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/MANIFEST.in +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/README.md +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/__init__.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/base.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/civics_cdf_validator.egg-info/SOURCES.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/civics_cdf_validator.egg-info/requires.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/civics_cdf_validator.egg-info/top_level.txt +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/gpunit_rules.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/loggers.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/office_utils.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/setup.cfg +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/setup.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/stats.py +0 -0
- {civics_cdf_validator-1.60.dev2 → civics_cdf_validator-1.60.dev4}/validator.py +0 -0
|
@@ -132,6 +132,11 @@ _UNTYPED_URI_PLATFORMS = frozenset([
|
|
|
132
132
|
])
|
|
133
133
|
_ALL_URI_PLATFORMS = _TYPED_URI_PLATFORMS | _UNTYPED_URI_PLATFORMS
|
|
134
134
|
|
|
135
|
+
_WINNER_POST_ELECTION_STATUSES = frozenset([
|
|
136
|
+
"winner",
|
|
137
|
+
"projected-winner",
|
|
138
|
+
])
|
|
139
|
+
|
|
135
140
|
|
|
136
141
|
def _get_office_roles(element, is_post_office_split_feed=False):
|
|
137
142
|
if is_post_office_split_feed:
|
|
@@ -1360,42 +1365,70 @@ class ValidateDuplicateColors(base.TreeRule):
|
|
|
1360
1365
|
"""
|
|
1361
1366
|
|
|
1362
1367
|
def check(self):
|
|
1363
|
-
|
|
1368
|
+
party_colors_by_id = {}
|
|
1369
|
+
party_objects = {}
|
|
1364
1370
|
for party in self.get_elements_by_class(self.election_tree, "Party"):
|
|
1371
|
+
party_id = party.get("objectId")
|
|
1372
|
+
party_objects[party_id] = party
|
|
1373
|
+
party_colors_by_id[party_id] = {}
|
|
1365
1374
|
color_element = party.find("Color")
|
|
1366
|
-
if color_element
|
|
1367
|
-
|
|
1368
|
-
|
|
1375
|
+
if element_has_text(color_element):
|
|
1376
|
+
party_colors_by_id[party_id]["Color"] = color_element.text.lower()
|
|
1377
|
+
colors_element = party.find("Colors")
|
|
1378
|
+
if colors_element is not None:
|
|
1379
|
+
for color_field in ("DarkThemeColor", "LightThemeColor"):
|
|
1380
|
+
sub_color_element = colors_element.find(color_field)
|
|
1381
|
+
if element_has_text(sub_color_element):
|
|
1382
|
+
party_colors_by_id[party_id][
|
|
1383
|
+
color_field
|
|
1384
|
+
] = sub_color_element.text.lower()
|
|
1369
1385
|
|
|
1370
1386
|
warning_log = []
|
|
1371
1387
|
for party_contest in self.get_elements_by_class(
|
|
1372
1388
|
element=self.election_tree, element_name="PartyContest"
|
|
1373
1389
|
):
|
|
1374
|
-
contest_colors = {
|
|
1390
|
+
contest_colors = {
|
|
1391
|
+
"Color": collections.defaultdict(list),
|
|
1392
|
+
"DarkThemeColor": collections.defaultdict(list),
|
|
1393
|
+
"LightThemeColor": collections.defaultdict(list),
|
|
1394
|
+
}
|
|
1375
1395
|
for party_ids_element in self.get_elements_by_class(
|
|
1376
1396
|
element=party_contest, element_name="PartyIds"
|
|
1377
1397
|
):
|
|
1378
1398
|
for party_id in party_ids_element.text.split():
|
|
1379
|
-
if party_id not in
|
|
1399
|
+
if party_id not in party_colors_by_id:
|
|
1400
|
+
continue
|
|
1401
|
+
party_colors = party_colors_by_id[party_id]
|
|
1402
|
+
party_element = party_objects[party_id]
|
|
1403
|
+
|
|
1404
|
+
has_party_colors = "Color" in party_colors or (
|
|
1405
|
+
"DarkThemeColor" in party_colors
|
|
1406
|
+
and "LightThemeColor" in party_colors
|
|
1407
|
+
)
|
|
1408
|
+
if not has_party_colors:
|
|
1380
1409
|
warning_log.append(
|
|
1381
1410
|
loggers.LogEntry(
|
|
1382
|
-
"Party (
|
|
1383
|
-
|
|
1411
|
+
f"Party ({party_id}) in PartyContest should have either"
|
|
1412
|
+
" Color or Colors.DarkThemeColor and Colors.LightThemeColor"
|
|
1413
|
+
" set.",
|
|
1414
|
+
[party_element],
|
|
1415
|
+
)
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
for color_field in ("Color", "DarkThemeColor", "LightThemeColor"):
|
|
1419
|
+
if color_field in party_colors:
|
|
1420
|
+
color_val = party_colors[color_field]
|
|
1421
|
+
contest_colors[color_field][color_val].append(party_element)
|
|
1422
|
+
|
|
1423
|
+
for color_field in ("Color", "DarkThemeColor", "LightThemeColor"):
|
|
1424
|
+
for color, parties in contest_colors[color_field].items():
|
|
1425
|
+
if len(parties) > 1:
|
|
1426
|
+
warning_log.append(
|
|
1427
|
+
loggers.LogEntry(
|
|
1428
|
+
f"Parties have the same {color_field} {color}.",
|
|
1429
|
+
parties,
|
|
1384
1430
|
)
|
|
1385
1431
|
)
|
|
1386
|
-
continue
|
|
1387
|
-
party_color = party_color_mapping[party_id][0]
|
|
1388
|
-
if party_color in contest_colors:
|
|
1389
|
-
contest_colors[party_color].append(party_color_mapping[party_id][1])
|
|
1390
|
-
else:
|
|
1391
|
-
contest_colors[party_color] = [party_color_mapping[party_id][1]]
|
|
1392
|
-
for color, parties in contest_colors.items():
|
|
1393
|
-
if len(parties) > 1:
|
|
1394
|
-
warning_log.append(
|
|
1395
|
-
loggers.LogEntry(
|
|
1396
|
-
"Parties have the same color %s." % color, parties
|
|
1397
|
-
)
|
|
1398
|
-
)
|
|
1399
1432
|
if warning_log:
|
|
1400
1433
|
raise loggers.ElectionWarning(warning_log)
|
|
1401
1434
|
|
|
@@ -3547,6 +3580,62 @@ class ImproperCandidateContest(base.TreeRule):
|
|
|
3547
3580
|
raise loggers.ElectionWarning(warning_log)
|
|
3548
3581
|
|
|
3549
3582
|
|
|
3583
|
+
class WinnerCountLimit(base.BaseRule):
|
|
3584
|
+
"""Number of winners must be less than or equal to NumberElected."""
|
|
3585
|
+
|
|
3586
|
+
def setup(self):
|
|
3587
|
+
self.candidate_post_election_status_by_object_id = (
|
|
3588
|
+
self._get_candidate_post_election_status_by_object_id()
|
|
3589
|
+
)
|
|
3590
|
+
|
|
3591
|
+
def _get_candidate_post_election_status_by_object_id(self):
|
|
3592
|
+
candidate_post_election_status_by_object_id = {}
|
|
3593
|
+
candidates = self.get_elements_by_class(
|
|
3594
|
+
self.election_tree, "CandidateCollection//Candidate"
|
|
3595
|
+
)
|
|
3596
|
+
for candidate in candidates:
|
|
3597
|
+
object_id = candidate.get("objectId")
|
|
3598
|
+
post_election_status = candidate.find("PostElectionStatus")
|
|
3599
|
+
if object_id and element_has_text(post_election_status):
|
|
3600
|
+
candidate_post_election_status_by_object_id[object_id] = (
|
|
3601
|
+
post_election_status.text.strip()
|
|
3602
|
+
)
|
|
3603
|
+
return candidate_post_election_status_by_object_id
|
|
3604
|
+
|
|
3605
|
+
def elements(self):
|
|
3606
|
+
return ["CandidateContest"]
|
|
3607
|
+
|
|
3608
|
+
def check(self, element):
|
|
3609
|
+
number_elected_element = element.find("NumberElected")
|
|
3610
|
+
number_elected = (
|
|
3611
|
+
int(number_elected_element.text)
|
|
3612
|
+
if element_has_text(number_elected_element)
|
|
3613
|
+
else 1
|
|
3614
|
+
)
|
|
3615
|
+
|
|
3616
|
+
candidate_ids_elements = element.findall("BallotSelection//CandidateIds")
|
|
3617
|
+
candidate_ids = set()
|
|
3618
|
+
for candidate_ids_element in candidate_ids_elements:
|
|
3619
|
+
if element_has_text(candidate_ids_element):
|
|
3620
|
+
candidate_ids.update(candidate_ids_element.text.split())
|
|
3621
|
+
|
|
3622
|
+
winner_count = 0
|
|
3623
|
+
for candidate_id in candidate_ids:
|
|
3624
|
+
status = self.candidate_post_election_status_by_object_id.get(
|
|
3625
|
+
candidate_id
|
|
3626
|
+
)
|
|
3627
|
+
if status in _WINNER_POST_ELECTION_STATUSES:
|
|
3628
|
+
winner_count += 1
|
|
3629
|
+
|
|
3630
|
+
if winner_count > number_elected:
|
|
3631
|
+
raise loggers.ElectionError.from_message(
|
|
3632
|
+
f"Contest {element.get('objectId')} has {winner_count} candidates"
|
|
3633
|
+
" with PostElectionStatus of 'winner' or 'projected-winner',"
|
|
3634
|
+
f" which exceeds NumberElected: {number_elected}.",
|
|
3635
|
+
[element],
|
|
3636
|
+
)
|
|
3637
|
+
|
|
3638
|
+
|
|
3550
3639
|
class MissingFieldsError(base.MissingFieldRule):
|
|
3551
3640
|
"""Check for missing fields for given entity types and field names.
|
|
3552
3641
|
|
|
@@ -4940,6 +5029,7 @@ ELECTION_RULES = COMMON_RULES + (
|
|
|
4940
5029
|
ValidateInfoUriAnnotation,
|
|
4941
5030
|
VoteCountTypesCoherency,
|
|
4942
5031
|
VoteCountValidSeatsDeltaTypes,
|
|
5032
|
+
WinnerCountLimit,
|
|
4943
5033
|
# go/keep-sorted end
|
|
4944
5034
|
)
|
|
4945
5035
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|