Repository: fedora-infra/bodhi

Issue:
I was not offered the "push to batched" button when update reached days in testing
When https://bodhi.fedoraproject.org/updates/FEDORA-EPEL-2017-1e6d4edc57 reached 14 days in testing, a "push to stable" button appeared, but no "push to batched". Perhaps we only remembered to add "push to batched" for karma-based pushes?

I was able to request it for batched using the CLI, so there is a workaround:

```
$ bodhi updates request FEDORA-EPEL-2017-1e6d4edc57 batched
```


Discussion:

Resolving patch:
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -878,6 +878,29 @@ def bug_link(context, bug, short=False):
     return link
 
 
+def push_to_batched_or_stable_button(context, update):
+    """
+    Form the push to batched or push to stable button, as appropriate.
+
+    Args:
+        context (mako.runtime.Context): The current template rendering context. Unused.
+        update (bodhi.server.models.Update): The Update we are rendering a button about.
+    Returns:
+        basestring: HTML for a button that will draw the push to stable or push to batched button,
+            as appropriate.
+    """
+    if getattr(update.request, 'description', None) == 'batched' or \
+            getattr(update.severity, 'description', None) == 'urgent':
+        button = ('stable', 'Stable')
+    elif getattr(update.request, 'description', None) in ('stable', None):
+        button = ('batched', 'Batched')
+    else:
+        return ''
+
+    return ('<a id="{}" class="btn btn-sm btn-success">'
+            '<span class="fa fa-fw fa-arrow-circle-right"></span> Push to {}</a>').format(*button)
+
+
 def testcase_link(context, test, short=False):
     """
     Form a URL to a given test description.


Test patch:
diff --git a/bodhi/tests/server/services/test_updates.py b/bodhi/tests/server/services/test_updates.py
--- a/bodhi/tests/server/services/test_updates.py
+++ b/bodhi/tests/server/services/test_updates.py
@@ -794,13 +794,11 @@ def test_provenpackager_request_update_queued_in_test_gating(self, publish, *arg
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
 
@@ -810,8 +808,9 @@ def test_provenpackager_request_update_queued_in_test_gating(self, publish, *arg
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure we can't push it until it passed test gating
         self.assertEqual(res.json_body['status'], 'error')
@@ -830,13 +829,11 @@ def test_provenpackager_request_update_running_in_test_gating(self, publish, *ar
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
 
@@ -846,8 +843,9 @@ def test_provenpackager_request_update_running_in_test_gating(self, publish, *ar
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure we can't push it until it passed test gating
         self.assertEqual(res.json_body['status'], 'error')
@@ -866,13 +864,11 @@ def test_provenpackager_request_update_failed_test_gating(self, publish, *args):
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
 
@@ -882,8 +878,9 @@ def test_provenpackager_request_update_failed_test_gating(self, publish, *args):
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure we can't push it until it passed test gating
         self.assertEqual(res.json_body['status'], 'error')
@@ -902,13 +899,11 @@ def test_provenpackager_request_update_ignored_by_test_gating(self, publish, *ar
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         assert 'does not have commit access to bodhi' not in res, res
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
@@ -919,8 +914,9 @@ def test_provenpackager_request_update_ignored_by_test_gating(self, publish, *ar
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure the reason we cannot push isn't test gating this time
         self.assertEqual(res.json_body['status'], 'error')
@@ -939,13 +935,11 @@ def test_provenpackager_request_update_waiting_on_test_gating(self, publish, *ar
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
 
@@ -955,8 +949,9 @@ def test_provenpackager_request_update_waiting_on_test_gating(self, publish, *ar
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure we can't push it until it passed test gating
         self.assertEqual(res.json_body['status'], 'error')
@@ -975,13 +970,11 @@ def test_provenpackager_request_update_with_none_test_gating(self, publish, *arg
         self.db.add(user)
         group = self.db.query(Group).filter_by(name=u'provenpackager').one()
         user.groups.append(group)
-        self.app_settings['test_gating.required'] = True
         self.db.commit()
 
-        app = TestApp(main({}, testing=u'bob', session=self.db, **self.app_settings))
         up_data = self.get_update(nvr)
-        up_data['csrf_token'] = app.get('/csrf').json_body['csrf_token']
-        res = app.post_json('/updates/', up_data)
+        up_data['csrf_token'] = self.app.get('/csrf').json_body['csrf_token']
+        res = self.app.post_json('/updates/', up_data)
         publish.assert_called_once_with(
             topic='update.request.testing', msg=mock.ANY)
 
@@ -991,8 +984,9 @@ def test_provenpackager_request_update_with_none_test_gating(self, publish, *arg
 
         # Try and submit the update to stable as a provenpackager
         post_data = dict(update=nvr, request='stable',
-                         csrf_token=app.get('/csrf').json_body['csrf_token'])
-        res = app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
+                         csrf_token=self.app.get('/csrf').json_body['csrf_token'])
+        with mock.patch.dict(config, {'test_gating.required': True}):
+            res = self.app.post_json('/updates/%s/request' % str(nvr), post_data, status=400)
 
         # Ensure the reason we can't push is not test gating
         self.assertEqual(res.json_body['status'], 'error')
@@ -3775,26 +3769,251 @@ def test_autopush_non_critical_update_with_no_negative_karma(self, publish, *arg
 
     @mock.patch(**mock_valid_requirements)
     @mock.patch('bodhi.server.notifications.publish')
-    def test_manually_push_to_stable_from_batched(self, publish, *args):
+    def test_edit_button_not_present_when_stable(self, publish, *args):
         """
