Repository: fedora-infra/bodhi

Issue:
The CLI edit command could also accept update titles to identify which update is being edited
Right now the CLI accepts the update id as a parameter to identify which update is being edited.

We could make [a couple regexes used in the bindings](https://github.com/fedora-infra/bodhi/blob/develop/bodhi/client/bindings.py#L238-L241) (or even a function that does what the cited code block does) available at the bindings module level so the client can share the code:

```
if re.search(r'(\.el|\.fc)\d\d?', kwargs['package']):
    kwargs['builds'] = kwargs['package']
elif re.search(r'FEDORA-(EPEL-)?\d{4,4}', kwargs['package']):
    kwargs['updateid'] = kwargs['package']
```

If we did this, then users could type ```bodhi updates edit --user bowlofeggs --notes "check it out" FEDORA-2017-4917bb3d50``` or ```bodhi updates edit --user bowlofeggs --notes "check it out" bodhi-2.5.0-1.fc26``` and the two commands would edit the same update.


Discussion:

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -17,6 +17,7 @@
 import subprocess
 import sys
 import traceback
+import re
 
 import click
 
@@ -175,13 +176,26 @@ def new(user, password, url, **kwargs):
             traceback.print_exc()
 
 
+def _validate_edit_update(ctx, param, value):
+    """
+    Callback used by click to validate the update argument given to the updates edit command.
+    the update argument can only be update id or update title
+    """
+    if re.search(bindings.UPDATE_ID_RE, value)\
+       or re.search(bindings.UPDATE_TITLE_RE, value):
+        return value
+    else:
+        raise click.BadParameter("Please provide an Update ID or an Update Title")
+
+
 @updates.command()
 @add_options(new_edit_options)
-@click.argument('updateid')
+@click.argument('update', callback=_validate_edit_update)
 @url_option
 def edit(user, password, url, **kwargs):
     """
     Edit an existing update.
+    The update argument can be an update id or the update title.
 
     Args:
         user (unicode): The username to authenticate as.
@@ -196,10 +210,13 @@ def edit(user, password, url, **kwargs):
     kwargs['notes'] = _get_notes(**kwargs)
 
     try:
-        query_param = {'updateid': kwargs['updateid']}
-        del(kwargs['updateid'])
-        resp = client.query(**query_param)
-        title = resp['updates'][0]['title']
+        if re.search(bindings.UPDATE_ID_RE, kwargs['update']):
+            query_param = {'updateid': kwargs['update']}
+            resp = client.query(**query_param)
+            title = resp['updates'][0]['title']
+        elif re.search(bindings.UPDATE_TITLE_RE, kwargs['update']):
+            title = kwargs['update']
+        del(kwargs['update'])
         kwargs['builds'] = title
         kwargs['edited'] = title
 
diff --git a/bodhi/client/bindings.py b/bodhi/client/bindings.py
--- a/bodhi/client/bindings.py
+++ b/bodhi/client/bindings.py
@@ -46,6 +46,9 @@
 STG_BASE_URL = 'https://bodhi.stg.fedoraproject.org/'
 STG_OPENID_API = 'https://id.stg.fedoraproject.org/api/v1/'
 
+UPDATE_ID_RE = r'FEDORA-(EPEL-)?\d{4,4}'
+UPDATE_TITLE_RE = r'(\.el|\.fc)\d\d?'
+
 
 class BodhiClientException(FedoraClientError):
     pass
@@ -235,9 +238,9 @@ def query(self, **kwargs):
         if 'package' in kwargs:
             # for Bodhi 1, 'package' could be a package name, build, or
             # update ID, so try and figure it out
-            if re.search(r'(\.el|\.fc)\d\d?', kwargs['package']):
+            if re.search(UPDATE_TITLE_RE, kwargs['package']):
                 kwargs['builds'] = kwargs['package']
-            elif re.search(r'FEDORA-(EPEL-)?\d{4,4}', kwargs['package']):
+            elif re.search(UPDATE_ID_RE, kwargs['package']):
                 kwargs['updateid'] = kwargs['package']
             else:
                 kwargs['packages'] = kwargs['package']


Test patch:
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
@@ -449,6 +449,68 @@ def test_notes_and_notes_file(self):
             self.assertEqual(result.exit_code, 1)
             self.assertEqual(result.output, u'ERROR: Cannot specify --notes and --notes-file\n')
 
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
+                return_value=client_test_data.EXAMPLE_UPDATE_MUNCH, autospec=True)
+    def test_update_title(self, send_request):
+        """
+        Assert that we can successfully edit an update using the update title.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit, ['drupal7-i18n-1.17-1.fc26', '--user', 'bowlofeggs',
+                          '--password', 's3kr3t', '--notes', 'this is an edited note',
+                          '--url', 'http://localhost:6543'])
+
+        self.assertEqual(result.exit_code, 0)
+        bindings_client = send_request.mock_calls[0][1][0]
+        send_request.assert_called_with(
+            bindings_client, 'updates/', auth=True, verb='POST',
+            data={
+                'close_bugs': True, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
+                'staging': False, 'builds': u'drupal7-i18n-1.17-1.fc26', 'autokarma': True,
+                'edited': u'drupal7-i18n-1.17-1.fc26', 'suggest': None,
+                'notes': u'this is an edited note', 'notes_file': None,
+                'request': None, 'bugs': u'', 'unstable_karma': None, 'type': 'bugfix'})
+
+    def test_wrong_update_title_argument(self):
+        """
+         Assert that an error is given if the edit update argument given is not an update id
+         nor an update title.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit, ['drupal7-i18n-1.17-1', '--user', 'bowlofeggs',
+                          '--password', 's3kr3t', '--notes', 'this is an edited note',
+                          '--url', 'http://localhost:6543'])
+        self.assertEqual(result.exit_code, 2)
+        expected = u'Usage: edit [OPTIONS] UPDATE\n\n' \
+                   u'Error: Invalid value for "update": ' \
+                   u'Please provide an Update ID or an Update Title\n'
+
+        self.assertEqual(result.output, expected)
+
+    def test_wrong_update_id_argument(self):
+        """
+         Assert that an error is given if the edit update argument given is not an update id
+         nor an update title.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit, ['FEDORA-20-cc8582d738', '--user', 'bowlofeggs',
+                          '--password', 's3kr3t', '--notes', 'this is an edited note',
+                          '--url', 'http://localhost:6543'])
+        self.assertEqual(result.exit_code, 2)
+        expected = u'Usage: edit [OPTIONS] UPDATE\n\n' \
+                   u'Error: Invalid value for "update": ' \
+                   u'Please provide an Update ID or an Update Title\n'
+
+        self.assertEqual(result.output, expected)
+
 
 class TestEditBuilrootOverrides(unittest.TestCase):
     """