Add new combined election type

This type is for when TC and PTL elections must overlap.  There is a
built in assumption that each "phase" of an election:
 - Nominations
 - Campaigning
 - Election

are completely synchronous

NOTE: This change leaves the template-emails broken, as I do not add new
functions for combined_$email.  This is because I'd like to
If64c075b4a799574bc51ccd13019d472ee9a4b0e merge first, and I'd also like
to avoid to many rebases

Change-Id: Id39c50e71dc91bea8034ce443287463094e2f37f
This commit is contained in:
Tony Breeds 2019-05-28 14:42:51 +10:00
parent 30ddc77e97
commit c6406fdbd5
9 changed files with 72 additions and 25 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ doc/source/archive_toc.rst
doc/source/*/*.rst
doc/source/ptl.rst
doc/source/tc.rst
doc/source/combined.rst
doc/source/events.rst
doc/source/configuration.rst
doc/source/results/*/announce_ptl.rst

View File

@ -99,10 +99,8 @@ def build_lists(app):
if utils.election_is_running():
# Current candidates
candidates_list = utils.build_candidates_list()
if not utils.is_tc_election():
render_list("ptl", candidates_list)
else:
render_list("tc", candidates_list)
election_type = utils.conf.get('election_type', '').lower()
render_list(election_type, candidates_list)
# Archived elections
previous_toc = [
@ -127,13 +125,9 @@ class CandidatesDirective(Directive):
def run(self):
if not utils.election_is_running():
return []
election_type = utils.conf.get('election_type', '').lower()
rst = '.. include:: '
if utils.is_tc_election():
rst += 'tc.rst'
else:
rst += 'ptl.rst'
rst = '.. include:: %s.rst' % (election_type)
result = ViewList()
for idx, line in enumerate(rst.splitlines()):
result.append(line, 'CandidatesDirective', idx)

View File

@ -0,0 +1,17 @@
{{ election.capitalize() }} TC Candidates
======================
{% for candidate in candidates['TC'] %}
* `{{ candidate['fullname'] }} {% if candidate['ircname'] is not none %}({{ candidate['ircname'] }}){% endif %} <{{ candidate['url'] }}>`__
{% endfor %}
{{ election.capitalize() }} PTL Candidates
======================
{% for project in projects|sort %}{% if project != 'TC' %}
* {{ project.replace('_', ' ') }}
{% for candidate in candidates[project] %}
* `{{ candidate['fullname'] }} {% if candidate['ircname'] is not none %}({{ candidate['ircname'] }}){% endif %} <{{ candidate['url'] }}>`__
{% endfor %}
{% endif %}{% endfor %}

View File

@ -55,6 +55,7 @@ def main():
args = parser.parse_args()
projects = utils.get_projects(tag=args.tag, fallback_to_master=True)
election_type = utils.conf.get('election_type', '').lower()
for review in get_reviews():
if review['status'] != 'NEW':
@ -77,7 +78,10 @@ def main():
candiate_ok = checks.validate_member(filepath)
if candiate_ok:
if not utils.is_tc_election():
# If we're a PTL election OR if the team is not TC we need
# to check for validating changes
if (election_type == 'ptl'
or (election_type == 'combined' and team != 'TC')):
if args.interactive:
print('The following commit and profile validate this '
'candidate:')

View File

@ -125,6 +125,7 @@ def main():
return 1
projects = utils.get_projects(tag=args.tag, fallback_to_master=True)
election_type = utils.conf.get('election_type', '').lower()
if args.files:
to_process = args.files
@ -134,13 +135,24 @@ def main():
to_process = utils.find_candidate_files(election=args.release)
for filepath in to_process:
email = utils.get_email(filepath)
team = os.path.basename(os.path.dirname(filepath))
# Some kind souls remove the .placeholder file when they upload
# a candidacy
if email == '.placeholder':
continue
candidate_ok = True
candidate_ok &= validate_filename(filepath)
candidate_ok &= validate_member(filepath)
if candidate_ok and not utils.is_tc_election():
candidate_ok &= check_for_changes(projects, filepath, args.limit)
if candidate_ok:
if (election_type == 'ptl'
or (election_type == 'combined' and team != 'TC')):
candidate_ok &= check_for_changes(projects, filepath,
args.limit)
errors |= not candidate_ok

View File

@ -133,6 +133,9 @@ def main():
args = parser.parse_args()
# NOTE(tonyb): If we're a "combined" election we'll have the required
# events for the statistics to render collectly so we can just use a quick
# 'is_tc' check here
if utils.is_tc_election():
print('This tool only works for PTL elections not TC')
return 0

View File

@ -31,7 +31,9 @@ fmt_args = dict(
start_release=start_release,
time_frame=time_frame,
)
if utils.is_tc_election():
election_type = utils.conf.get('election_type', '').lower()
if election_type in ['tc', 'combined']:
fmt_args.update(dict(
start_nominations=utils.get_event('TC Nominations')['start_str'],
end_nominations=utils.get_event('TC Nominations')['end_str'],
@ -42,7 +44,11 @@ if utils.is_tc_election():
poll_name='%s TC Election' % (conf['release'].capitalize()),
))
template_names += ['campaigning_kickoff']
else:
# NOTE(tonyb): In the case of a "combined" election we assume that the dates
# for each "phase" (nominations, campaigning or elections) overlap so updating
# the end_nominations key here with the PTL date should be safe
if election_type in ['ptl', 'combined']:
# NOTE(tonyb): We need an empty item last to ensure the path ends in a
# tailing '/'
stats.collect_project_stats(os.path.join(utils.CANDIDATE_PATH,

View File

@ -41,26 +41,35 @@ class TestGerritUtils(base.ElectionTestCase):
class TestFindCandidateFiles(base.ElectionTestCase):
@mock.patch.object(utils, 'is_tc_election', return_value=False)
@mock.patch('os.path.exists', return_value=True)
@mock.patch('os.listdir', side_effect=[['SomeProject', 'TC'],
['invalid@example.com']])
def test_ptl_lists(self, mock_listdir, mock_path_exists,
mock_is_tc_election):
candidate_files = utils.find_candidate_files(election='fake')
def test_ptl_lists(self, mock_listdir, mock_path_exists):
with mock.patch.dict(utils.conf, dict(election_type='ptl')):
candidate_files = utils.find_candidate_files(election='fake')
self.assertEqual(['candidates/fake/SomeProject/invalid@example.com'],
candidate_files)
@mock.patch.object(utils, 'is_tc_election', return_value=True)
@mock.patch('os.path.exists', return_value=True)
@mock.patch('os.listdir', side_effect=[['SomeProject', 'TC'],
['invalid@example.com']])
def test_tc_lists(self, mock_listdir, mock_path_exists,
mock_is_tc_election):
candidate_files = utils.find_candidate_files(election='fake')
def test_tc_lists(self, mock_listdir, mock_path_exists):
with mock.patch.dict(utils.conf, dict(election_type='tc')):
candidate_files = utils.find_candidate_files(election='fake')
self.assertEqual(['candidates/fake/TC/invalid@example.com'],
candidate_files)
@mock.patch('os.path.exists', return_value=True)
@mock.patch('os.listdir', side_effect=[['SomeProject', 'TC'],
['invalid@example.com'],
['invalid@example.com']])
def test_combined_lists(self, mock_listdir, mock_path_exists):
with mock.patch.dict(utils.conf, dict(election_type='combined')):
candidate_files = utils.find_candidate_files(election='fake')
self.assertEqual(['candidates/fake/SomeProject/invalid@example.com',
'candidates/fake/TC/invalid@example.com'],
candidate_files)
class TestBuildCandidatesList(base.ElectionTestCase):
@mock.patch.object(utils, 'lookup_member')

View File

@ -286,17 +286,18 @@ def election_is_running():
def find_candidate_files(election=conf['release']):
election_path = os.path.join(CANDIDATE_PATH, election)
election_type = conf.get('election_type', '').lower()
if os.path.exists(election_path):
project_list = os.listdir(election_path)
else:
project_list = []
if is_tc_election():
if election_type == 'tc':
project_list = list(filter(
lambda p: p in ['TC'],
project_list
))
else:
elif election_type == 'ptl':
project_list = list(filter(
lambda p: p not in ['TC'],
project_list