-        Test manually push to stable from batched when autokarma is disabled
+        Assert that the edit button is not present on stable updates.
         """
         nvr = u'bodhi-2.0.0-2.fc17'
         args = self.get_update(nvr)
         resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.date_stable = datetime.utcnow()
+        update.status = UpdateStatus.stable
+        update.pushed = True
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Edit text not in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
+        self.assertNotIn('Push to Stable', resp)
+        self.assertNotIn('Edit', resp)
 
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_batched_button_present_when_karma_reached(self, publish, *args):
+        """
+        Assert that the "Push to Batched" button appears when the required karma is
+        reached.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.status = UpdateStatus.testing
+        update.request = None
+        update.pushed = True
+        update.autokarma = False
+        update.stable_karma = 1
+        update.comment(self.db, 'works', 1, 'bowlofeggs')
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Batched text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertIn('Push to Batched', resp)
+        self.assertNotIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_stable_button_present_when_karma_reached_urgent(self, publish, *args):
+        """
+        Assert that the "Push to Stable" button appears when the required karma is
+        reached for an urgent update.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.severity = UpdateSeverity.urgent
+        update.status = UpdateStatus.testing
+        update.request = None
+        update.pushed = True
+        update.autokarma = False
+        update.stable_karma = 1
+        update.comment(self.db, 'works', 1, 'bowlofeggs')
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Stable text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
+        self.assertIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_stable_button_present_when_karma_reached_and_batched(self, publish, *args):
+        """
+        Assert that the "Push to Stable" button appears when the required karma is
+        reached and the update is already batched.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
         update = Update.get(nvr, self.db)
         update.status = UpdateStatus.testing
         update.request = UpdateRequest.batched
+        update.pushed = True
+        update.autokarma = False
+        update.stable_karma = 1
+        update.comment(self.db, 'works', 1, 'bowlofeggs')
         self.db.commit()
 
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
         # Checks Push to Stable text in the html page for this update
-        id = 'bodhi-2.0.0-2.fc17'
-        resp = self.app.get('/updates/%s' % id,
-                            headers={'Accept': 'text/html'})
         self.assertIn('text/html', resp.headers['Content-Type'])
