diff --git a/.gitignore b/.gitignore index 9fabec9c..7001c4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/doc/source/_exts/candidates.py b/doc/source/_exts/candidates.py index 1d88ede6..5dae851a 100644 --- a/doc/source/_exts/candidates.py +++ b/doc/source/_exts/candidates.py @@ -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) diff --git a/doc/source/_exts/combined.jinja b/doc/source/_exts/combined.jinja new file mode 100644 index 00000000..3287e2e7 --- /dev/null +++ b/doc/source/_exts/combined.jinja @@ -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 %} diff --git a/openstack_election/cmds/check_all_candidacies.py b/openstack_election/cmds/check_all_candidacies.py index ce29273d..42a58976 100755 --- a/openstack_election/cmds/check_all_candidacies.py +++ b/openstack_election/cmds/check_all_candidacies.py @@ -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:') diff --git a/openstack_election/cmds/ci_check_all_candidate_files.py b/openstack_election/cmds/ci_check_all_candidate_files.py index 1bd29d15..8d9ef0d9 100755 --- a/openstack_election/cmds/ci_check_all_candidate_files.py +++ b/openstack_election/cmds/ci_check_all_candidate_files.py @@ -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 diff --git a/openstack_election/cmds/render_statistics.py b/openstack_election/cmds/render_statistics.py index 1bccfe14..c3578230 100755 --- a/openstack_election/cmds/render_statistics.py +++ b/openstack_election/cmds/render_statistics.py @@ -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 diff --git a/openstack_election/cmds/template_emails.py b/openstack_election/cmds/template_emails.py index 8c5a0197..f467cc34 100644 --- a/openstack_election/cmds/template_emails.py +++ b/openstack_election/cmds/template_emails.py @@ -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, diff --git a/openstack_election/tests/test_utils.py b/openstack_election/tests/test_utils.py index a8c1f48f..171aaab1 100644 --- a/openstack_election/tests/test_utils.py +++ b/openstack_election/tests/test_utils.py @@ -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') diff --git a/openstack_election/utils.py b/openstack_election/utils.py index fa187c11..39cf3a35 100644 --- a/openstack_election/utils.py +++ b/openstack_election/utils.py @@ -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