Repository: fedora-infra/bodhi

Issue:
Can not create multiple buildroot overrides
In bodhi 2 the "Candidate build" field in the form for creating buildroot overrides only accepts one value. In bodhi 1 this could be a long list of builds. It is not uncommon that you need the same buildroot override in all releases (except rawhide). This mean that I now have to fill the bodhi 2 form 6 times for F23, F22, F21, EPEL7, EPEL6 and EPEL5, instead of just once in bodhi 1. If I need overrides for two packagas I need to fill the form 12 times instead of once. This takes a lot of time that could be used for more interesting things.

Also, if you list more than one build in the "Candidate build" field and press submit, the submission just silently fails without giving any feedback. There is no information given telling the sumitter why nothing happens.



Discussion:
Agreed.  Good point.

I just ran into this as well.


Resolving patch:
diff --git a/bodhi/models/models.py b/bodhi/models/models.py
--- a/bodhi/models/models.py
+++ b/bodhi/models/models.py
@@ -1984,7 +1984,8 @@ def new(cls, request, **data):
         build = data['build']
 
         if build.override is not None:
-            request.errors.add('body', 'nvr', 'This build already is in a buildroot override')
+            request.errors.add('body', 'nvr',
+                               '%s is already in a override' % build.nvr)
             return
 
         old_build = db.query(Build).filter(and_(
diff --git a/bodhi/services/overrides.py b/bodhi/services/overrides.py
--- a/bodhi/services/overrides.py
+++ b/bodhi/services/overrides.py
@@ -23,9 +23,13 @@
 from bodhi import log
 from bodhi.models import Build, BuildrootOverride, Package, Release
 import bodhi.schemas
-from bodhi.validators import (validate_override_build, validate_expiration_date,
-                              validate_packages, validate_releases,
-                              validate_username)
+from bodhi.validators import (
+    validate_override_builds,
+    validate_expiration_date,
+    validate_packages,
+    validate_releases,
+    validate_username,
+)
 
 
 override = Service(name='override', path='/overrides/{nvr}',
@@ -136,13 +140,17 @@ def query_overrides(request):
 @overrides.post(schema=bodhi.schemas.SaveOverrideSchema,
                 acl=bodhi.security.packagers_allowed_acl,
                 accept=("application/json", "text/json"), renderer='json',
-                validators=(validate_override_build, validate_expiration_date),
-                )
+                validators=(
+                    validate_override_builds,
+                    validate_expiration_date,
+                ))
 @overrides.post(schema=bodhi.schemas.SaveOverrideSchema,
                 acl=bodhi.security.packagers_allowed_acl,
                 accept=("application/javascript"), renderer="jsonp",
-                validators=(validate_override_build, validate_expiration_date),
-                )
+                validators=(
+                    validate_override_builds,
+                    validate_expiration_date,
+                ))
 def save_override(request):
     """Save a buildroot override
 
@@ -153,18 +161,32 @@ def save_override(request):
     data = request.validated
 
     edited = data.pop("edited")
-    build = data['build']
 
+    caveats = []
     try:
         if edited is None:
-            log.info("Creating a new buildroot override: %s" % data['nvr'])
-
-            override = BuildrootOverride.new(request, build=build,
-                                             submitter=request.user,
-                                             notes=data['notes'],
-                                             expiration_date=data['expiration_date'],
-                                             )
-
+            builds = data['builds']
+            overrides = []
+            if len(builds) > 1:
+                caveats.append({
+                    'name': 'nvrs',
+                    'description': 'Your override submission was '
+                    'split into %i.' % len(builds)
+                })
+            for build in builds:
+                log.info("Creating a new buildroot override: %s" % build.nvr)
+                overrides.append(BuildrootOverride.new(
+                    request,
+                    build=build,
+                    submitter=request.user,
+                    notes=data['notes'],
+                    expiration_date=data['expiration_date'],
+                ))
+
+            if len(builds) > 1:
+                result = dict(overrides=overrides)
+            else:
+                result = overrides[0]
         else:
             log.info("Editing buildroot override: %s" % edited)
 
@@ -174,16 +196,25 @@ def save_override(request):
                 request.errors.add('body', 'edited', 'No such build')
                 return
 
-            override = BuildrootOverride.edit(
+            result = BuildrootOverride.edit(
                     request, edited=edited, submitter=request.user,
                     notes=data["notes"], expired=data["expired"],
                     expiration_date=data["expiration_date"]
                     )
 
+            if not result:
+                # Some error inside .edit(...)
+                return
+
     except Exception as e:
         log.exception(e)
         request.errors.add('body', 'override',
                            'Unable to save buildroot override: %s' % e)
         return
 
-    return override
+    if not isinstance(result, dict):
+        result = result.__json__()
+
+    result['caveats'] = caveats
+
+    return result
diff --git a/bodhi/validators.py b/bodhi/validators.py
--- a/bodhi/validators.py
+++ b/bodhi/validators.py
@@ -31,6 +31,8 @@
                      Bug, TestCase, ReleaseState, Stack)
 from .util import get_nvr, tokenize, taskotron_results
 
+import bodhi.schemas
+
 
 # This one is a colander validator which is different from the cornice
 # validators defined elsehwere.
@@ -683,15 +685,37 @@ def validate_comment_id(request):
         request.errors.status = HTTPNotFound.code
 
 
-def validate_override_build(request):
+def validate_override_builds(request):
     """ Ensure that the build is properly tagged """
-    nvr = request.validated['nvr']
+    nvrs = bodhi.schemas.splitter(request.validated['nvr'])
     db = request.db
 
-    build = Build.get(nvr, db)
+    if not nvrs:
+        request.errors.add('body', 'nvr',
+                           'A comma-separated list of NVRs is required.')
+        return
 
-    if build is not None:
+    if len(nvrs) != 1 and request.validated['edited']:
+        request.errors.add('body', 'nvr', 'Cannot combine multiple NVRs '
+                           'with editing a buildroot override.')
+        return
+
+    builds = []
+    for nvr in nvrs:
+        result = _validate_override_build(request, nvr, db)
+        if not result:
+            # Then there was some error.
+            return
+        builds.append(result)
+
+    request.validated['builds'] = builds
 
+
+def _validate_override_build(request, nvr, db):
+    """ Workhorse function for validate_override_builds """
+    build = Build.get(nvr, db)
+    print "in _validate with build", build
+    if build is not None:
         if not build.release:
             # Oddly, the build has no associated release.  Let's try to figure
             # that out and apply it.
@@ -726,8 +750,13 @@ def validate_override_build(request):
         tag_types, tag_rels = Release.get_tags()
         valid_tags = tag_types['candidate'] + tag_types['testing']
 
-        tags = [tag['name'] for tag in request.koji.listTags(nvr)
-                if tag['name'] in valid_tags]
+        try:
+            tags = [tag['name'] for tag in request.koji.listTags(nvr)
+                    if tag['name'] in valid_tags]
+        except Exception as e:
+            request.errors.add('body', 'nvr', "Couldn't determine koji tags "
+                               "for %s, %r" % (nvr, str(e)))
+            return
 
         release = Release.from_tags(tags, db)
 
@@ -746,7 +775,7 @@ def validate_override_build(request):
         db.add(build)
         db.flush()
 
-    request.validated['build'] = build
+    return build
 
 
 def validate_expiration_date(request):


Test patch:
diff --git a/bodhi/tests/functional/test_overrides.py b/bodhi/tests/functional/test_overrides.py
--- a/bodhi/tests/functional/test_overrides.py
+++ b/bodhi/tests/functional/test_overrides.py
@@ -214,6 +214,53 @@ def test_create_override(self, publish):
                           expiration_date.strftime("%Y-%m-%d %H:%M:%S"))
         self.assertEquals(o['expired_date'], None)
 
+    @mock.patch('bodhi.notifications.publish')
+    def test_create_override_multiple_nvr(self, publish):
+        session = DBSession()
+
+        release = Release.get(u'F17', session)
+        package = Package(name=u'not-bodhi')
+        session.add(package)
+        build1 = Build(nvr=u'not-bodhi-2.0-2.fc17', package=package,
+                      release=release)
+        session.add(build1)
+        session.flush()
+
+        package = Package(name=u'another-not-bodhi')
+        session.add(package)
+        build2 = Build(nvr=u'another-not-bodhi-2.0-2.fc17', package=package,
+                      release=release)
+        session.add(build2)
+        session.flush()
+
+        expiration_date = datetime.utcnow() + timedelta(days=1)
+
+        data = {
+            'nvr': ','.join([build1.nvr, build2.nvr]),
+            'notes': u'blah blah blah',
+            'expiration_date': expiration_date,
+            'csrf_token': self.get_csrf_token(),
+        }
+        res = self.app.post('/overrides/', data)
+
+        self.assertEquals(len(publish.call_args_list), 2)
+
+        result = res.json_body
+        self.assertEquals(result['caveats'][0]['description'],
+                          'Your override submission was split into 2.')
+
+        o1, o2 = result['overrides']
+        self.assertEquals(o1['build_id'], build1.id)
+        self.assertEquals(o1['notes'], 'blah blah blah')
+        self.assertEquals(o1['expiration_date'],
+                          expiration_date.strftime("%Y-%m-%d %H:%M:%S"))
+        self.assertEquals(o1['expired_date'], None)
+        self.assertEquals(o2['build_id'], build2.id)
+        self.assertEquals(o2['notes'], 'blah blah blah')
+        self.assertEquals(o2['expiration_date'],
+                          expiration_date.strftime("%Y-%m-%d %H:%M:%S"))
+        self.assertEquals(o2['expired_date'], None)
+
     @mock.patch('bodhi.notifications.publish')
     def test_create_override_too_long(self, publish):
         session = DBSession()
@@ -359,6 +406,20 @@ def test_edit_expiration_date(self):
                           expiration_date.strftime("%Y-%m-%d %H:%M:%S"))
         self.assertEquals(override['expired_date'], None)
 
+    def test_edit_fail_on_multiple(self):
+        old_nvr = u'bodhi-2.0-1.fc17'
+
+        res = self.app.get('/overrides/%s' % old_nvr)
+        o = res.json_body['override']
+        o.update({'nvr': old_nvr + ',wat', 'notes': 'blah blah blah blah',
+                  'edited': old_nvr, 'csrf_token': self.get_csrf_token()})
+        res = self.app.post('/overrides/', o, status=400)
+        result = res.json_body
+        self.assertEquals(
+            result['errors'][0]['description'],
+            'Cannot combine multiple NVRs with editing a buildroot override.',
+        )
+
     @mock.patch('bodhi.notifications.publish')
     def test_expire_override(self, publish):
         old_nvr = u'bodhi-2.0-1.fc17'