-        self.assertIn(id, resp)
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
+        self.assertIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_batched_button_present_when_time_reached(self, publish, *args):
+        """
+        Assert that the "Push to Batched" button appears when the required time in testing is
+        reached.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.status = UpdateStatus.testing
+        update.request = None
+        update.pushed = True
+        # This update has been in testing a while, so a "Push to Batched" button should appear.
+        update.date_testing = datetime.now() - timedelta(days=30)
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Batched text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertIn('Push to Batched', resp)
+        self.assertNotIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_stable_button_present_when_time_reached_and_urgent(self, publish, *args):
+        """
+        Assert that the "Push to Stable" button appears when the required time in testing is
+        reached.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.severity = UpdateSeverity.urgent
+        update.status = UpdateStatus.testing
+        update.request = None
+        update.pushed = True
+        # This urgent update has been in testing a while, so a "Push to Stable" button should
+        # appear.
+        update.date_testing = datetime.now() - timedelta(days=30)
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Stable text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
+        self.assertIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_stable_button_present_when_time_reached_and_batched(self, publish, *args):
+        """
+        Assert that the "Push to Stable" button appears when the required time in testing is
+        reached and the update is already batched.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.status = UpdateStatus.testing
+        update.request = UpdateRequest.batched
+        update.pushed = True
+        # This update has been in testing a while, so a "Push to Stable" button should appear.
+        update.date_testing = datetime.now() - timedelta(days=30)
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Stable text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
+        self.assertIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_batched_button_present_when_time_reached_critpath(self, publish, *args):
+        """
+        Assert that the "Push to Batched" button appears when it should for a critpath update.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.status = UpdateStatus.testing
+        update.request = None
+        update.pushed = True
+        update.critpath = True
+        # This update has been in testing a while, so a "Push to Batched" button should appear.
+        update.date_testing = datetime.now() - timedelta(days=30)
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Batched text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertIn('Push to Batched', resp)
+        self.assertNotIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
+
+    @mock.patch(**mock_valid_requirements)
+    @mock.patch('bodhi.server.notifications.publish')
+    def test_push_to_stable_button_present_when_time_reached_and_batched_critpath(self, publish,
+                                                                                  *args):
+        """
+        Assert that the "Push to Stable" button appears when the required time in testing is
+        reached and the update is already batched.
+        """
+        nvr = u'bodhi-2.0.0-2.fc17'
+        args = self.get_update(nvr)
+        resp = self.app.post_json('/updates/', args)
+        update = Update.get(nvr, self.db)
+        update.critpath = True
+        update.status = UpdateStatus.testing
+        update.request = UpdateRequest.batched
+        update.pushed = True
+        # This update has been in testing a while, so a "Push to Batched" button should appear.
+        update.date_testing = datetime.now() - timedelta(days=30)
+        self.db.commit()
+
+        resp = self.app.get('/updates/%s' % nvr, headers={'Accept': 'text/html'})
+
+        # Checks Push to Stable text in the html page for this update
+        self.assertIn('text/html', resp.headers['Content-Type'])
+        self.assertIn(nvr, resp)
+        self.assertNotIn('Push to Batched', resp)
         self.assertIn('Push to Stable', resp)
+        self.assertIn('Edit', resp)
 
     @mock.patch(**mock_valid_requirements)
     @mock.patch('bodhi.server.notifications.publish')
diff --git a/bodhi/tests/server/test_util.py b/bodhi/tests/server/test_util.py
--- a/bodhi/tests/server/test_util.py
+++ b/bodhi/tests/server/test_util.py
@@ -24,7 +24,7 @@
 from bodhi.server import util
 from bodhi.server.buildsys import setup_buildsystem, teardown_buildsystem
 from bodhi.server.config import config
-from bodhi.server.models import TestGatingStatus
+from bodhi.server.models import TestGatingStatus, Update, UpdateRequest, UpdateSeverity
 from bodhi.tests.server import base
 
 
@@ -102,6 +102,58 @@ def test_short_true(self):
              "#1234567</a>"))
 
 
+class TestPushToBatchedOrStableButton(base.BaseTestCase):
+    """Test the push_to_batched_or_stable_button() function."""
+    def test_request_is_batched(self):
+        """The function should render a Push to Stable button if the request is batched."""
+        u = Update.query.all()[0]
+        u.request = UpdateRequest.batched
+
+        a = util.push_to_batched_or_stable_button(None, u)
+
+        self.assertTrue('id="stable"' in a)
+        self.assertTrue('</span> Push to Stable</a>' in a)
+
+    def test_request_is_none(self):
+        """The function should render a Push to Stable button if the request is None."""
+        u = Update.query.all()[0]
+        u.request = None
+
+        a = util.push_to_batched_or_stable_button(None, u)
+
+        self.assertTrue('id="batched"' in a)
+        self.assertTrue('</span> Push to Batched</a>' in a)
+
+    def test_request_is_other(self):
+        """The function should return '' request is something else, like testing."""
+        u = Update.query.all()[0]
+        u.request = UpdateRequest.testing
+
+        a = util.push_to_batched_or_stable_button(None, u)
+
+        self.assertEqual(a, '')
+
+    def test_request_is_stable(self):
+        """The function should render a Push to Batched button if the request is stable."""
+        u = Update.query.all()[0]
+        u.request = UpdateRequest.stable
+
+        a = util.push_to_batched_or_stable_button(None, u)
+
+        self.assertTrue('id="batched"' in a)
+        self.assertTrue('</span> Push to Batched</a>' in a)
+
+    def test_severity_is_urgent(self):
+        """The function should render a Push to Stable button if the severity is urgent."""
+        u = Update.query.all()[0]
+        u.severity = UpdateSeverity.urgent
+
+        a = util.push_to_batched_or_stable_button(None, u)
+
+        self.assertTrue('id="stable"' in a)
+        self.assertTrue('</span> Push to Stable</a>' in a)
+
+
 class TestUtils(base.BaseTestCase):
 
     def setUp(self):