Repository: fedora-infra/bodhi

Issue:
EnumSymbol needs an __lt__() method
The Composer crashed on production with this traceback:

```
Traceback (most recent call last):
  File "/usr/lib/python3.7/site-packages/moksha/hub/api/consumer.py", line 207, in _do_work
    self.consume(message)
  File "/usr/lib/python3.7/site-packages/bodhi/server/consumers/masher.py", line 195, in consume
    self.work(msg)
  File "/usr/lib/python3.7/site-packages/bodhi/server/consumers/masher.py", line 249, in work
    for compose in self._get_composes(body):
  File "/usr/lib/python3.7/site-packages/bodhi/server/consumers/masher.py", line 231, in _get_composes
    return [c.__json__(composer=True) for c in composes]
  File "/usr/lib/python3.7/site-packages/bodhi/server/consumers/masher.py", line 231, in <listcomp>
    return [c.__json__(composer=True) for c in composes]
  File "/usr/lib/python3.7/site-packages/bodhi/server/models.py", line 3877, in __json__
    include=include)
  File "/usr/lib/python3.7/site-packages/bodhi/server/models.py", line 354, in __json__
    include=include)
  File "/usr/lib/python3.7/site-packages/bodhi/server/models.py", line 394, in _to_json
    attribute = getattr(obj, name)
  File "/usr/lib/python3.7/site-packages/bodhi/server/models.py", line 3749, in content_type
    if self.updates:
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/attributes.py", line 242, in __get__
    return self.impl.get(instance_state(instance), dict_)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/attributes.py", line 601, in get
    value = self.callable_(state, passive)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/strategies.py", line 631, in _load_for_state
    session, state, primary_key_identity, passive)
  File "<string>", line 1, in <lambda>
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/strategies.py", line 771, in _emit_lazyload
    with_post_criteria(set_default_params).all()
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/ext/baked.py", line 457, in all
    return list(self)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/ext/baked.py", line 359, in __iter__
    self.session._autoflush()
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/session.py", line 1434, in _autoflush
    self.flush()
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/session.py", line 2254, in flush
    self._flush(objects)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/session.py", line 2380, in _flush
    transaction.rollback(_capture_exception=True)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/util/compat.py", line 249, in reraise
    raise value
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/session.py", line 2344, in _flush
    flush_context.execute()
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 391, in execute
    rec.execute(self)
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/unitofwork.py", line 556, in execute
    uow
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 156, in save_obj
    base_mapper, states, uowtransaction
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 286, in _organize_states_for_save
    states):
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 1258, in _connections_for_states
    for state in _sort_states(states):
  File "/usr/lib64/python3.7/site-packages/sqlalchemy/orm/persistence.py", line 1280, in _sort_states
    sorted(persistent, key=lambda q: q.key[1])
TypeError: '<' not supported between instances of 'EnumSymbol' and 'EnumSymbol'
```

I found [an sqlalchemy issue](https://bitbucket.org/zzzeek/sqlalchemy/issues/4232/another-case-of-illegal-sorting-in-python) where this was discussed and it sounds like we just need to give our EnumSymbol an ```__lt__()``` method and call it a day. Something like this will probably work:

```
def __lt__(self, other):
    """
    Return True if self.value is less than other.value.

    Args:
        other (EnumSymbol): The other EnumSymbol we are being compared to.
    Returns:
        bool: True if self.value is less than other.value, False otherwise.
    """"
    return self.value < other.value
```

I tried something like the above on production and it did get us past this crash.


Discussion:

Resolving patch:
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -74,6 +74,17 @@ def __init__(self, cls_, name, value, description):
         self.value = value
         self.description = description
 
+    def __lt__(self, other):
+        """
+        Return True if self.value is less than other.value.
+
+        Args:
+            other (EnumSymbol): The other EnumSymbol we are being compared to.
+        Returns:
+            bool: True if self.value is less than other.value, False otherwise.
+        """
+        return self.value < other.value
+
     def __reduce__(self):
         """
         Allow unpickling to return the symbol linked to the DeclEnum class.


Test patch:
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -459,6 +459,17 @@ def test___json__(self):
 
         self.assertEqual(s.__json__(), 'value')
 
+    def test___lt__(self):
+        """Ensure that EnumSymbols support sorting."""
+        open_source = model.EnumSymbol(model.UpdateStatus, 'open source', 'open_source',
+                                       'Open Source')
+        closed_source = model.EnumSymbol(model.UpdateStatus, 'closed source',
+                                         'closed_source', 'Closed Source')
+
+        self.assertLess(closed_source, open_source)
+        self.assertGreater(open_source, closed_source)
+        self.assertNotEqual(open_source, closed_source)
+
     def test___reduce__(self):
         """Ensure correct operation of the __reduce__() method by pickling an instance."""
         s = model.EnumSymbol(model.UpdateStatus, 'testing', 'testing', 'testing')