civics-cdf-validator 1.60.dev1__tar.gz → 1.60.dev3__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.dev1/civics_cdf_validator.egg-info → civics_cdf_validator-1.60.dev3}/PKG-INFO +1 -1
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/rules.py +125 -5
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/version.py +1 -1
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/CONTRIBUTING.md +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/LICENSE-2.0.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/MANIFEST.in +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/README.md +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/__init__.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/base.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/SOURCES.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/requires.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/civics_cdf_validator.egg-info/top_level.txt +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/gpunit_rules.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/loggers.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/office_utils.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/setup.cfg +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/setup.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/stats.py +0 -0
- {civics_cdf_validator-1.60.dev1 → civics_cdf_validator-1.60.dev3}/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:
|
|
@@ -1714,6 +1719,65 @@ class PersonsMissingPartyData(base.BaseRule):
|
|
|
1714
1719
|
)
|
|
1715
1720
|
|
|
1716
1721
|
|
|
1722
|
+
class OnlyOneCandidateImagePerPerson(base.BaseRule):
|
|
1723
|
+
"""Ensure only one candidate-image is provided per Person."""
|
|
1724
|
+
|
|
1725
|
+
def elements(self):
|
|
1726
|
+
return ["Person"]
|
|
1727
|
+
|
|
1728
|
+
def check(self, element):
|
|
1729
|
+
image_uris = element.findall("ImageUri")
|
|
1730
|
+
candidate_images = []
|
|
1731
|
+
for image_uri in image_uris:
|
|
1732
|
+
annotation = image_uri.get("Annotation", "").strip()
|
|
1733
|
+
if annotation == "candidate-image":
|
|
1734
|
+
candidate_images.append(image_uri)
|
|
1735
|
+
|
|
1736
|
+
if len(candidate_images) > 1:
|
|
1737
|
+
raise loggers.ElectionError.from_message(
|
|
1738
|
+
"Person has {} ImageUri fields annotated as 'candidate-image'."
|
|
1739
|
+
" Must have at most one.".format(len(candidate_images)),
|
|
1740
|
+
candidate_images,
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
class UniqueCandidateImageUris(base.TreeRule):
|
|
1745
|
+
"""Check that candidate-image URIs are unique to a person."""
|
|
1746
|
+
|
|
1747
|
+
def check(self):
|
|
1748
|
+
root = self.election_tree.getroot()
|
|
1749
|
+
if root is None:
|
|
1750
|
+
return
|
|
1751
|
+
|
|
1752
|
+
persons_by_candidate_image_uri = collections.defaultdict(list)
|
|
1753
|
+
person_collection = root.find("PersonCollection")
|
|
1754
|
+
if person_collection is None:
|
|
1755
|
+
return
|
|
1756
|
+
for person in person_collection.findall("Person"):
|
|
1757
|
+
for image_uri_element in person.findall("ImageUri"):
|
|
1758
|
+
if not element_has_text(image_uri_element):
|
|
1759
|
+
continue
|
|
1760
|
+
annotation = image_uri_element.get("Annotation", "").strip()
|
|
1761
|
+
if annotation == "candidate-image":
|
|
1762
|
+
image_uri = image_uri_element.text.strip()
|
|
1763
|
+
if image_uri:
|
|
1764
|
+
persons_by_candidate_image_uri[image_uri].append(person)
|
|
1765
|
+
|
|
1766
|
+
error_log = []
|
|
1767
|
+
for image_uri, persons in persons_by_candidate_image_uri.items():
|
|
1768
|
+
if len(persons) > 1:
|
|
1769
|
+
person_ids = sorted(person.get("objectId") for person in persons)
|
|
1770
|
+
error_log.append(
|
|
1771
|
+
loggers.LogEntry(
|
|
1772
|
+
f"Candidate image URI '{image_uri}' is shared by multiple"
|
|
1773
|
+
f" people: [{', '.join(person_ids)}].",
|
|
1774
|
+
persons,
|
|
1775
|
+
)
|
|
1776
|
+
)
|
|
1777
|
+
if error_log:
|
|
1778
|
+
raise loggers.ElectionError(error_log)
|
|
1779
|
+
|
|
1780
|
+
|
|
1717
1781
|
class AllCaps(base.BaseRule):
|
|
1718
1782
|
"""Name elements should not be in all uppercase.
|
|
1719
1783
|
|
|
@@ -2364,11 +2428,8 @@ class ValidURIAnnotation(base.BaseRule):
|
|
|
2364
2428
|
"URI {} is missing annotation.".format(ascii_url), [uri]
|
|
2365
2429
|
)
|
|
2366
2430
|
|
|
2367
|
-
# Skip platform checks for
|
|
2368
|
-
if
|
|
2369
|
-
re.search(r"candidate-image", annotation)
|
|
2370
|
-
or annotation == "office-contact_form"
|
|
2371
|
-
):
|
|
2431
|
+
# Skip platform checks for office contact form annotations.
|
|
2432
|
+
if annotation == "office-contact_form":
|
|
2372
2433
|
continue
|
|
2373
2434
|
|
|
2374
2435
|
ann_elements = annotation.split("-")
|
|
@@ -3491,6 +3552,62 @@ class ImproperCandidateContest(base.TreeRule):
|
|
|
3491
3552
|
raise loggers.ElectionWarning(warning_log)
|
|
3492
3553
|
|
|
3493
3554
|
|
|
3555
|
+
class WinnerCountLimit(base.BaseRule):
|
|
3556
|
+
"""Number of winners must be less than or equal to NumberElected."""
|
|
3557
|
+
|
|
3558
|
+
def setup(self):
|
|
3559
|
+
self.candidate_post_election_status_by_object_id = (
|
|
3560
|
+
self._get_candidate_post_election_status_by_object_id()
|
|
3561
|
+
)
|
|
3562
|
+
|
|
3563
|
+
def _get_candidate_post_election_status_by_object_id(self):
|
|
3564
|
+
candidate_post_election_status_by_object_id = {}
|
|
3565
|
+
candidates = self.get_elements_by_class(
|
|
3566
|
+
self.election_tree, "CandidateCollection//Candidate"
|
|
3567
|
+
)
|
|
3568
|
+
for candidate in candidates:
|
|
3569
|
+
object_id = candidate.get("objectId")
|
|
3570
|
+
post_election_status = candidate.find("PostElectionStatus")
|
|
3571
|
+
if object_id and element_has_text(post_election_status):
|
|
3572
|
+
candidate_post_election_status_by_object_id[object_id] = (
|
|
3573
|
+
post_election_status.text.strip()
|
|
3574
|
+
)
|
|
3575
|
+
return candidate_post_election_status_by_object_id
|
|
3576
|
+
|
|
3577
|
+
def elements(self):
|
|
3578
|
+
return ["CandidateContest"]
|
|
3579
|
+
|
|
3580
|
+
def check(self, element):
|
|
3581
|
+
number_elected_element = element.find("NumberElected")
|
|
3582
|
+
number_elected = (
|
|
3583
|
+
int(number_elected_element.text)
|
|
3584
|
+
if element_has_text(number_elected_element)
|
|
3585
|
+
else 1
|
|
3586
|
+
)
|
|
3587
|
+
|
|
3588
|
+
candidate_ids_elements = element.findall("BallotSelection//CandidateIds")
|
|
3589
|
+
candidate_ids = set()
|
|
3590
|
+
for candidate_ids_element in candidate_ids_elements:
|
|
3591
|
+
if element_has_text(candidate_ids_element):
|
|
3592
|
+
candidate_ids.update(candidate_ids_element.text.split())
|
|
3593
|
+
|
|
3594
|
+
winner_count = 0
|
|
3595
|
+
for candidate_id in candidate_ids:
|
|
3596
|
+
status = self.candidate_post_election_status_by_object_id.get(
|
|
3597
|
+
candidate_id
|
|
3598
|
+
)
|
|
3599
|
+
if status in _WINNER_POST_ELECTION_STATUSES:
|
|
3600
|
+
winner_count += 1
|
|
3601
|
+
|
|
3602
|
+
if winner_count > number_elected:
|
|
3603
|
+
raise loggers.ElectionError.from_message(
|
|
3604
|
+
f"Contest {element.get('objectId')} has {winner_count} candidates"
|
|
3605
|
+
" with PostElectionStatus of 'winner' or 'projected-winner',"
|
|
3606
|
+
f" which exceeds NumberElected: {number_elected}.",
|
|
3607
|
+
[element],
|
|
3608
|
+
)
|
|
3609
|
+
|
|
3610
|
+
|
|
3494
3611
|
class MissingFieldsError(base.MissingFieldRule):
|
|
3495
3612
|
"""Check for missing fields for given entity types and field names.
|
|
3496
3613
|
|
|
@@ -4811,6 +4928,7 @@ COMMON_RULES = (
|
|
|
4811
4928
|
OfficesHaveJurisdictionID,
|
|
4812
4929
|
OfficesHaveValidOfficeLevel,
|
|
4813
4930
|
OfficesHaveValidOfficeRole,
|
|
4931
|
+
OnlyOneCandidateImagePerPerson,
|
|
4814
4932
|
OptionalAndEmpty,
|
|
4815
4933
|
OtherType,
|
|
4816
4934
|
PartyLeadershipMustExist,
|
|
@@ -4820,6 +4938,7 @@ COMMON_RULES = (
|
|
|
4820
4938
|
PersonsMissingPartyData,
|
|
4821
4939
|
Schema,
|
|
4822
4940
|
URIValidator,
|
|
4941
|
+
UniqueCandidateImageUris,
|
|
4823
4942
|
UniqueLabel,
|
|
4824
4943
|
UniqueStableID,
|
|
4825
4944
|
UniqueURIPerAnnotationCategory,
|
|
@@ -4882,6 +5001,7 @@ ELECTION_RULES = COMMON_RULES + (
|
|
|
4882
5001
|
ValidateInfoUriAnnotation,
|
|
4883
5002
|
VoteCountTypesCoherency,
|
|
4884
5003
|
VoteCountValidSeatsDeltaTypes,
|
|
5004
|
+
WinnerCountLimit,
|
|
4885
5005
|
# go/keep-sorted end
|
|
4886
5006
|
)
|
|
4887
5007
|
|
|
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
|