Repository: fedora-infra/bodhi

Issue:
If a user edits an update after Bodhi comments it can be pushed, it won't comment again
I noticed that my Bodhi update in Bodhi reached 14 days in testing this week, but Bodhi never commented on it to tell me that it had been in testing long enough to be pushed to stable:

https://bodhi.fedoraproject.org/updates/FEDORA-EPEL-2016-cd060eeb3c

After poking around in the staging db for a while to find out what gives, I found that the update's [```met_testing_requirements()```](https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/server/models/models.py#L1830-L1854) method was returning True because long ago Bodhi had commented to state that the update could be pushed to stable, before I had edited the update and reset its karma and date_testing. The code doesn't take the karma reset event into account:

```
        min_num_days = self.release.mandatory_days_in_testing
        if min_num_days:
            if not self.meets_testing_requirements:
                return False
        else:
            return True
        for comment in self.comments:
            if comment.user.name == u'bodhi' and \
               comment.text.startswith('This update has reached') and \
               'and can be pushed to stable now if the ' \
               'maintainer wishes' in comment.text:
                return True
        return False
```

I think we probably want to take [this loop](https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/server/models/models.py#L691-L698) from the ```karma``` property and make it into a reusable method that just returns comments that should be considered for karma and testing requirements:

```
        for comment in reversed(self.comments):
            if (comment.user.name == u'bodhi' and
                    ('New build' in comment.text or 'Removed build' in comment.text)):
                # We only want to consider comments since the most recent karma reset, which happens
                # whenever a build is added or removed from an Update. Since we are traversing the
                # comments in reverse order, once we find one of these comments we can simply exit
                # this loop.
                break
```

This way we can have ```met_testing_requirements()``` and ```karma()``` both use this new method to get the comments that they use to do what they do.
If a user edits an update after Bodhi comments it can be pushed, it won't comment again
I noticed that my Bodhi update in Bodhi reached 14 days in testing this week, but Bodhi never commented on it to tell me that it had been in testing long enough to be pushed to stable:

https://bodhi.fedoraproject.org/updates/FEDORA-EPEL-2016-cd060eeb3c

After poking around in the staging db for a while to find out what gives, I found that the update's [```met_testing_requirements()```](https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/server/models/models.py#L1830-L1854) method was returning True because long ago Bodhi had commented to state that the update could be pushed to stable, before I had edited the update and reset its karma and date_testing. The code doesn't take the karma reset event into account:

```
        min_num_days = self.release.mandatory_days_in_testing
        if min_num_days:
            if not self.meets_testing_requirements:
                return False
        else:
            return True
        for comment in self.comments:
            if comment.user.name == u'bodhi' and \
               comment.text.startswith('This update has reached') and \
               'and can be pushed to stable now if the ' \
               'maintainer wishes' in comment.text:
                return True
        return False
```

I think we probably want to take [this loop](https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/server/models/models.py#L691-L698) from the ```karma``` property and make it into a reusable method that just returns comments that should be considered for karma and testing requirements:

```
        for comment in reversed(self.comments):
            if (comment.user.name == u'bodhi' and
                    ('New build' in comment.text or 'Removed build' in comment.text)):
                # We only want to consider comments since the most recent karma reset, which happens
                # whenever a build is added or removed from an Update. Since we are traversing the
                # comments in reverse order, once we find one of these comments we can simply exit
                # this loop.
                break
```

This way we can have ```met_testing_requirements()``` and ```karma()``` both use this new method to get the comments that they use to do what they do.


Discussion:
A good way to test this when fixing it is to add a test to this module:

https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/tests/server/scripts/test_approve_testing.py
A good way to test this when fixing it is to add a test to this module:

https://github.com/fedora-infra/bodhi/blob/2.4.0/bodhi/tests/server/scripts/test_approve_testing.py

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -797,17 +797,7 @@ def _composite_karma(self):
         positive_karma = 0
         negative_karma = 0
         users_counted = set()
-        # We want to traverse the comments in reverse order so we only consider the most recent
-        # comments from any given user, and only the comments since the most recent karma reset
-        # event.
-        for comment in reversed(self.comments):
-            if (comment.user.name == u'bodhi' and
-                    ('New build' in comment.text or 'Removed build' in comment.text)):
-                # We only want to consider comments since the most recent karma reset, which happens
-                # whenever a build is added or removed from an Update. Since we are traversing the
-                # comments in reverse order, once we find one of these comments we can simply exit
-                # this loop.
-                break
+        for comment in self.comments_since_karma_reset:
             if comment.karma and not comment.anonymous and comment.user.name not in users_counted:
                 # Make sure we only count the last comment this user made
                 users_counted.add(comment.user.name)
@@ -818,6 +808,28 @@ def _composite_karma(self):
 
         return positive_karma, negative_karma
 
+    @property
+    def comments_since_karma_reset(self):
+        """
+        Return the comments since the most recent karma reset event, which
+        occurs whenever a build is added or removed from an Update.
+        :return: an iterable of recent comments
+        :rtype: generator
+        """
+        # We want to traverse the comments in reverse order so we only consider
+        # the most recent comments from any given user and only the comments
+        # since the most recent karma reset event.
+        for comment in reversed(self.comments):
+            if (comment.user.name == u'bodhi' and
+                    ('New build' in comment.text or 'Removed build' in comment.text)):
+                # We only want to consider comments since the most recent karma
+                # reset, which happens whenever a build is added or removed
+                # from an Update. Since we are traversing the comments in
+                # reverse order, once we find one of these comments we can
+                # simply exit this loop.
+                break
+            yield comment
+
     @classmethod
     def new(cls, request, data):
         """ Create a new update """
@@ -1954,7 +1966,7 @@ def met_testing_requirements(self):
                 return False
         else:
             return True
-        for comment in self.comments:
+        for comment in self.comments_since_karma_reset:
             if comment.user.name == u'bodhi' and \
                comment.text.startswith('This update has reached') and \
                'and can be pushed to stable now if the ' \


Test patch:
diff --git a/bodhi/tests/server/scripts/test_approve_testing.py b/bodhi/tests/server/scripts/test_approve_testing.py
--- a/bodhi/tests/server/scripts/test_approve_testing.py
+++ b/bodhi/tests/server/scripts/test_approve_testing.py
@@ -333,3 +333,38 @@ def test_non_autokarma_update_without_mandatory_days_in_testing(self):
             c.user.name
             for c in self.db.query(models.Comment).order_by(models.Comment.timestamp).all()]
         self.assertEqual(usernames, [u'guest', u'anonymous', u'hunter2'])
+
+    @patch.dict(config, [('fedora.mandatory_days_in_testing', 14)])
+    def test_subsequent_comments_after_initial_push_comment(self):
+        """
+        If a user edits an update after Bodhi comments a testing_approval_msg,
+        Bodhi should send an additional testing_approval_msg when the revised
+        update is eligible to be pushed to stable.
+
+        See https://github.com/fedora-infra/bodhi/issues/1310
+        """
+        update = self.db.query(models.Update).all()[0]
+        update.request = None
+        update.status = models.UpdateStatus.testing
+        update.date_testing = datetime.now() - timedelta(days=14)
+        self.db.flush()
+
+        with patch(
+                'bodhi.server.scripts.approve_testing._get_db_session', return_value=self.db):
+            approve_testing.main(['nosetests', 'some_config.ini'])
+            update.comment(self.db, u"Removed build", 0, u'bodhi')
+            approve_testing.main(['nosetests', 'some_config.ini'])
+
+        bodhi = self.db.query(models.User).filter_by(name=u'bodhi').one()
+        cmnts = self.db.query(models.Comment).filter_by(update_id=update.id, user_id=bodhi.id)
+        # There are 3 comments: testing_approval_msg, build change, testing_approval_msg
+        self.assertEqual(cmnts.count(), 3)
+        self.assertEqual(
+            cmnts[0].text,
+            config.get('testing_approval_msg') %
+            update.release.mandatory_days_in_testing)
+        self.assertEqual(cmnts[1].text, 'Removed build')
+        self.assertEqual(
+            cmnts[2].text,
+            config.get('testing_approval_msg') %
+            update.release.mandatory_days_in_testing)