Repository: fedora-infra/bodhi

Issue:
RFE for lazy maintainers ... how many days is the package in testing?
Unfortunately, most of packages I maintain has very low number of testers so (especially for EPEL branches) I usually end with waiting until the 7 (or 14) days expires and I can push it to the stable. Therefore, the following command is pretty common for me:
```
 $ bodhi updates query --status testing --user mcepl
ldapvi-1.7-29.fc26                    rpm    enhancement  testing   2018-02-19
ldapvi-1.7-29.fc27                    rpm    enhancement  testing   2018-02-19
ldapvi-1.7-29.el7                     rpm    newpackage   testing   2018-02-19
vim-vimoutliner-0.4.0-8.el7           rpm    enhancement  testing   2018-02-17
youtube-dl-2018.02.08-2.el6           rpm    enhancement  testing   2018-02-12
youtube-dl-2018.02.08-2.el7           rpm    enhancement  testing   2018-02-13
gen-oath-safe-0.10.1-3.el7            rpm    enhancement  testing   2018-02-07
youtube-dl-2016.12.09-1.fc23          rpm    enhancement  testing   2016-12-13
8 updates found (8 shown)
 $
```
(BTW, there used to be parameter ``--mine`` as an alias for ``--user=$ME``, wasn't there?)
(and yes, I am afflicted by #752 as well :( )

For lazy bums like myself, it would be enhancement, if I could have in the end number of days the package is in the state it is, e.g., something like:
```
ldapvi-1.7-29.el7                     rpm    newpackage   testing   2018-02-19 (7)
```


Discussion:
On 02/27/2018 04:02 AM, Matěj Cepl wrote:
> (BTW, there used to be parameter |--mine| as an alias for |--user=$ME|,
> wasn't there?)

```--mine``` is present in Bodhi >= 2.10.0:

https://bodhi.fedoraproject.org/docs/user/release_notes.html#id56

> For lazy bums like myself, it would be enhancement, if I could have in
> the end number of days the package is in the state it is, e.g.,
> something like:
> 
> |ldapvi-1.7-29.el7 rpm newpackage testing 2018-02-19 (7) |

Good idea, thanks for the RFE!

I can work on this.
But we need at least a 4 (or 5?) chars column for that, and the line is already at 80 chars:
```
ldapvi-1.7-29.el7                     rpm    newpackage   testing   2018-02-19 (7)
ldapvi-1.7-29.el7                     rpm    newpackage   testing   2018-02-19(14)
```
Should I take the space from the name column? Or we can overpass the 80chars limit?
In my opinion the best option would be to remove the update type column ('newpackage') and use those 11 chars for the new column + more space for the name of the update. I don't think it's essential to have that column in the brief output. 
Either that, or just abbreviate (``new``, ``bug``, ``sec``)?

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -379,7 +379,13 @@ def edit(user, password, url, **kwargs):
 @handle_errors
 def query(url, mine=False, **kwargs):
     # User Docs that show in the --help
-    """Query updates on Bodhi."""
+    """Query updates on Bodhi.
+
+    A leading '*' means that this is a 'security' update.
+
+    The number between brackets next to the date indicates the number of days
+    the update is in the current state.
+    """
     # Developer Docs
     """
     Query updates based on flags.
@@ -726,7 +732,7 @@ def print_resp(resp, client, verbose=False):
             click.echo(client.update_str(resp.updates[0]))
         else:
             for update in resp.updates:
-                click.echo(client.update_str(update, minimal=True).strip())
+                click.echo(client.update_str(update, minimal=True))
         if 'total' in resp:
             click.echo('%s updates found (%d shown)' % (
                 resp.total, len(resp.updates)))
diff --git a/bodhi/client/bindings.py b/bodhi/client/bindings.py
--- a/bodhi/client/bindings.py
+++ b/bodhi/client/bindings.py
@@ -129,6 +129,25 @@ def wrapper(*args, **kwargs):
     return wrapper
 
 
+def _days_since(data_str):
+    """
+    Return number of days since the datetime passed as input in the form '%Y-%m-%d %H:%M:%S'.
+
+    This can be used to calculate how many days an update is in current state by passing
+    directly the 'date_pushed' or 'date_submitted' from the Update object.
+    This is also useful to easily mock the output, since datetime.datetime.utcnow()
+    cannot be mocked.
+
+    Args:
+        data_str (basestring): The 'date_pushed' or 'date_submitted' from the Update object.
+
+    Returns:
+        int: Number of days since the date in input.
+    """
+    update_time = datetime.datetime.strptime(data_str, '%Y-%m-%d %H:%M:%S')
+    return (datetime.datetime.utcnow() - update_time).days
+
+
 class BodhiClient(OpenIdBaseClient):
     """Python bindings to the Bodhi server REST API."""
 
@@ -651,13 +670,16 @@ def update_str(self, update, minimal=False):
             return update
         if minimal:
             val = ""
+            security = '*' if update['type'] == u'security' else ' '
             date = update['date_pushed'] and update['date_pushed'].split()[0] \
                 or update['date_submitted'].split()[0]
-            val += ' %-37s %-6s %-11s  %-8s  %10s' % (
-                update['builds'][0]['nvr'], update['content_type'], update['type'],
-                update['status'], date)
+            days_in_status = _days_since(update['date_pushed']) if update['date_pushed'] \
+                else _days_since(update['date_submitted'])
+            val += '%s%-40s %-9s  %-8s  %10s (%d)' % (
+                security, update['builds'][0]['nvr'], update['content_type'],
+                update['status'], date, days_in_status)
             for build in update['builds'][1:]:
-                val += '\n %s' % build['nvr']
+                val += '\n  %s' % build['nvr']
             return val
 
         # Content will be formatted as wrapped lines, each line is in format


Test patch:
diff --git a/bodhi/tests/client/__init__.py b/bodhi/tests/client/__init__.py
--- a/bodhi/tests/client/__init__.py
+++ b/bodhi/tests/client/__init__.py
@@ -435,8 +435,8 @@
                   u'show_popups': True}}]})
 
 EXAMPLE_QUERY_OUTPUT_MULTI = """\
-nodejs-grunt-wrap-0.3.0-2.fc25        rpm    newpackage   testing   2017-02-13
-nodejs-grunt-wrap-0.3.0-2.fc25        rpm    newpackage   testing   2017-02-13
+ nodejs-grunt-wrap-0.3.0-2.fc25           rpm        testing   2017-02-13 (17)
+ nodejs-grunt-wrap-0.3.0-2.fc25           rpm        testing   2017-02-13 (17)
 2 updates found (2 shown)
 """
 
diff --git a/bodhi/tests/client/test___init__.py b/bodhi/tests/client/test___init__.py
--- a/bodhi/tests/client/test___init__.py
+++ b/bodhi/tests/client/test___init__.py
@@ -512,6 +512,8 @@ def test_query_single_update(self, send_request):
 
     @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
                 mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings._days_since',
+                mock.MagicMock(return_value=17))
     @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
                 return_value=client_test_data.EXAMPLE_QUERY_MUNCH_MULTI, autospec=True)
     def test_query_multiple_update(self, send_request):
diff --git a/bodhi/tests/client/test_bindings.py b/bodhi/tests/client/test_bindings.py
--- a/bodhi/tests/client/test_bindings.py
+++ b/bodhi/tests/client/test_bindings.py
@@ -988,28 +988,68 @@ def test_bugs(self):
             client_test_data.EXPECTED_UPDATE_OUTPUT.replace(
                 '[-3, 3]', '[-3, 3]\n        Bugs: 1234 - it broke\n            : 1235 - halp')))
 
-    def test_minimal(self):
+    @mock.patch('bodhi.client.bindings.datetime.datetime')
+    def test_minimal(self, mock_datetime):
         """Ensure correct output when minimal is True."""
         client = bindings.BodhiClient()
+        mock_datetime.utcnow = mock.Mock(return_value=datetime(2016, 10, 24, 12, 0, 0))
+        mock_datetime.strptime = datetime.strptime
 
         text = client.update_str(client_test_data.EXAMPLE_UPDATE_MUNCH, minimal=True)
 
-        expected_output = (' bodhi-2.2.4-1.el7                     rpm    bugfix       stable    '
-                           '2016-10-21')
+        expected_output = (' bodhi-2.2.4-1.el7                        rpm        stable    '
+                           '2016-10-21 (2)')
+        self.assertEqual(text, expected_output)
+
+    @mock.patch.dict(
+        client_test_data.EXAMPLE_UPDATE_MUNCH,
+        {u'date_pushed': u'',
+         u'pushed': False,
+         u'status': u'pending'})
+    @mock.patch('bodhi.client.bindings.datetime.datetime')
+    def test_minimal_not_pushed(self, mock_datetime):
+        """Ensure correct output when minimal is True and not yet pushed."""
+        client = bindings.BodhiClient()
+        mock_datetime.utcnow = mock.Mock(return_value=datetime(2016, 10, 5, 23, 0, 0))
+        mock_datetime.strptime = datetime.strptime
+
+        text = client.update_str(client_test_data.EXAMPLE_UPDATE_MUNCH, minimal=True)
+
+        expected_output = (' bodhi-2.2.4-1.el7                        rpm        pending   '
+                           '2016-10-05 (0)')
+        self.assertEqual(text, expected_output)
+
+    @mock.patch.dict(
+        client_test_data.EXAMPLE_UPDATE_MUNCH,
+        {u'type': u'security'})
+    @mock.patch('bodhi.client.bindings.datetime.datetime')
+    def test_minimal_type_security(self, mock_datetime):
+        """Ensure correct output when minimal is True and type security"""
+        client = bindings.BodhiClient()
+        mock_datetime.utcnow = mock.Mock(return_value=datetime(2016, 10, 24, 12, 0, 0))
+        mock_datetime.strptime = datetime.strptime
+
+        text = client.update_str(client_test_data.EXAMPLE_UPDATE_MUNCH, minimal=True)
+
+        expected_output = ('*bodhi-2.2.4-1.el7                        rpm        stable    '
+                           '2016-10-21 (2)')
         self.assertEqual(text, expected_output)
 
     @mock.patch.dict(
         client_test_data.EXAMPLE_UPDATE_MUNCH,
         {u'builds': [{u'epoch': 0, u'nvr': u'bodhi-2.2.4-1.el7', u'signed': True},
                      {u'epoch': 0, u'nvr': u'bodhi-pants-2.2.4-1.el7', u'signed': True}]})
-    def test_minimal_with_multiple_builds(self):
+    @mock.patch('bodhi.client.bindings.datetime.datetime')
+    def test_minimal_with_multiple_builds(self, mock_datetime):
         """Ensure correct output when minimal is True, and multiple builds"""
         client = bindings.BodhiClient()
+        mock_datetime.utcnow = mock.Mock(return_value=datetime(2016, 10, 24, 12, 0, 0))
+        mock_datetime.strptime = datetime.strptime
 
         text = client.update_str(client_test_data.EXAMPLE_UPDATE_MUNCH, minimal=True)
 
-        expected_output = (u' bodhi-2.2.4-1.el7                     rpm    bugfix       stable    '
-                           '2016-10-21\n bodhi-pants-2.2.4-1.el7')
+        expected_output = (' bodhi-2.2.4-1.el7                        rpm        stable    '
+                           '2016-10-21 (2)\n  bodhi-pants-2.2.4-1.el7')
         self.assertEqual(text, expected_output)
 
     @mock.patch.dict(client_test_data.EXAMPLE_UPDATE_MUNCH, {u'request': u'stable'})