From db94ea2d59d222dd08508184a5b4619687cd1e20 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: May 14 2016 01:16:32 +0000 Subject: [PATCH 1/5] Allow hiding a user from the frontpage task list. This adds new query arguments to the taskList hub xmlrpc endpoint, and then makes use of those arguments in koji-web. A new optional configuration value is added for koji-web: `HiddenUser`, which can be used to specify which user account should be hidden. This could be useful for deployments that have a continuous-integration account, the spam from which makes the frontpage difficult to read. Unit test cases are also added for some functions of the hub taskList endpoint. Signed-off-by: Ralph Bean --- diff --git a/Makefile b/Makefile index d8f2658..beea79c 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ git-clean: @git clean -d -q -x test: - nosetests --with-coverage --cover-package . + PYTHONPATH=hub/. nosetests --with-coverage --cover-package . subdirs: for d in $(SUBDIRS); do make -C $$d; [ $$? = 0 ] || exit 1; done diff --git a/hub/kojihub.py b/hub/kojihub.py index ba7a972..72a2085 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -9708,11 +9708,17 @@ class RootExports(object): Options(dictionary): option[type]: meaning arch[list]: limit to tasks for given arches + not_arch[list]: limit to tasks without the given arches state[list]: limit to tasks of given state + not_state[list]: limit to tasks not of the given state owner[int]: limit to tasks owned by the user with the given ID + not_owner[int]: limit to tasks not owned by the user with the given ID host_id[int]: limit to tasks running on the host with the given ID + not_host_id[int]: limit to tasks running on the hosts with IDs other than the given ID channel_id[int]: limit to tasks in the specified channel + not_channel_id[int]: limit to tasks not in the specified channel parent[int]: limit to tasks with the given parent + not_parent[int]: limit to tasks without the given parent decode[bool]: whether or not xmlrpc data in the 'request' and 'result' fields should be decoded; defaults to False method[str]: limit to tasks of the given method @@ -9752,17 +9758,32 @@ class RootExports(object): aliases = [f[1] for f in flist] conditions = [] - for f in ['arch','state']: + + for f in ['arch', 'state']: + # Include list types if opts.has_key(f): conditions.append('%s IN %%(%s)s' % (f, f)) + # Exclude list types + if opts.has_key('not_' + f): + conditions.append('%s NOT IN %%(not_%s)s' % (f, f)) + for f in ['owner', 'host_id', 'channel_id', 'parent']: + # Include int types if opts.has_key(f): if opts[f] is None: conditions.append('%s IS NULL' % f) else: conditions.append('%s = %%(%s)i' % (f, f)) + # Exclude int types + if opts.has_key('not_' + f): + if opts['not_' + f] is None: + conditions.append('%s IS NOT NULL' % f) + else: + conditions.append('%s != %%(not_%s)i' % (f, f)) + if opts.has_key('method'): conditions.append('method = %(method)s') + time_opts = [ ['createdBefore', 'create_time', '<'], ['createdAfter', 'create_time', '>'], diff --git a/tests/test_hub/__init__.py b/tests/test_hub/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_hub/__init__.py diff --git a/tests/test_hub/test_listing.py b/tests/test_hub/test_listing.py new file mode 100644 index 0000000..27e60f5 --- /dev/null +++ b/tests/test_hub/test_listing.py @@ -0,0 +1,60 @@ +import unittest +import mock + +import kojihub + + +class TestListing(unittest.TestCase): + def setUp(self): + self.hub = kojihub.RootExports() + self.standard_processor_kwargs = dict( + tables=mock.ANY, + columns=mock.ANY, + values=mock.ANY, + joins=mock.ANY, + clauses=mock.ANY, + opts=mock.ANY, + aliases=mock.ANY, + ) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_basic_invocation(self, processor): + generator = self.hub.listTasks() + results = list(generator) # Exhaust the generator + processor.assert_called_once_with(**self.standard_processor_kwargs) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_owner(self, processor): + generator = self.hub.listTasks(opts={'owner': 1}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['owner = %(owner)i'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_not_owner(self, processor): + generator = self.hub.listTasks(opts={'not_owner': 1}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['owner != %(not_owner)i'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_arch(self, processor): + generator = self.hub.listTasks(opts={'arch': ['x86_64']}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['arch IN %(arch)s'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_not_arch(self, processor): + generator = self.hub.listTasks(opts={'not_arch': ['x86_64']}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['arch NOT IN %(not_arch)s'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) diff --git a/www/conf/web.conf b/www/conf/web.conf index 3bd6f91..1b167be 100644 --- a/www/conf/web.conf +++ b/www/conf/web.conf @@ -28,3 +28,8 @@ LibPath = /usr/share/koji-web/lib # If False, then the footer will be included as another Kid Template. # Defaults to True LiteralFooter = True + +# This can be the numeric ID of a user that you want to hide from tasks listed +# on the front page. You might want to, for instance, hide the activity of an +# account used for continuous integration. +#HiddenUser = 5372 diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 522aa59..84d4a74 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -272,14 +272,23 @@ def index(environ, packageOrder='package_name', packageStart=None): values = _initValues(environ) server = _getServer(environ) + opts = environ['koji.options'] user = environ['koji.currentUser'] - values['builds'] = server.listBuilds(userID=(user and user['id'] or None), queryOpts={'order': '-build_id', 'limit': 10}) + values['builds'] = server.listBuilds( + userID=(user and user['id'] or None), + queryOpts={'order': '-build_id', 'limit': 10} + ) taskOpts = {'parent': None, 'decode': True} if user: taskOpts['owner'] = user['id'] - values['tasks'] = server.listTasks(opts=taskOpts, queryOpts={'order': '-id', 'limit': 10}) + if opts.get('HiddenUser'): + taskOpts['not_owner'] = opts['HiddenUser'] + values['tasks'] = server.listTasks( + opts=taskOpts, + queryOpts={'order': '-id', 'limit': 10} + ) values['order'] = '-id' From f482f104f2f05f603153533737f109c56d3e4971 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: May 16 2016 16:41:49 +0000 Subject: [PATCH 2/5] Remove unused var. --- diff --git a/tests/test_hub/test_listing.py b/tests/test_hub/test_listing.py index 27e60f5..b219876 100644 --- a/tests/test_hub/test_listing.py +++ b/tests/test_hub/test_listing.py @@ -20,7 +20,7 @@ class TestListing(unittest.TestCase): @mock.patch('kojihub.QueryProcessor') def test_list_tasks_basic_invocation(self, processor): generator = self.hub.listTasks() - results = list(generator) # Exhaust the generator + list(generator) # Exhaust the generator processor.assert_called_once_with(**self.standard_processor_kwargs) @mock.patch('kojihub.QueryProcessor') From df13b4dfb67dbb1b5dc4692a57466923cd778d65 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: May 16 2016 16:48:05 +0000 Subject: [PATCH 3/5] Make HiddenUser into HiddenUsers. At @mikeb's suggestion in the code review, this makes HiddenUsers plural. This makes the whole changeset a little more invasive than it was before, so please review carefully. --- diff --git a/hub/kojihub.py b/hub/kojihub.py index 72a2085..1e483f6 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -9711,14 +9711,14 @@ class RootExports(object): not_arch[list]: limit to tasks without the given arches state[list]: limit to tasks of given state not_state[list]: limit to tasks not of the given state - owner[int]: limit to tasks owned by the user with the given ID - not_owner[int]: limit to tasks not owned by the user with the given ID - host_id[int]: limit to tasks running on the host with the given ID - not_host_id[int]: limit to tasks running on the hosts with IDs other than the given ID - channel_id[int]: limit to tasks in the specified channel - not_channel_id[int]: limit to tasks not in the specified channel - parent[int]: limit to tasks with the given parent - not_parent[int]: limit to tasks without the given parent + owner[int|list]: limit to tasks owned by the user with the given ID + not_owner[int|list]: limit to tasks not owned by the user with the given ID + host_id[int|list]: limit to tasks running on the host with the given ID + not_host_id[int|list]: limit to tasks running on the hosts with IDs other than the given ID + channel_id[int|list]: limit to tasks in the specified channel + not_channel_id[int|list]: limit to tasks not in the specified channel + parent[int|list]: limit to tasks with the given parent + not_parent[int|list]: limit to tasks without the given parent decode[bool]: whether or not xmlrpc data in the 'request' and 'result' fields should be decoded; defaults to False method[str]: limit to tasks of the given method @@ -9772,12 +9772,16 @@ class RootExports(object): if opts.has_key(f): if opts[f] is None: conditions.append('%s IS NULL' % f) + elif isinstance(opts[f], types.ListType): + conditions.append('%s IN %%(%s)s' % (f, f)) else: conditions.append('%s = %%(%s)i' % (f, f)) # Exclude int types if opts.has_key('not_' + f): if opts['not_' + f] is None: conditions.append('%s IS NOT NULL' % f) + elif isinstance(opts['not_' + f], types.ListType): + conditions.append('%s NOT IN %%(not_%s)s' % (f, f)) else: conditions.append('%s != %%(not_%s)i' % (f, f)) diff --git a/tests/test_hub/test_listing.py b/tests/test_hub/test_listing.py index b219876..a39f2b0 100644 --- a/tests/test_hub/test_listing.py +++ b/tests/test_hub/test_listing.py @@ -24,7 +24,7 @@ class TestListing(unittest.TestCase): processor.assert_called_once_with(**self.standard_processor_kwargs) @mock.patch('kojihub.QueryProcessor') - def test_list_tasks_by_owner(self, processor): + def test_list_tasks_by_owner_as_int(self, processor): generator = self.hub.listTasks(opts={'owner': 1}) results = list(generator) # Exhaust the generator arguments = self.standard_processor_kwargs.copy() @@ -33,7 +33,7 @@ class TestListing(unittest.TestCase): self.assertEqual(results, []) @mock.patch('kojihub.QueryProcessor') - def test_list_tasks_by_not_owner(self, processor): + def test_list_tasks_by_not_owner_as_int(self, processor): generator = self.hub.listTasks(opts={'not_owner': 1}) results = list(generator) # Exhaust the generator arguments = self.standard_processor_kwargs.copy() @@ -58,3 +58,21 @@ class TestListing(unittest.TestCase): arguments['clauses'] = ['arch NOT IN %(not_arch)s'] processor.assert_called_once_with(**arguments) self.assertEqual(results, []) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_owner_as_list(self, processor): + generator = self.hub.listTasks(opts={'owner': [1, 2]}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['owner IN %(owner)s'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) + + @mock.patch('kojihub.QueryProcessor') + def test_list_tasks_by_not_owner_as_list(self, processor): + generator = self.hub.listTasks(opts={'not_owner': [1, 2]}) + results = list(generator) # Exhaust the generator + arguments = self.standard_processor_kwargs.copy() + arguments['clauses'] = ['owner NOT IN %(not_owner)s'] + processor.assert_called_once_with(**arguments) + self.assertEqual(results, []) diff --git a/www/conf/web.conf b/www/conf/web.conf index 1b167be..f7dd34d 100644 --- a/www/conf/web.conf +++ b/www/conf/web.conf @@ -29,7 +29,7 @@ LibPath = /usr/share/koji-web/lib # Defaults to True LiteralFooter = True -# This can be the numeric ID of a user that you want to hide from tasks listed -# on the front page. You might want to, for instance, hide the activity of an -# account used for continuous integration. -#HiddenUser = 5372 +# This can be a comma-delimited list of the numeric IDs of users that you want +# to hide from tasks listed on the front page. You might want to, for instance, +# hide the activity of an account used for continuous integration. +#HiddenUsers = 5372,1234 diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 84d4a74..1fdcc5b 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -283,8 +283,8 @@ def index(environ, packageOrder='package_name', packageStart=None): taskOpts = {'parent': None, 'decode': True} if user: taskOpts['owner'] = user['id'] - if opts.get('HiddenUser'): - taskOpts['not_owner'] = opts['HiddenUser'] + if opts.get('HiddenUsers'): + taskOpts['not_owner'] = opts['HiddenUsers'] values['tasks'] = server.listTasks( opts=taskOpts, queryOpts={'order': '-id', 'limit': 10} From d35e20b4685e2072821222d0cf90c3884571b7a0 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: May 16 2016 16:55:35 +0000 Subject: [PATCH 4/5] Split the HiddenUsers string pulled from our config. --- diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 1fdcc5b..8d9b737 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -284,7 +284,9 @@ def index(environ, packageOrder='package_name', packageStart=None): if user: taskOpts['owner'] = user['id'] if opts.get('HiddenUsers'): - taskOpts['not_owner'] = opts['HiddenUsers'] + taskOpts['not_owner'] = [ + int(userid.strip()) for userid in opts['HiddenUsers'].split() + ] values['tasks'] = server.listTasks( opts=taskOpts, queryOpts={'order': '-id', 'limit': 10} From 567102f18f7fa8b6914b2c3f80c2578bed2c95de Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: May 16 2016 16:58:58 +0000 Subject: [PATCH 5/5] Space-delimited. --- diff --git a/www/conf/web.conf b/www/conf/web.conf index f7dd34d..4069258 100644 --- a/www/conf/web.conf +++ b/www/conf/web.conf @@ -29,7 +29,7 @@ LibPath = /usr/share/koji-web/lib # Defaults to True LiteralFooter = True -# This can be a comma-delimited list of the numeric IDs of users that you want +# This can be a space-delimited list of the numeric IDs of users that you want # to hide from tasks listed on the front page. You might want to, for instance, # hide the activity of an account used for continuous integration. -#HiddenUsers = 5372,1234 +#HiddenUsers = 5372 1234 diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 8d9b737..fb3907e 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -285,7 +285,7 @@ def index(environ, packageOrder='package_name', packageStart=None): taskOpts['owner'] = user['id'] if opts.get('HiddenUsers'): taskOpts['not_owner'] = [ - int(userid.strip()) for userid in opts['HiddenUsers'].split() + int(userid) for userid in opts['HiddenUsers'].split() ] values['tasks'] = server.listTasks( opts=taskOpts,