civics-cdf-validator 1.67.dev2__tar.gz → 1.67.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.67.dev2/civics_cdf_validator.egg-info → civics_cdf_validator-1.67.dev3}/PKG-INFO +1 -1
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3/civics_cdf_validator.egg-info}/PKG-INFO +1 -1
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/civics_cdf_validator.egg-info/SOURCES.txt +1 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/rules.py +2 -460
- civics_cdf_validator-1.67.dev3/rulesets/officeholder_rules.py +484 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/version.py +1 -1
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/CONTRIBUTING.md +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/LICENSE-2.0.txt +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/MANIFEST.in +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/README.md +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/__init__.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/base.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/civics_cdf_validator.egg-info/dependency_links.txt +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/civics_cdf_validator.egg-info/entry_points.txt +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/civics_cdf_validator.egg-info/requires.txt +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/civics_cdf_validator.egg-info/top_level.txt +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/gpunit_rules.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/loggers.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/office_utils.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/rulesets/__init__.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/rulesets/common_rules.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/rulesets/rule_util.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/setup.cfg +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/setup.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/stats.py +0 -0
- {civics_cdf_validator-1.67.dev2 → civics_cdf_validator-1.67.dev3}/validator.py +0 -0
|
@@ -18,7 +18,6 @@ from __future__ import print_function
|
|
|
18
18
|
import collections
|
|
19
19
|
import datetime
|
|
20
20
|
import enum
|
|
21
|
-
import hashlib
|
|
22
21
|
import re
|
|
23
22
|
|
|
24
23
|
from botocore import utils as botocore_utils
|
|
@@ -26,6 +25,7 @@ from civics_cdf_validator import base
|
|
|
26
25
|
from civics_cdf_validator import gpunit_rules
|
|
27
26
|
from civics_cdf_validator import loggers
|
|
28
27
|
from civics_cdf_validator.rulesets import common_rules
|
|
28
|
+
from civics_cdf_validator.rulesets import officeholder_rules
|
|
29
29
|
from civics_cdf_validator.rulesets import rule_util
|
|
30
30
|
import dateutil
|
|
31
31
|
from frozendict import frozendict
|
|
@@ -962,63 +962,6 @@ class ContestHasMultipleOffices(base.BaseRule):
|
|
|
962
962
|
)
|
|
963
963
|
|
|
964
964
|
|
|
965
|
-
class PersonHasOffice(base.ValidReferenceRule):
|
|
966
|
-
"""Ensure that each non-party-leader Person object linked to one Office."""
|
|
967
|
-
|
|
968
|
-
def _gather_reference_values(self):
|
|
969
|
-
root = self.election_tree.getroot()
|
|
970
|
-
|
|
971
|
-
person_ids = set()
|
|
972
|
-
person_collection = root.find("PersonCollection")
|
|
973
|
-
if person_collection is not None:
|
|
974
|
-
person_ids = {p.attrib["objectId"] for p in person_collection}
|
|
975
|
-
return person_ids
|
|
976
|
-
|
|
977
|
-
def _gather_defined_values(self):
|
|
978
|
-
root = self.election_tree.getroot()
|
|
979
|
-
|
|
980
|
-
person_reference_ids = set()
|
|
981
|
-
# Add party leaders provided in the External Identifier
|
|
982
|
-
for external_id in root.findall(".//Party//ExternalIdentifier"):
|
|
983
|
-
other_type = external_id.find("OtherType")
|
|
984
|
-
if (
|
|
985
|
-
other_type is not None
|
|
986
|
-
and other_type.text in rule_util.PARTY_LEADERSHIP_TYPES
|
|
987
|
-
):
|
|
988
|
-
person_reference_ids.add(external_id.find("Value").text)
|
|
989
|
-
# Add party leaders provided in the Leadership entity
|
|
990
|
-
for leader_id in root.findall(".//Party//PartyLeaderId"):
|
|
991
|
-
if leader_id.text:
|
|
992
|
-
person_reference_ids.add(leader_id.text)
|
|
993
|
-
|
|
994
|
-
office_holder_tenure_collection = root.find("OfficeHolderTenureCollection")
|
|
995
|
-
if office_holder_tenure_collection is not None:
|
|
996
|
-
for office_holder_tenure in office_holder_tenure_collection.findall(
|
|
997
|
-
"OfficeHolderTenure"
|
|
998
|
-
):
|
|
999
|
-
id_obj = office_holder_tenure.find("OfficeHolderPersonId")
|
|
1000
|
-
if id_obj is not None and id_obj.text:
|
|
1001
|
-
person_reference_ids.add(id_obj.text.strip())
|
|
1002
|
-
|
|
1003
|
-
office_collection = root.find("OfficeCollection")
|
|
1004
|
-
if (
|
|
1005
|
-
office_holder_tenure_collection is None
|
|
1006
|
-
and office_collection is not None
|
|
1007
|
-
):
|
|
1008
|
-
for office in office_collection.findall("Office"):
|
|
1009
|
-
id_obj = office.find("OfficeHolderPersonIds")
|
|
1010
|
-
if id_obj is not None and id_obj.text:
|
|
1011
|
-
ids = id_obj.text.strip().split()
|
|
1012
|
-
if len(ids) > 1:
|
|
1013
|
-
msg = "Office has {} OfficeHolders. Must have exactly one.".format(
|
|
1014
|
-
str(len(ids))
|
|
1015
|
-
)
|
|
1016
|
-
raise loggers.ElectionError.from_message(msg, [office])
|
|
1017
|
-
person_reference_ids.update(ids)
|
|
1018
|
-
|
|
1019
|
-
return person_reference_ids
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
965
|
class VoteCountTypesCoherency(base.BaseRule):
|
|
1023
966
|
"""Ensure VoteCount types describe the appropriate votable."""
|
|
1024
967
|
|
|
@@ -1136,24 +1079,6 @@ class ContestHasValidContestStage(base.BaseRule):
|
|
|
1136
1079
|
)
|
|
1137
1080
|
|
|
1138
1081
|
|
|
1139
|
-
class DateOfBirthIsInPast(base.DateRule):
|
|
1140
|
-
"""Date of Birth should not be in the future."""
|
|
1141
|
-
|
|
1142
|
-
def elements(self):
|
|
1143
|
-
return ["PersonCollection"]
|
|
1144
|
-
|
|
1145
|
-
def check(self, element):
|
|
1146
|
-
for person_element in element:
|
|
1147
|
-
date_of_birthday = person_element.find("DateOfBirth")
|
|
1148
|
-
if date_of_birthday is not None and date_of_birthday.text:
|
|
1149
|
-
date_of_birthday = base.PartialDate.init_partial_date(
|
|
1150
|
-
date_of_birthday.text
|
|
1151
|
-
)
|
|
1152
|
-
self.check_for_date_in_past(date_of_birthday, person_element)
|
|
1153
|
-
if self.error_log:
|
|
1154
|
-
raise loggers.ElectionError(self.error_log)
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
1082
|
class ElectionContainsStartAndEndDates(base.DateRule):
|
|
1158
1083
|
"""Election elements should have start and end dates populated."""
|
|
1159
1084
|
|
|
@@ -1468,349 +1393,6 @@ class DateStatusMatches(base.DateRule):
|
|
|
1468
1393
|
raise loggers.ElectionInfo.from_message(msg, [election_elem])
|
|
1469
1394
|
|
|
1470
1395
|
|
|
1471
|
-
class OfficeSelectionMethodMatch(base.BaseRule):
|
|
1472
|
-
"""Office and OfficeHolderTenure need to have matching selection methods.
|
|
1473
|
-
|
|
1474
|
-
Ensure that the OfficeSelectionMethod of a given OfficeHolderTenure is
|
|
1475
|
-
also
|
|
1476
|
-
present in the list of SelectionMethods of the Office it points to.
|
|
1477
|
-
"""
|
|
1478
|
-
|
|
1479
|
-
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
1480
|
-
self.office_selection_methods = {}
|
|
1481
|
-
officeholder_tenure_elements = self.get_elements_by_class(
|
|
1482
|
-
election_tree, "OfficeHolderTenure"
|
|
1483
|
-
)
|
|
1484
|
-
office_elements = self.get_elements_by_class(election_tree, "Office")
|
|
1485
|
-
if officeholder_tenure_elements:
|
|
1486
|
-
for element in office_elements:
|
|
1487
|
-
office_id = element.get("objectId")
|
|
1488
|
-
selection_methods = {
|
|
1489
|
-
selection_method.text
|
|
1490
|
-
for selection_method in element.findall("SelectionMethod")
|
|
1491
|
-
}
|
|
1492
|
-
self.office_selection_methods[office_id] = selection_methods
|
|
1493
|
-
|
|
1494
|
-
def elements(self):
|
|
1495
|
-
return ["OfficeHolderTenure"]
|
|
1496
|
-
|
|
1497
|
-
def check(self, element):
|
|
1498
|
-
office_id = element.find("OfficeId").text
|
|
1499
|
-
office_selection_method = element.find("OfficeSelectionMethod").text
|
|
1500
|
-
if (
|
|
1501
|
-
office_id not in self.office_selection_methods
|
|
1502
|
-
or office_selection_method
|
|
1503
|
-
not in self.office_selection_methods[office_id]
|
|
1504
|
-
):
|
|
1505
|
-
raise loggers.ElectionError.from_message(
|
|
1506
|
-
"OfficeSelectionMethod does not have a matching SelectionMethod"
|
|
1507
|
-
" in the corresponding Office element.",
|
|
1508
|
-
[element],
|
|
1509
|
-
)
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
class OfficeHolderTenureTermDates(base.DateRule):
|
|
1513
|
-
"""OfficeHolderTenure elements should contain valid term dates."""
|
|
1514
|
-
|
|
1515
|
-
def elements(self):
|
|
1516
|
-
return ["OfficeHolderTenure"]
|
|
1517
|
-
|
|
1518
|
-
def check(self, element):
|
|
1519
|
-
start_date = element.find("StartDate")
|
|
1520
|
-
end_date = element.find("EndDate")
|
|
1521
|
-
if rule_util.element_has_text(start_date) and rule_util.element_has_text(
|
|
1522
|
-
end_date
|
|
1523
|
-
):
|
|
1524
|
-
start_date_obj = base.PartialDate.init_partial_date(start_date.text)
|
|
1525
|
-
end_date_obj = base.PartialDate.init_partial_date(end_date.text)
|
|
1526
|
-
if end_date_obj < start_date_obj:
|
|
1527
|
-
raise loggers.ElectionError.from_message(
|
|
1528
|
-
"OfficeHolderTenure element has an EndDate that is before the"
|
|
1529
|
-
" StartDate.",
|
|
1530
|
-
[element],
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
class OfficeTermDates(base.DateRule):
|
|
1535
|
-
"""Office elements should contain valid term dates.
|
|
1536
|
-
|
|
1537
|
-
Offices with OfficeHolderPersonIds should have a Term declared. Given
|
|
1538
|
-
term should have a start date. If term also has an end date then end date
|
|
1539
|
-
should come after start date. Post Office split feed office objects should
|
|
1540
|
-
not have a Term element.
|
|
1541
|
-
"""
|
|
1542
|
-
|
|
1543
|
-
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
1544
|
-
self.is_post_office_split_feed = False
|
|
1545
|
-
officeholder_tenure_collection_element = self.get_elements_by_class(
|
|
1546
|
-
election_tree, "OfficeHolderTenureCollection"
|
|
1547
|
-
)
|
|
1548
|
-
if officeholder_tenure_collection_element:
|
|
1549
|
-
self.is_post_office_split_feed = True
|
|
1550
|
-
|
|
1551
|
-
def elements(self):
|
|
1552
|
-
return ["Office"]
|
|
1553
|
-
|
|
1554
|
-
def check(self, element):
|
|
1555
|
-
self.reset_instance_vars()
|
|
1556
|
-
if self.is_post_office_split_feed:
|
|
1557
|
-
term = element.find("Term")
|
|
1558
|
-
if term is not None:
|
|
1559
|
-
raise loggers.ElectionError.from_message(
|
|
1560
|
-
"Office should not contain Term data in post Office split feed.",
|
|
1561
|
-
[element],
|
|
1562
|
-
)
|
|
1563
|
-
if self.error_log:
|
|
1564
|
-
raise loggers.ElectionError(self.error_log)
|
|
1565
|
-
else:
|
|
1566
|
-
off_per_id = element.find("OfficeHolderPersonIds")
|
|
1567
|
-
if rule_util.element_has_text(off_per_id):
|
|
1568
|
-
term = element.find("Term")
|
|
1569
|
-
if term is None:
|
|
1570
|
-
raise loggers.ElectionWarning.from_message(
|
|
1571
|
-
"The Office is missing a Term.", [element]
|
|
1572
|
-
)
|
|
1573
|
-
|
|
1574
|
-
self.gather_dates(term)
|
|
1575
|
-
if self.start_date is None:
|
|
1576
|
-
raise loggers.ElectionWarning.from_message(
|
|
1577
|
-
"The Office is missing a Term > StartDate.", [element]
|
|
1578
|
-
)
|
|
1579
|
-
elif self.end_date is not None:
|
|
1580
|
-
self.check_end_after_start()
|
|
1581
|
-
|
|
1582
|
-
if self.error_log:
|
|
1583
|
-
raise loggers.ElectionError(self.error_log)
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
class RemovePersonAndOfficeHolderId60DaysAfterEndDate(base.TreeRule):
|
|
1587
|
-
"""Office elements should contain valid term dates.
|
|
1588
|
-
|
|
1589
|
-
Check if 60 days after the specified office term EndDate,
|
|
1590
|
-
the associated Person and OfficeholderId may be removed from the feed.
|
|
1591
|
-
However, if the Person is elected to another office,
|
|
1592
|
-
they will be kept in the feed with the same stable ID.
|
|
1593
|
-
"""
|
|
1594
|
-
|
|
1595
|
-
def check(self):
|
|
1596
|
-
info_log = []
|
|
1597
|
-
persons = self.get_elements_by_class(self.election_tree, "Person")
|
|
1598
|
-
offices = self.get_elements_by_class(self.election_tree, "Office")
|
|
1599
|
-
officeholder_tenure_collection = self.get_elements_by_class(
|
|
1600
|
-
self.election_tree, "OfficeHolderTenureCollection"
|
|
1601
|
-
)
|
|
1602
|
-
is_post_office_split_feed = bool(officeholder_tenure_collection)
|
|
1603
|
-
person_office_dict = dict()
|
|
1604
|
-
outdated_offices = []
|
|
1605
|
-
outdated_officeholder_tenures = []
|
|
1606
|
-
outdated_officetenure_persons = dict()
|
|
1607
|
-
if is_post_office_split_feed:
|
|
1608
|
-
officeholder_tenures = self.get_elements_by_class(
|
|
1609
|
-
self.election_tree, "OfficeHolderTenure"
|
|
1610
|
-
)
|
|
1611
|
-
for officeholder_tenure in officeholder_tenures:
|
|
1612
|
-
office_holder_person_elem = officeholder_tenure.find(
|
|
1613
|
-
"OfficeHolderPersonId"
|
|
1614
|
-
)
|
|
1615
|
-
person_id = office_holder_person_elem.text
|
|
1616
|
-
date_validator = base.DateRule(None, None)
|
|
1617
|
-
date_validator.gather_dates(officeholder_tenure)
|
|
1618
|
-
end_date = date_validator.end_date
|
|
1619
|
-
if end_date is not None:
|
|
1620
|
-
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
1621
|
-
days=-60
|
|
1622
|
-
)
|
|
1623
|
-
partial_date_sixty_days = base.PartialDate(
|
|
1624
|
-
sixty_days_earlier.year,
|
|
1625
|
-
sixty_days_earlier.month,
|
|
1626
|
-
sixty_days_earlier.day,
|
|
1627
|
-
)
|
|
1628
|
-
if end_date < partial_date_sixty_days:
|
|
1629
|
-
outdated_officeholder_tenures.append(officeholder_tenure)
|
|
1630
|
-
if person_id in outdated_officetenure_persons.keys():
|
|
1631
|
-
outdated_officetenure_persons[person_id].append(
|
|
1632
|
-
(office_holder_person_elem, end_date)
|
|
1633
|
-
)
|
|
1634
|
-
else:
|
|
1635
|
-
outdated_officetenure_persons[person_id] = [
|
|
1636
|
-
(office_holder_person_elem, end_date)
|
|
1637
|
-
]
|
|
1638
|
-
for outdated_officeholder_tenure in outdated_officeholder_tenures:
|
|
1639
|
-
info_message = (
|
|
1640
|
-
"The officeholder tenure end date is more than 60 days"
|
|
1641
|
-
" in the past; this OfficeHolderTenure element can be removed"
|
|
1642
|
-
" from the feed."
|
|
1643
|
-
)
|
|
1644
|
-
info_log.append(
|
|
1645
|
-
loggers.LogEntry(info_message, [outdated_officeholder_tenure])
|
|
1646
|
-
)
|
|
1647
|
-
for person_id, value_list in outdated_officetenure_persons.items():
|
|
1648
|
-
has_recent_tenure = False
|
|
1649
|
-
office_holder_person_elem = None
|
|
1650
|
-
for value in value_list:
|
|
1651
|
-
office_holder_person_elem = value[0]
|
|
1652
|
-
end_date = value[1]
|
|
1653
|
-
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
1654
|
-
days=-60
|
|
1655
|
-
)
|
|
1656
|
-
partial_date_sixty_days = base.PartialDate(
|
|
1657
|
-
sixty_days_earlier.year,
|
|
1658
|
-
sixty_days_earlier.month,
|
|
1659
|
-
sixty_days_earlier.day,
|
|
1660
|
-
)
|
|
1661
|
-
if end_date > partial_date_sixty_days:
|
|
1662
|
-
has_recent_tenure = True
|
|
1663
|
-
if not has_recent_tenure and office_holder_person_elem is not None:
|
|
1664
|
-
info_message = (
|
|
1665
|
-
"All officeholder tenures ended more than 60 days ago. "
|
|
1666
|
-
"Therefore, you can remove the person and the "
|
|
1667
|
-
"related officeholder tenures from the feed."
|
|
1668
|
-
)
|
|
1669
|
-
info_log.append(
|
|
1670
|
-
loggers.LogEntry(info_message, [office_holder_person_elem])
|
|
1671
|
-
)
|
|
1672
|
-
else:
|
|
1673
|
-
for office in offices:
|
|
1674
|
-
person_id = office.find("OfficeHolderPersonIds").text
|
|
1675
|
-
if person_id in person_office_dict:
|
|
1676
|
-
person_office_dict[person_id].append(office.get("objectId"))
|
|
1677
|
-
else:
|
|
1678
|
-
person_office_dict[person_id] = [office.get("objectId")]
|
|
1679
|
-
term = office.find(".//Term")
|
|
1680
|
-
if term is not None:
|
|
1681
|
-
date_validator = base.DateRule(None, None)
|
|
1682
|
-
date_validator.gather_dates(term)
|
|
1683
|
-
end_date_person = date_validator.end_date
|
|
1684
|
-
if end_date_person is not None:
|
|
1685
|
-
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
1686
|
-
days=-60
|
|
1687
|
-
)
|
|
1688
|
-
partial_date_sixty_days = base.PartialDate(
|
|
1689
|
-
sixty_days_earlier.year,
|
|
1690
|
-
sixty_days_earlier.month,
|
|
1691
|
-
sixty_days_earlier.day,
|
|
1692
|
-
)
|
|
1693
|
-
if end_date_person < partial_date_sixty_days:
|
|
1694
|
-
outdated_offices.append(office.get("objectId"))
|
|
1695
|
-
for person in persons:
|
|
1696
|
-
pid = person.get("objectId")
|
|
1697
|
-
if person_office_dict.get(pid) is not None:
|
|
1698
|
-
check_person_outdated = all(
|
|
1699
|
-
item in outdated_offices for item in person_office_dict.get(pid)
|
|
1700
|
-
)
|
|
1701
|
-
if check_person_outdated:
|
|
1702
|
-
info_message = (
|
|
1703
|
-
"The officeholder mandates ended more than 60 days ago. "
|
|
1704
|
-
"Therefore, you can remove the person and the related offices "
|
|
1705
|
-
"from the feed."
|
|
1706
|
-
)
|
|
1707
|
-
info_log.append(loggers.LogEntry(info_message, [person]))
|
|
1708
|
-
if info_log:
|
|
1709
|
-
raise loggers.ElectionInfo(info_log)
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
class UniqueStartDatesForOfficeRoleAndJurisdiction(base.BaseRule):
|
|
1713
|
-
"""Office StartDates should be unique within a certain group.
|
|
1714
|
-
|
|
1715
|
-
Office StartDates should be unique amongst a group of Office entries
|
|
1716
|
-
with the same jurisdiction-id and office-role.
|
|
1717
|
-
"""
|
|
1718
|
-
|
|
1719
|
-
def elements(self):
|
|
1720
|
-
return ["OfficeCollection"]
|
|
1721
|
-
|
|
1722
|
-
def _filter_out_past_end_dates(self, offices):
|
|
1723
|
-
valid_offices = []
|
|
1724
|
-
for office in offices:
|
|
1725
|
-
term = office.find(".//Term")
|
|
1726
|
-
if term is not None:
|
|
1727
|
-
date_validator = base.DateRule(None, None, ocd_id_validator=None)
|
|
1728
|
-
try:
|
|
1729
|
-
date_validator.gather_dates(term)
|
|
1730
|
-
if date_validator.end_date is not None:
|
|
1731
|
-
date_validator.check_for_date_not_in_past(
|
|
1732
|
-
date_validator.end_date, date_validator.end_elem
|
|
1733
|
-
)
|
|
1734
|
-
if not date_validator.error_log:
|
|
1735
|
-
valid_offices.append(office)
|
|
1736
|
-
except loggers.ElectionError:
|
|
1737
|
-
continue
|
|
1738
|
-
return valid_offices
|
|
1739
|
-
|
|
1740
|
-
def _count_start_dates_by_jurisdiction_role(self, element):
|
|
1741
|
-
offices = element.findall("Office")
|
|
1742
|
-
offices = self._filter_out_past_end_dates(offices)
|
|
1743
|
-
jurisdiction_role_mapping = {}
|
|
1744
|
-
for office in offices:
|
|
1745
|
-
office_role = ""
|
|
1746
|
-
jurisdiction_id = ""
|
|
1747
|
-
start_date = ""
|
|
1748
|
-
|
|
1749
|
-
start_date_elem = office.find(".//Term//StartDate")
|
|
1750
|
-
if not rule_util.element_has_text(start_date_elem):
|
|
1751
|
-
continue
|
|
1752
|
-
start_date = start_date_elem.text
|
|
1753
|
-
|
|
1754
|
-
office_roles = rule_util.get_entity_info_for_value_type(
|
|
1755
|
-
office, "office-role"
|
|
1756
|
-
)
|
|
1757
|
-
if office_roles:
|
|
1758
|
-
office_role = office_roles[0]
|
|
1759
|
-
|
|
1760
|
-
jurisdiction_ids = rule_util.get_entity_info_for_value_type(
|
|
1761
|
-
office, "jurisdiction-id"
|
|
1762
|
-
)
|
|
1763
|
-
if jurisdiction_ids:
|
|
1764
|
-
jurisdiction_id = jurisdiction_ids[0]
|
|
1765
|
-
|
|
1766
|
-
office_hash = hashlib.sha256(
|
|
1767
|
-
(office_role + jurisdiction_id).encode("utf-8")
|
|
1768
|
-
).hexdigest()
|
|
1769
|
-
if office_hash not in jurisdiction_role_mapping.keys():
|
|
1770
|
-
jurisdiction_role_mapping[office_hash] = dict({
|
|
1771
|
-
"jurisdiction_id": jurisdiction_id,
|
|
1772
|
-
"office_role": office_role,
|
|
1773
|
-
"start_dates": dict({}),
|
|
1774
|
-
})
|
|
1775
|
-
|
|
1776
|
-
office_date_info = jurisdiction_role_mapping[office_hash]
|
|
1777
|
-
if start_date not in office_date_info["start_dates"].keys():
|
|
1778
|
-
office_date_info["start_dates"][start_date] = set()
|
|
1779
|
-
|
|
1780
|
-
office_date_info["start_dates"][start_date].add(office)
|
|
1781
|
-
|
|
1782
|
-
return jurisdiction_role_mapping
|
|
1783
|
-
|
|
1784
|
-
def check(self, element):
|
|
1785
|
-
warning_log = []
|
|
1786
|
-
|
|
1787
|
-
start_counts = self._count_start_dates_by_jurisdiction_role(element)
|
|
1788
|
-
for start_info in start_counts.values():
|
|
1789
|
-
start_date_map = start_info["start_dates"]
|
|
1790
|
-
if len(start_date_map.keys()) == 1:
|
|
1791
|
-
start_date = list(start_date_map.keys())[0]
|
|
1792
|
-
# this accounts for offices with only one entry (i.e. US Pres)
|
|
1793
|
-
if len(start_date_map[start_date]) > 1:
|
|
1794
|
-
warning_log.append(
|
|
1795
|
-
loggers.LogEntry(
|
|
1796
|
-
(
|
|
1797
|
-
"Only one unique StartDate found for each"
|
|
1798
|
-
" jurisdiction-id: {} and office-role: {}. {} appears {}"
|
|
1799
|
-
" times."
|
|
1800
|
-
).format(
|
|
1801
|
-
start_info["jurisdiction_id"],
|
|
1802
|
-
start_info["office_role"],
|
|
1803
|
-
start_date,
|
|
1804
|
-
len(start_date_map[start_date]),
|
|
1805
|
-
),
|
|
1806
|
-
start_date_map[start_date],
|
|
1807
|
-
)
|
|
1808
|
-
)
|
|
1809
|
-
|
|
1810
|
-
if warning_log:
|
|
1811
|
-
raise loggers.ElectionWarning(warning_log)
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
1396
|
class ValidateInfoUriAnnotation(base.BaseRule):
|
|
1815
1397
|
"""InfoUri is an annotated Uri that accepts the following annotations.
|
|
1816
1398
|
|
|
@@ -3209,33 +2791,6 @@ class UnreferencedEntitiesElectionDates(common_rules.UnreferencedEntitiesBase):
|
|
|
3209
2791
|
)
|
|
3210
2792
|
|
|
3211
2793
|
|
|
3212
|
-
class UnreferencedEntitiesOfficeholders(common_rules.UnreferencedEntitiesBase):
|
|
3213
|
-
"""CDF offices and party leadership entities are top-level in officeholders feeds.
|
|
3214
|
-
|
|
3215
|
-
This rule is a warning for CDF parties since we ask for all parties for some
|
|
3216
|
-
LatAm feeds.
|
|
3217
|
-
"""
|
|
3218
|
-
|
|
3219
|
-
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
3220
|
-
office_holder_tenure_collection = self.get_elements_by_class(
|
|
3221
|
-
election_tree, "OfficeHolderTenure"
|
|
3222
|
-
)
|
|
3223
|
-
is_post_office_split = False
|
|
3224
|
-
if office_holder_tenure_collection:
|
|
3225
|
-
is_post_office_split = True
|
|
3226
|
-
super(UnreferencedEntitiesOfficeholders, self).__init__(
|
|
3227
|
-
election_tree,
|
|
3228
|
-
schema_tree,
|
|
3229
|
-
(
|
|
3230
|
-
frozenset(["OfficeHolderTenure", "Leadership"])
|
|
3231
|
-
if is_post_office_split
|
|
3232
|
-
else frozenset(["Office", "Leadership"])
|
|
3233
|
-
),
|
|
3234
|
-
frozenset(["Party"]),
|
|
3235
|
-
**kwargs,
|
|
3236
|
-
)
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
2794
|
class ElectoralCommissionCollectionExists(base.BaseRule):
|
|
3240
2795
|
"""ElectoralCommissionCollection should exist."""
|
|
3241
2796
|
|
|
@@ -3765,6 +3320,7 @@ class ValidateIncludeInAggregationBallotSelections(base.BaseRule):
|
|
|
3765
3320
|
# To add new rules, create a new class, inherit the base rule,
|
|
3766
3321
|
# and add it to the correct rule list.
|
|
3767
3322
|
COMMON_RULES = common_rules.COMMON_RULES
|
|
3323
|
+
OFFICEHOLDER_RULES = officeholder_rules.OFFICEHOLDER_RULES
|
|
3768
3324
|
|
|
3769
3325
|
ELECTION_RULES = COMMON_RULES + (
|
|
3770
3326
|
# go/keep-sorted start
|
|
@@ -3830,20 +3386,6 @@ ELECTION_RULES = COMMON_RULES + (
|
|
|
3830
3386
|
)
|
|
3831
3387
|
|
|
3832
3388
|
|
|
3833
|
-
OFFICEHOLDER_RULES = COMMON_RULES + (
|
|
3834
|
-
# go/keep-sorted start
|
|
3835
|
-
DateOfBirthIsInPast,
|
|
3836
|
-
OfficeHolderTenureTermDates,
|
|
3837
|
-
OfficeSelectionMethodMatch,
|
|
3838
|
-
OfficeTermDates,
|
|
3839
|
-
PersonHasOffice,
|
|
3840
|
-
RemovePersonAndOfficeHolderId60DaysAfterEndDate,
|
|
3841
|
-
UniqueStartDatesForOfficeRoleAndJurisdiction,
|
|
3842
|
-
UnreferencedEntitiesOfficeholders,
|
|
3843
|
-
common_rules.ProhibitElectionData,
|
|
3844
|
-
# go/keep-sorted end
|
|
3845
|
-
)
|
|
3846
|
-
|
|
3847
3389
|
COMMITTEE_RULES = COMMON_RULES + (
|
|
3848
3390
|
# go/keep-sorted start
|
|
3849
3391
|
AffiliationEndDateOccursAfterStartDate,
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Validation rules for officeholder feeds of the CDF XML validator."""
|
|
15
|
+
|
|
16
|
+
from __future__ import print_function
|
|
17
|
+
|
|
18
|
+
import datetime
|
|
19
|
+
import hashlib
|
|
20
|
+
|
|
21
|
+
from civics_cdf_validator import base
|
|
22
|
+
from civics_cdf_validator import loggers
|
|
23
|
+
from civics_cdf_validator.rulesets import common_rules
|
|
24
|
+
from civics_cdf_validator.rulesets import rule_util
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PersonHasOffice(base.ValidReferenceRule):
|
|
28
|
+
"""Ensure that each non-party-leader Person object linked to one Office."""
|
|
29
|
+
|
|
30
|
+
def _gather_reference_values(self):
|
|
31
|
+
root = self.election_tree.getroot()
|
|
32
|
+
|
|
33
|
+
person_ids = set()
|
|
34
|
+
person_collection = root.find("PersonCollection")
|
|
35
|
+
if person_collection is not None:
|
|
36
|
+
person_ids = {p.attrib["objectId"] for p in person_collection}
|
|
37
|
+
return person_ids
|
|
38
|
+
|
|
39
|
+
def _gather_defined_values(self):
|
|
40
|
+
root = self.election_tree.getroot()
|
|
41
|
+
|
|
42
|
+
person_reference_ids = set()
|
|
43
|
+
# Add party leaders provided in the External Identifier
|
|
44
|
+
for external_id in root.findall(".//Party//ExternalIdentifier"):
|
|
45
|
+
other_type = external_id.find("OtherType")
|
|
46
|
+
if (
|
|
47
|
+
other_type is not None
|
|
48
|
+
and other_type.text in rule_util.PARTY_LEADERSHIP_TYPES
|
|
49
|
+
):
|
|
50
|
+
person_reference_ids.add(external_id.find("Value").text)
|
|
51
|
+
# Add party leaders provided in the Leadership entity
|
|
52
|
+
for leader_id in root.findall(".//Party//PartyLeaderId"):
|
|
53
|
+
if leader_id.text:
|
|
54
|
+
person_reference_ids.add(leader_id.text)
|
|
55
|
+
|
|
56
|
+
office_holder_tenure_collection = root.find("OfficeHolderTenureCollection")
|
|
57
|
+
if office_holder_tenure_collection is not None:
|
|
58
|
+
for office_holder_tenure in office_holder_tenure_collection.findall(
|
|
59
|
+
"OfficeHolderTenure"
|
|
60
|
+
):
|
|
61
|
+
id_obj = office_holder_tenure.find("OfficeHolderPersonId")
|
|
62
|
+
if id_obj is not None and id_obj.text:
|
|
63
|
+
person_reference_ids.add(id_obj.text.strip())
|
|
64
|
+
|
|
65
|
+
office_collection = root.find("OfficeCollection")
|
|
66
|
+
if (
|
|
67
|
+
office_holder_tenure_collection is None
|
|
68
|
+
and office_collection is not None
|
|
69
|
+
):
|
|
70
|
+
for office in office_collection.findall("Office"):
|
|
71
|
+
id_obj = office.find("OfficeHolderPersonIds")
|
|
72
|
+
if id_obj is not None and id_obj.text:
|
|
73
|
+
ids = id_obj.text.strip().split()
|
|
74
|
+
if len(ids) > 1:
|
|
75
|
+
msg = "Office has {} OfficeHolders. Must have exactly one.".format(
|
|
76
|
+
str(len(ids))
|
|
77
|
+
)
|
|
78
|
+
raise loggers.ElectionError.from_message(msg, [office])
|
|
79
|
+
person_reference_ids.update(ids)
|
|
80
|
+
|
|
81
|
+
return person_reference_ids
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class DateOfBirthIsInPast(base.DateRule):
|
|
85
|
+
"""Date of Birth should not be in the future."""
|
|
86
|
+
|
|
87
|
+
def elements(self):
|
|
88
|
+
return ["PersonCollection"]
|
|
89
|
+
|
|
90
|
+
def check(self, element):
|
|
91
|
+
for person_element in element:
|
|
92
|
+
date_of_birthday = person_element.find("DateOfBirth")
|
|
93
|
+
if date_of_birthday is not None and date_of_birthday.text:
|
|
94
|
+
date_of_birthday = base.PartialDate.init_partial_date(
|
|
95
|
+
date_of_birthday.text
|
|
96
|
+
)
|
|
97
|
+
self.check_for_date_in_past(date_of_birthday, person_element)
|
|
98
|
+
if self.error_log:
|
|
99
|
+
raise loggers.ElectionError(self.error_log)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OfficeSelectionMethodMatch(base.BaseRule):
|
|
103
|
+
"""Office and OfficeHolderTenure need to have matching selection methods.
|
|
104
|
+
|
|
105
|
+
Ensure that the OfficeSelectionMethod of a given OfficeHolderTenure is
|
|
106
|
+
also
|
|
107
|
+
present in the list of SelectionMethods of the Office it points to.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
111
|
+
self.office_selection_methods = {}
|
|
112
|
+
officeholder_tenure_elements = self.get_elements_by_class(
|
|
113
|
+
election_tree, "OfficeHolderTenure"
|
|
114
|
+
)
|
|
115
|
+
office_elements = self.get_elements_by_class(election_tree, "Office")
|
|
116
|
+
if officeholder_tenure_elements:
|
|
117
|
+
for element in office_elements:
|
|
118
|
+
office_id = element.get("objectId")
|
|
119
|
+
selection_methods = {
|
|
120
|
+
selection_method.text
|
|
121
|
+
for selection_method in element.findall("SelectionMethod")
|
|
122
|
+
}
|
|
123
|
+
self.office_selection_methods[office_id] = selection_methods
|
|
124
|
+
|
|
125
|
+
def elements(self):
|
|
126
|
+
return ["OfficeHolderTenure"]
|
|
127
|
+
|
|
128
|
+
def check(self, element):
|
|
129
|
+
office_id = element.find("OfficeId").text
|
|
130
|
+
office_selection_method = element.find("OfficeSelectionMethod").text
|
|
131
|
+
if (
|
|
132
|
+
office_id not in self.office_selection_methods
|
|
133
|
+
or office_selection_method
|
|
134
|
+
not in self.office_selection_methods[office_id]
|
|
135
|
+
):
|
|
136
|
+
raise loggers.ElectionError.from_message(
|
|
137
|
+
"OfficeSelectionMethod does not have a matching SelectionMethod"
|
|
138
|
+
" in the corresponding Office element.",
|
|
139
|
+
[element],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class OfficeHolderTenureTermDates(base.DateRule):
|
|
144
|
+
"""OfficeHolderTenure elements should contain valid term dates."""
|
|
145
|
+
|
|
146
|
+
def elements(self):
|
|
147
|
+
return ["OfficeHolderTenure"]
|
|
148
|
+
|
|
149
|
+
def check(self, element):
|
|
150
|
+
start_date = element.find("StartDate")
|
|
151
|
+
end_date = element.find("EndDate")
|
|
152
|
+
if rule_util.element_has_text(start_date) and rule_util.element_has_text(
|
|
153
|
+
end_date
|
|
154
|
+
):
|
|
155
|
+
start_date_obj = base.PartialDate.init_partial_date(start_date.text)
|
|
156
|
+
end_date_obj = base.PartialDate.init_partial_date(end_date.text)
|
|
157
|
+
if end_date_obj < start_date_obj:
|
|
158
|
+
raise loggers.ElectionError.from_message(
|
|
159
|
+
"OfficeHolderTenure element has an EndDate that is before the"
|
|
160
|
+
" StartDate.",
|
|
161
|
+
[element],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class OfficeTermDates(base.DateRule):
|
|
166
|
+
"""Office elements should contain valid term dates.
|
|
167
|
+
|
|
168
|
+
Offices with OfficeHolderPersonIds should have a Term declared. Given
|
|
169
|
+
term should have a start date. If term also has an end date then end date
|
|
170
|
+
should come after start date. Post Office split feed office objects should
|
|
171
|
+
not have a Term element.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
175
|
+
self.is_post_office_split_feed = False
|
|
176
|
+
officeholder_tenure_collection_element = self.get_elements_by_class(
|
|
177
|
+
election_tree, "OfficeHolderTenureCollection"
|
|
178
|
+
)
|
|
179
|
+
if officeholder_tenure_collection_element:
|
|
180
|
+
self.is_post_office_split_feed = True
|
|
181
|
+
|
|
182
|
+
def elements(self):
|
|
183
|
+
return ["Office"]
|
|
184
|
+
|
|
185
|
+
def check(self, element):
|
|
186
|
+
self.reset_instance_vars()
|
|
187
|
+
if self.is_post_office_split_feed:
|
|
188
|
+
term = element.find("Term")
|
|
189
|
+
if term is not None:
|
|
190
|
+
raise loggers.ElectionError.from_message(
|
|
191
|
+
"Office should not contain Term data in post Office split feed.",
|
|
192
|
+
[element],
|
|
193
|
+
)
|
|
194
|
+
if self.error_log:
|
|
195
|
+
raise loggers.ElectionError(self.error_log)
|
|
196
|
+
else:
|
|
197
|
+
off_per_id = element.find("OfficeHolderPersonIds")
|
|
198
|
+
if rule_util.element_has_text(off_per_id):
|
|
199
|
+
term = element.find("Term")
|
|
200
|
+
if term is None:
|
|
201
|
+
raise loggers.ElectionWarning.from_message(
|
|
202
|
+
"The Office is missing a Term.", [element]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.gather_dates(term)
|
|
206
|
+
if self.start_date is None:
|
|
207
|
+
raise loggers.ElectionWarning.from_message(
|
|
208
|
+
"The Office is missing a Term > StartDate.", [element]
|
|
209
|
+
)
|
|
210
|
+
elif self.end_date is not None:
|
|
211
|
+
self.check_end_after_start()
|
|
212
|
+
|
|
213
|
+
if self.error_log:
|
|
214
|
+
raise loggers.ElectionError(self.error_log)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class RemovePersonAndOfficeHolderId60DaysAfterEndDate(base.TreeRule):
|
|
218
|
+
"""Office elements should contain valid term dates.
|
|
219
|
+
|
|
220
|
+
Check if 60 days after the specified office term EndDate,
|
|
221
|
+
the associated Person and OfficeholderId may be removed from the feed.
|
|
222
|
+
However, if the Person is elected to another office,
|
|
223
|
+
they will be kept in the feed with the same stable ID.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def check(self):
|
|
227
|
+
info_log = []
|
|
228
|
+
persons = self.get_elements_by_class(self.election_tree, "Person")
|
|
229
|
+
offices = self.get_elements_by_class(self.election_tree, "Office")
|
|
230
|
+
officeholder_tenure_collection = self.get_elements_by_class(
|
|
231
|
+
self.election_tree, "OfficeHolderTenureCollection"
|
|
232
|
+
)
|
|
233
|
+
is_post_office_split_feed = bool(officeholder_tenure_collection)
|
|
234
|
+
person_office_dict = dict()
|
|
235
|
+
outdated_offices = []
|
|
236
|
+
outdated_officeholder_tenures = []
|
|
237
|
+
outdated_officetenure_persons = dict()
|
|
238
|
+
if is_post_office_split_feed:
|
|
239
|
+
officeholder_tenures = self.get_elements_by_class(
|
|
240
|
+
self.election_tree, "OfficeHolderTenure"
|
|
241
|
+
)
|
|
242
|
+
for officeholder_tenure in officeholder_tenures:
|
|
243
|
+
office_holder_person_elem = officeholder_tenure.find(
|
|
244
|
+
"OfficeHolderPersonId"
|
|
245
|
+
)
|
|
246
|
+
person_id = office_holder_person_elem.text
|
|
247
|
+
date_validator = base.DateRule(None, None)
|
|
248
|
+
date_validator.gather_dates(officeholder_tenure)
|
|
249
|
+
end_date = date_validator.end_date
|
|
250
|
+
if end_date is not None:
|
|
251
|
+
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
252
|
+
days=-60
|
|
253
|
+
)
|
|
254
|
+
partial_date_sixty_days = base.PartialDate(
|
|
255
|
+
sixty_days_earlier.year,
|
|
256
|
+
sixty_days_earlier.month,
|
|
257
|
+
sixty_days_earlier.day,
|
|
258
|
+
)
|
|
259
|
+
if end_date < partial_date_sixty_days:
|
|
260
|
+
outdated_officeholder_tenures.append(officeholder_tenure)
|
|
261
|
+
if person_id in outdated_officetenure_persons.keys():
|
|
262
|
+
outdated_officetenure_persons[person_id].append(
|
|
263
|
+
(office_holder_person_elem, end_date)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
outdated_officetenure_persons[person_id] = [
|
|
267
|
+
(office_holder_person_elem, end_date)
|
|
268
|
+
]
|
|
269
|
+
for outdated_officeholder_tenure in outdated_officeholder_tenures:
|
|
270
|
+
info_message = (
|
|
271
|
+
"The officeholder tenure end date is more than 60 days"
|
|
272
|
+
" in the past; this OfficeHolderTenure element can be removed"
|
|
273
|
+
" from the feed."
|
|
274
|
+
)
|
|
275
|
+
info_log.append(
|
|
276
|
+
loggers.LogEntry(info_message, [outdated_officeholder_tenure])
|
|
277
|
+
)
|
|
278
|
+
for person_id, value_list in outdated_officetenure_persons.items():
|
|
279
|
+
has_recent_tenure = False
|
|
280
|
+
office_holder_person_elem = None
|
|
281
|
+
for value in value_list:
|
|
282
|
+
office_holder_person_elem = value[0]
|
|
283
|
+
end_date = value[1]
|
|
284
|
+
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
285
|
+
days=-60
|
|
286
|
+
)
|
|
287
|
+
partial_date_sixty_days = base.PartialDate(
|
|
288
|
+
sixty_days_earlier.year,
|
|
289
|
+
sixty_days_earlier.month,
|
|
290
|
+
sixty_days_earlier.day,
|
|
291
|
+
)
|
|
292
|
+
if end_date > partial_date_sixty_days:
|
|
293
|
+
has_recent_tenure = True
|
|
294
|
+
if not has_recent_tenure and office_holder_person_elem is not None:
|
|
295
|
+
info_message = (
|
|
296
|
+
"All officeholder tenures ended more than 60 days ago. "
|
|
297
|
+
"Therefore, you can remove the person and the "
|
|
298
|
+
"related officeholder tenures from the feed."
|
|
299
|
+
)
|
|
300
|
+
info_log.append(
|
|
301
|
+
loggers.LogEntry(info_message, [office_holder_person_elem])
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
for office in offices:
|
|
305
|
+
person_id = office.find("OfficeHolderPersonIds").text
|
|
306
|
+
if person_id in person_office_dict:
|
|
307
|
+
person_office_dict[person_id].append(office.get("objectId"))
|
|
308
|
+
else:
|
|
309
|
+
person_office_dict[person_id] = [office.get("objectId")]
|
|
310
|
+
term = office.find(".//Term")
|
|
311
|
+
if term is not None:
|
|
312
|
+
date_validator = base.DateRule(None, None)
|
|
313
|
+
date_validator.gather_dates(term)
|
|
314
|
+
end_date_person = date_validator.end_date
|
|
315
|
+
if end_date_person is not None:
|
|
316
|
+
sixty_days_earlier = datetime.datetime.now() + datetime.timedelta(
|
|
317
|
+
days=-60
|
|
318
|
+
)
|
|
319
|
+
partial_date_sixty_days = base.PartialDate(
|
|
320
|
+
sixty_days_earlier.year,
|
|
321
|
+
sixty_days_earlier.month,
|
|
322
|
+
sixty_days_earlier.day,
|
|
323
|
+
)
|
|
324
|
+
if end_date_person < partial_date_sixty_days:
|
|
325
|
+
outdated_offices.append(office.get("objectId"))
|
|
326
|
+
for person in persons:
|
|
327
|
+
pid = person.get("objectId")
|
|
328
|
+
if person_office_dict.get(pid) is not None:
|
|
329
|
+
check_person_outdated = all(
|
|
330
|
+
item in outdated_offices for item in person_office_dict.get(pid)
|
|
331
|
+
)
|
|
332
|
+
if check_person_outdated:
|
|
333
|
+
info_message = (
|
|
334
|
+
"The officeholder mandates ended more than 60 days ago. "
|
|
335
|
+
"Therefore, you can remove the person and the related offices "
|
|
336
|
+
"from the feed."
|
|
337
|
+
)
|
|
338
|
+
info_log.append(loggers.LogEntry(info_message, [person]))
|
|
339
|
+
if info_log:
|
|
340
|
+
raise loggers.ElectionInfo(info_log)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class UniqueStartDatesForOfficeRoleAndJurisdiction(base.BaseRule):
|
|
344
|
+
"""Office StartDates should be unique within a certain group.
|
|
345
|
+
|
|
346
|
+
Office StartDates should be unique amongst a group of Office entries
|
|
347
|
+
with the same jurisdiction-id and office-role.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
def elements(self):
|
|
351
|
+
return ["OfficeCollection"]
|
|
352
|
+
|
|
353
|
+
def _filter_out_past_end_dates(self, offices):
|
|
354
|
+
valid_offices = []
|
|
355
|
+
for office in offices:
|
|
356
|
+
term = office.find(".//Term")
|
|
357
|
+
if term is not None:
|
|
358
|
+
date_validator = base.DateRule(None, None, ocd_id_validator=None)
|
|
359
|
+
try:
|
|
360
|
+
date_validator.gather_dates(term)
|
|
361
|
+
if date_validator.end_date is not None:
|
|
362
|
+
date_validator.check_for_date_not_in_past(
|
|
363
|
+
date_validator.end_date, date_validator.end_elem
|
|
364
|
+
)
|
|
365
|
+
if not date_validator.error_log:
|
|
366
|
+
valid_offices.append(office)
|
|
367
|
+
except loggers.ElectionError:
|
|
368
|
+
continue
|
|
369
|
+
return valid_offices
|
|
370
|
+
|
|
371
|
+
def _count_start_dates_by_jurisdiction_role(self, element):
|
|
372
|
+
offices = element.findall("Office")
|
|
373
|
+
offices = self._filter_out_past_end_dates(offices)
|
|
374
|
+
jurisdiction_role_mapping = {}
|
|
375
|
+
for office in offices:
|
|
376
|
+
office_role = ""
|
|
377
|
+
jurisdiction_id = ""
|
|
378
|
+
start_date = ""
|
|
379
|
+
|
|
380
|
+
start_date_elem = office.find(".//Term//StartDate")
|
|
381
|
+
if not rule_util.element_has_text(start_date_elem):
|
|
382
|
+
continue
|
|
383
|
+
start_date = start_date_elem.text
|
|
384
|
+
|
|
385
|
+
office_roles = rule_util.get_entity_info_for_value_type(
|
|
386
|
+
office, "office-role"
|
|
387
|
+
)
|
|
388
|
+
if office_roles:
|
|
389
|
+
office_role = office_roles[0]
|
|
390
|
+
|
|
391
|
+
jurisdiction_ids = rule_util.get_entity_info_for_value_type(
|
|
392
|
+
office, "jurisdiction-id"
|
|
393
|
+
)
|
|
394
|
+
if jurisdiction_ids:
|
|
395
|
+
jurisdiction_id = jurisdiction_ids[0]
|
|
396
|
+
|
|
397
|
+
office_hash = hashlib.sha256(
|
|
398
|
+
(office_role + jurisdiction_id).encode("utf-8")
|
|
399
|
+
).hexdigest()
|
|
400
|
+
if office_hash not in jurisdiction_role_mapping.keys():
|
|
401
|
+
jurisdiction_role_mapping[office_hash] = dict({
|
|
402
|
+
"jurisdiction_id": jurisdiction_id,
|
|
403
|
+
"office_role": office_role,
|
|
404
|
+
"start_dates": dict({}),
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
office_date_info = jurisdiction_role_mapping[office_hash]
|
|
408
|
+
if start_date not in office_date_info["start_dates"].keys():
|
|
409
|
+
office_date_info["start_dates"][start_date] = set()
|
|
410
|
+
|
|
411
|
+
office_date_info["start_dates"][start_date].add(office)
|
|
412
|
+
|
|
413
|
+
return jurisdiction_role_mapping
|
|
414
|
+
|
|
415
|
+
def check(self, element):
|
|
416
|
+
warning_log = []
|
|
417
|
+
|
|
418
|
+
start_counts = self._count_start_dates_by_jurisdiction_role(element)
|
|
419
|
+
for start_info in start_counts.values():
|
|
420
|
+
start_date_map = start_info["start_dates"]
|
|
421
|
+
if len(start_date_map.keys()) == 1:
|
|
422
|
+
start_date = list(start_date_map.keys())[0]
|
|
423
|
+
# this accounts for offices with only one entry (i.e. US Pres)
|
|
424
|
+
if len(start_date_map[start_date]) > 1:
|
|
425
|
+
warning_log.append(
|
|
426
|
+
loggers.LogEntry(
|
|
427
|
+
(
|
|
428
|
+
"Only one unique StartDate found for each"
|
|
429
|
+
" jurisdiction-id: {} and office-role: {}. {} appears {}"
|
|
430
|
+
" times."
|
|
431
|
+
).format(
|
|
432
|
+
start_info["jurisdiction_id"],
|
|
433
|
+
start_info["office_role"],
|
|
434
|
+
start_date,
|
|
435
|
+
len(start_date_map[start_date]),
|
|
436
|
+
),
|
|
437
|
+
start_date_map[start_date],
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if warning_log:
|
|
442
|
+
raise loggers.ElectionWarning(warning_log)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class UnreferencedEntitiesOfficeholders(common_rules.UnreferencedEntitiesBase):
|
|
446
|
+
"""CDF offices and party leadership entities are top-level in officeholders feeds.
|
|
447
|
+
|
|
448
|
+
This rule is a warning for CDF parties since we ask for all parties for some
|
|
449
|
+
LatAm feeds.
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
def __init__(self, election_tree, schema_tree, **kwargs):
|
|
453
|
+
office_holder_tenure_collection = self.get_elements_by_class(
|
|
454
|
+
election_tree, "OfficeHolderTenure"
|
|
455
|
+
)
|
|
456
|
+
is_post_office_split = False
|
|
457
|
+
if office_holder_tenure_collection:
|
|
458
|
+
is_post_office_split = True
|
|
459
|
+
super(UnreferencedEntitiesOfficeholders, self).__init__(
|
|
460
|
+
election_tree,
|
|
461
|
+
schema_tree,
|
|
462
|
+
(
|
|
463
|
+
frozenset(["OfficeHolderTenure", "Leadership"])
|
|
464
|
+
if is_post_office_split
|
|
465
|
+
else frozenset(["Office", "Leadership"])
|
|
466
|
+
),
|
|
467
|
+
frozenset(["Party"]),
|
|
468
|
+
**kwargs,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
OFFICEHOLDER_RULES = common_rules.COMMON_RULES + (
|
|
473
|
+
# go/keep-sorted start
|
|
474
|
+
DateOfBirthIsInPast,
|
|
475
|
+
OfficeHolderTenureTermDates,
|
|
476
|
+
OfficeSelectionMethodMatch,
|
|
477
|
+
OfficeTermDates,
|
|
478
|
+
PersonHasOffice,
|
|
479
|
+
RemovePersonAndOfficeHolderId60DaysAfterEndDate,
|
|
480
|
+
UniqueStartDatesForOfficeRoleAndJurisdiction,
|
|
481
|
+
UnreferencedEntitiesOfficeholders,
|
|
482
|
+
common_rules.ProhibitElectionData,
|
|
483
|
+
# go/keep-sorted end
|
|
484
|
+
)
|
|
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
|
|
File without changes
|
|
File without changes
|