From de67f4aa0bcefb776251d6a00a32cbd9ae8b41f9 Mon Sep 17 00:00:00 2001
From: Corey Maynard <me@coreymaynard.com>
Date: Tue, 21 Jun 2011 14:34:01 -0400
Subject: [PATCH] Adds KarmaTypes to Bodhi. Allows for more detailed karma
 awarding using DeclEnum's.

---
 bodhi/__init__.py                 |    2 +-
 bodhi/models/models.py            |  218 +++++++++++++++++++------------------
 bodhi/resources.py                |    7 +-
 bodhi/templates/model.mak         |   22 ++--
 bodhi/tests/models/__init__.py    |    2 +-
 bodhi/tests/models/test_models.py |   25 +---
 bodhi/util.py                     |    2 +-
 7 files changed, 139 insertions(+), 139 deletions(-)

diff --git a/bodhi/__init__.py b/bodhi/__init__.py
index d3fc95c..5085484 100644
--- a/bodhi/__init__.py
+++ b/bodhi/__init__.py
@@ -44,8 +44,8 @@ def main(global_config, **settings):
                     accept='text/html',
                     renderer='bodhi:templates/instance.mak')
     config.add_view('bodhi.views.view_model',
-                    accept='text/html',
                     context='bodhi.resources.BodhiResource',
+                    accept='text/html',
                     renderer='bodhi:templates/model.mak')
 
     return config.make_wsgi_app()
diff --git a/bodhi/models/models.py b/bodhi/models/models.py
index 42ef3a4..ebd8801 100644
--- a/bodhi/models/models.py
+++ b/bodhi/models/models.py
@@ -13,7 +13,7 @@ from sqlalchemy import Unicode, UnicodeText, PickleType, Integer, Boolean
 from sqlalchemy import DateTime
 from sqlalchemy import Table, Column, ForeignKey
 from sqlalchemy import and_, or_
-from sqlalchemy.orm import scoped_session, sessionmaker, relation, relationship
+from sqlalchemy.orm import scoped_session, sessionmaker, relation
 from sqlalchemy.ext.declarative import declarative_base, synonym_for
 from sqlalchemy.exc import IntegrityError
 from sqlalchemy.orm.exc import NoResultFound
@@ -28,7 +28,7 @@ log = logging.getLogger(__name__)
 
 config = {} # FIXME!
 from bunch import Bunch
-identity = Bunch(current=Bunch(user_name=u'Bob'))
+identity = Bunch(current=Bunch(user_name=u'bodhi'))
 
 class BodhiBase(object):
     """ Our custom model base class """
@@ -98,6 +98,13 @@ class UpdateRequest(DeclEnum):
     obsolete = 'O', 'obsolete'
     unpush = 'U', 'unpush'
 
+class KarmaType(DeclEnum):
+    none = 'N', 'no karma'
+    works = 'W', 'still works'
+    new_errors = 'E', 'new errors'
+    passes = 'P', 'passes'
+    fails = 'F', 'fails'
+
 ##
 ## Association tables
 ##
@@ -110,13 +117,14 @@ class UpdateRequest(DeclEnum):
 #        Column('update_id', Integer, ForeignKey('updates.id')),
 #        Column('build_id', Integer, ForeignKey('builds.id')))
 
-
-##
-
 update_bug_table = Table('update_bug_table', metadata,
         Column('update_id', Integer, ForeignKey('updates.id')),
         Column('bug_id', Integer, ForeignKey('bugs.id')))
 
+comment_bug_table = Table('comment_bug_table', metadata,
+        Column('comment_id', Integer, ForeignKey('comments.id')),
+        Column('bug_id', Integer, ForeignKey('bugs.id')))
+
 update_cve_table = Table('update_cve_table', metadata,
         Column('update_id', Integer, ForeignKey('updates.id')),
         Column('cve_id', Integer, ForeignKey('cves.id')))
@@ -149,8 +157,6 @@ class Package(Base):
     id = Column(Integer, primary_key=True)
     name = Column(Unicode(50), unique=True, nullable=False)
     committers = Column(PickleType, default=None)
-    stable_karma = Column(Integer, default=3)
-    unstable_karma = Column(Integer, default=-3)
 
     builds = relation('Build', backref='package')
 
@@ -277,8 +283,7 @@ class Update(Base):
 
     id = Column(Integer, primary_key=True)
     _title = Column('title', UnicodeText)
-    # TODO: more flexible karma schema
-    karma = Column(Integer, default=0)
+
     notes = Column(UnicodeText, nullable=False) # Mandatory notes
 
     # Enumerated types
@@ -324,8 +329,8 @@ class Update(Base):
     builds = relation('Build', backref='update', lazy=False)
 
     # Many-to-many relationships
-    bugs = relationship('Bug', secondary=update_bug_table,
-                        backref='updates', lazy=False)
+    bugs = relation('Bug', secondary=update_bug_table,
+                    backref='updates', lazy=False)
     cves = relation('CVE', secondary=update_cve_table,
                     backref='updates', lazy=False)
 
@@ -346,12 +351,19 @@ class Update(Base):
         return delim.join([build.nvr for build in self.builds])
 
     @property
-    def stable_karma(self):
-        return self.builds[0].package.stable_karma
+    def karma_counts(self):
+        """ Return a dictionary of all the counts for each karma type on this update """
+        types = {}
+        for c in self.comments:
+            if types.has_key(c.karma.name):
+                types[c.karma.name] += 1
+            else:
+                types[c.karma.name] = 1
+        return types
 
-    @property
-    def unstable_karma(self):
-        return self.builds[0].package.unstable_karma
+    def karma_by_type(self, type):
+        """ Return the number points awarded to a specific karma type """
+        return self.karma_counts[type] if self.karma_counts.has_key(type) else 0
 
     def get_bugstring(self, show_titles=False):
         """Return a space-delimited string of bug numbers for this update """
@@ -439,7 +451,7 @@ class Update(Base):
         if action is UpdateRequest.unpush:
             self.unpush()
             self.comment(u'This update has been unpushed',
-                         author=identity.current.user_name)
+                         user=identity.current)
             flash_log("%s has been unpushed" % self.title)
             return
         elif action is UpdateRequest.obsolete:
@@ -463,7 +475,7 @@ class Update(Base):
         flash_log("%s has been submitted for %s" % (
             self.title, action.description))
         self.comment(u'This update has been submitted for %s' %
-                action.description, author=identity.current.user_name)
+                action.description, user=identity.current)
         mail.send_admin(action.description, self)
 
     def request_complete(self):
@@ -534,12 +546,12 @@ class Update(Base):
         """
         if self.status is UpdateStatus.stable:
             self.comment(u'This update has been pushed to stable',
-                         author=u'bodhi')
+                         user=u'bodhi')
         elif self.status is UpdateStatus.testing:
             self.comment(u'This update has been pushed to testing',
-                         author=u'bodhi')
+                         user=u'bodhi')
         elif self.status is UpdateStatus.obsolete:
-            self.comment(u'This update has been obsoleted', author=u'bodhi')
+            self.comment(u'This update has been obsoleted', user=u'bodhi')
 
     def send_update_notice(self):
         log.debug("Sending update notice for %s" % self.title)
@@ -588,9 +600,12 @@ class Update(Base):
             val += u"  Update ID: %s\n" % self.alias
         val += u"""    Release: %s
      Status: %s
-       Type: %s
-      Karma: %d""" % (self.release.long_name, self.status.description, 
-                      self.type.description, self.karma)
+       Type: %s\n""" % (self.release.long_name, self.status.description, 
+                      self.type.description)
+        
+        for c in self.comments:
+            val += u"    %s: %d\n" % (c.karma.description.title(), self.karma_counts[c.karma.name])
+
         if self.request != None:
             val += u"\n    Request: %s" % self.request.description
         if len(self.bugs):
@@ -608,19 +623,19 @@ class Update(Base):
             val += u"   Comments: "
             comments = []
             for comment in self.comments:
-                if comment.anonymous:
-                    anonymous = " (unauthenticated)"
+                if comment.user:
+                    user = comment.user.name
                 else:
-                    anonymous = ""
-                comments.append(u"%s%s%s - %s (karma %s)" % (' ' * 13,
-                                comment.author, anonymous, comment.timestamp,
-                                comment.karma))
+                    user = "(unauthenticated)"
+                comments.append(u"%s%s - %s" % (' ' * 13,
+                                user, comment.timestamp))
                 if comment.text:
                     text = wrap(comment.text, initial_indent=' ' * 13,
                                 subsequent_indent=' ' * 13, width=67)
                     comments.append(u'\n'.join(text))
             val += u'\n'.join(comments).lstrip() + u'\n'
-        val += u"\n  %s\n" % (config.get('base_address') + url(self.get_url()))
+        #TODO: implement url function
+        #val += u"\n  %s\n" % (config.get('base_address') + url(self.get_url()))
         return val
 
     def get_build_tag(self):
@@ -701,7 +716,7 @@ class Update(Base):
 
     def get_pushed_color(self):
         age = get_age_in_days(self.date_pushed)
-        if age == 0 or self.karma < 0:
+        if age == 0:
             color = '#ff0000' # red
         elif age < 4:
             color = '#ff6600' # orange
@@ -711,64 +726,47 @@ class Update(Base):
             color = '#00ff00' # green
         return color
 
-    def comment(self, text, karma=0, author=None, anonymous=False):
-        """ Add a comment to this update, adjusting the karma appropriately.
+    def comment(self, text, karma=KarmaType.none, bugs=None, user=None):
+        """ Add a comment to this update, storing optional karma type,
+        any bugs related to this specific comment, either the registered user
+        or anonymous author. """
+        if not self.request and (karma == KarmaType.passes or karma == KarmaType.works) and \
+                self.karma_by_type('works') + self.karma_by_type('passes') == 2:
+            log.info("Automatically marking %s as stable" % self.title)
+            self.request = UpdateRequest.stable
+            self.pushed = False
+            self.date_pushed = None
+            mail.send(self.get_maintainers(), 'stablekarma', self)
+            mail.send_admin('stablekarma', self)
+        if self.status is UpdateStatus.testing and \
+                (karma == KarmaType.fails or karma == KarmaType.new_errors) and \
+                self.karma_by_type('fails') + self.karma_by_type('new_errors') > 0:
+            log.info("Automatically unpushing %s" % self.title)
+            self.obsolete()
+            mail.send(self.get_maintainers(), 'unstable', self)
+        comment = Comment(text=text, karma=karma)
 
-        Each user has the ability to comment as much as they want, but only
-        their last karma adjustment will be counted.  If the karma reaches
-        the 'stable_karma' value, then request that this update be marked
-        as stable.  If it reaches the 'unstable_karma', it is unpushed.
-        """
-        if not author:
-            author = identity.current.user_name
-        if not anonymous and karma != 0 and \
-           not filter(lambda c: c.user.name == author and c.karma == karma,
-                      self.comments):
-            mycomments = [c.karma for c in self.comments if c.user.name == author]
-            if karma == 1 and -1 in mycomments:
-                self.karma += 2
-            elif karma == -1 and 1 in mycomments:
-                self.karma -= 2
-            else:
-                self.karma += karma
-            log.info("Updated %s karma to %d" % (self.title, self.karma))
-            if self.stable_karma != 0 and self.stable_karma == self.karma:
-                log.info("Automatically marking %s as stable" % self.title)
-                self.request = UpdateRequest.stable
-                self.pushed = False
-                self.date_pushed = None
-                mail.send(self.get_maintainers(), 'stablekarma', self)
-                mail.send_admin('stablekarma', self)
-            if self.status is UpdateStatus.testing \
-                    and self.unstable_karma != 0 \
-                    and self.karma == self.unstable_karma:
-                log.info("Automatically unpushing %s" % self.title)
-                self.obsolete()
-                mail.send(self.get_maintainers(), 'unstable', self)
-
-        comment = Comment(text=text, karma=karma, anonymous=anonymous)
-
-        if anonymous:
-            author = u'anonymous'
-        try:
-            user = User.query.filter_by(name=author).one()
-        except NoResultFound:
-            user = User(name=author)
-            DBSession.add(user)
-        user.comments.append(comment)
+        if user:
+            user.comments.append(comment)
+
+        if bugs:
+            for bug in bugs:
+                comment.bugs.append(bug)
 
         DBSession.add(comment)
         self.comments.append(comment)
-        DBSession.flush()
+        DBSession.commit()
 
         # Send a notification to everyone that has commented on this update
         people = set()
         for person in self.get_maintainers():
             people.add(person)
         for comment in self.comments:
-            if comment.anonymous or comment.user.name == u'bodhi':
-                continue
-            people.add(comment.user.name)
+            if comment.user:
+                if comment.user.name == u'bodhi':
+                    continue
+                people.add(comment.user.name)
+
         mail.send(people, 'comment', self)
 
     def unpush(self):
@@ -790,7 +788,6 @@ class Update(Base):
         self.pushed = False
         self.status = UpdateStatus.unpushed
         mail.send_admin('unpushed', self)
-        #DBSession.flush()
 
     def untag(self):
         """ Untag all of the builds in this update """
@@ -808,15 +805,26 @@ class Update(Base):
         action, changes in the repository will not propagate until the next
         mash takes place.
         """
+        from sqlalchemy.orm.exc import NoResultFound
+
         log.debug("Obsoleting %s" % self.title)
         self.untag()
         self.status = UpdateStatus.obsolete
         self.request = None
+
+        try:
+            user = User.query.filter(User.name==u'bodhi').one()
+        except NoResultFound:
+            user = User(name=u'bodhi')
+            session = DBSession()
+            session.add(user)
+            session.commit()
+
         if newer:
             self.comment(u"This update has been obsoleted by %s" % newer,
-                         author=u'bodhi')
+                         user=user)
         else:
-            self.comment(u"This update has been obsoleted", author=u'bodhi')
+            self.comment(u"This update has been obsoleted", user=user)
         DBSession.flush()
 
     def get_maintainers(self):
@@ -831,35 +839,28 @@ class Update(Base):
                     people.add(committer)
         return list(people)
 
-    def get_comments(self):
-        sorted = []
-        sorted.extend(self.comments)
-        sorted.sort(lambda x, y: cmp(x.timestamp, y.timestamp))
-        return sorted
-
 
 class Comment(Base):
     __tablename__ = 'comments'
 
     id = Column(Integer, primary_key=True)
-    karma = Column(Integer, default=0)
+    karma = Column(KarmaType.db_type(), default=KarmaType.works,
+            nullable=False)
+    
     text = Column(UnicodeText)
-    anonymous = Column(Boolean, default=False)
     timestamp = Column(DateTime, default=datetime.utcnow)
 
+    #One To One
     update_id = Column(Integer, ForeignKey('updates.id'))
     user_id = Column(Integer, ForeignKey('users.id'))
 
+    #Many To Many
+    bugs = relation('Bug', secondary=comment_bug_table,
+                    backref='comments', lazy=False)
+
     def __str__(self):
-        karma = '0'
-        if self.karma != 0:
-            karma = '%+d' % (self.karma,)
-        if self.anonymous:
-            anonymous = " (unauthenticated)"
-        else:
-            anonymous = ""
-        return "%s%s - %s (karma: %s)\n%s" % (self.author, anonymous,
-                                              self.timestamp, karma, self.text)
+        return "%s - %s (karma: %s)\n%s" % (self.user.name if self.user else None,
+                                        self.timestamp, self.karma.name, self.text)
 
 
 class CVE(Base):
@@ -992,6 +993,7 @@ class User(Base):
     __tablename__ = 'users'
 
     id = Column(Integer, primary_key=True)
+
     name = Column(Unicode(64), unique=True, nullable=False)
 
     # One-to-many relationships
@@ -1008,7 +1010,6 @@ class User(Base):
 #    # packages =  Many to many?
 #    # updates
 
-
 def populate():
     session = DBSession()
     user = User(name=u'bodhi')
@@ -1017,17 +1018,22 @@ def populate():
     session.add(release)
     pkg = Package(name=u'bodhi')
     session.add(pkg)
-    build = Build(nvr=u'bodhi-2.0-1.fc15', release=release)
+    build = Build(nvr=u'bodhi-2.0-1.fc15', package=pkg, release=release)
     session.add(build)
-    update = Update(builds=[build], user=user, notes=u'Useful details!', release=release)
-    update.type = UpdateType.bugfix
     bug = Bug(bug_id=12345)
+    bug2 = Bug(bug_id=12346)
     session.add(bug)
+    session.add(bug2)
+    update = Update(builds=[build], user=user, notes=u'Useful details!', release=release)
+    update.type = UpdateType.bugfix
     update.bugs.append(bug)
+    update.bugs.append(bug2)
     session.add(update)
     session.commit()
-    #session.flush()
-    #transaction.commit()
+    update.comment('Always look on the bright side of life', KarmaType.passes, None, user)
+    update.comment('It turned me into a newt', KarmaType.fails, [bug], user)
+    update.comment('I got better', KarmaType.works, [bug, bug2], user)
+    update.comment('Run Away!', KarmaType.passes)
 
 def initialize_sql(engine):
     DBSession.configure(bind=engine)
diff --git a/bodhi/resources.py b/bodhi/resources.py
index 6d15cad..6bac375 100644
--- a/bodhi/resources.py
+++ b/bodhi/resources.py
@@ -3,7 +3,7 @@ import logging
 from sqlalchemy.orm.exc import NoResultFound
 
 from bodhi.models import initialize_sql, DBSession
-from bodhi.models import Update, Package, Build, Release, User, Bug
+from bodhi.models import Update, Comment, Package, Build, Release, User, Bug
 
 log = logging.getLogger(__name__)
 
@@ -66,6 +66,10 @@ class BugResource(BodhiResource):
     __column__ = u'bug_id'
     __filter__ = int
 
+class CommentResource(BodhiResource):
+    __model__ = Comment
+    __column__ = u'comment_id'
+    __filter__ = int
 
 root = BodhiRoot()
 
@@ -76,6 +80,7 @@ def default_get_root(request):
         u'builds':  BuildResource,
         u'packages': PackageResource,
         u'releases': ReleaseResource,
+        u'comments': CommentResource,
         u'users': UserResource,
         u'bugs': BugResource,
         })
diff --git a/bodhi/templates/model.mak b/bodhi/templates/model.mak
index 6854880..25874e1 100644
--- a/bodhi/templates/model.mak
+++ b/bodhi/templates/model.mak
@@ -1,16 +1,16 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-    <title></title>
+	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+	<title></title>
 </head>
-  <body>
-      <table class="stylized">
-          ${page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)')}
-          <caption>${caption}</caption>
-          <col class="c1" />
-          ${grid}
-      </table>
-  </body>
+<body>
+	<table class="stylized">
+	${page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)')}
+	<caption>${caption}</caption>
+	<col class="c1" />
+	${grid}
+	</table>
+</body>
 </html>
diff --git a/bodhi/tests/models/__init__.py b/bodhi/tests/models/__init__.py
index fe75486..76e8cbb 100644
--- a/bodhi/tests/models/__init__.py
+++ b/bodhi/tests/models/__init__.py
@@ -12,7 +12,7 @@ def setup():
     """Function called by nose on module load"""
     # Doesn't appear to be necessary as long as our functional test suite
     # runs first (which sets up the DBSession)
-    #setup_db()
+    setup_db()
 
 def teardown():
     """Function called by nose after all tests in this module ran"""
diff --git a/bodhi/tests/models/test_models.py b/bodhi/tests/models/test_models.py
index ddd5a01..d88ac95 100644
--- a/bodhi/tests/models/test_models.py
+++ b/bodhi/tests/models/test_models.py
@@ -8,7 +8,7 @@ from datetime import datetime
 from sqlalchemy.exc import IntegrityError
 
 from bodhi import models as model
-from bodhi.models import UpdateStatus, UpdateType, UpdateRequest
+from bodhi.models import UpdateStatus, UpdateType, UpdateRequest, KarmaType
 from bodhi.tests.models import ModelTest
 from bodhi.exceptions import InvalidRequest
 
@@ -50,8 +50,6 @@ class TestPackage(ModelTest):
     attrs = dict(
         name = u"TurboGears",
         committers = ['lmacken'],
-        stable_karma = 3,
-        unstable_karma = -3,
         )
 
 
@@ -108,7 +106,6 @@ class TestUpdate(ModelTest):
         request=UpdateRequest.testing,
         close_bugs=True,
         notes=u'foobar',
-        karma=0,
         )
 
     def do_get_dependencies(self):
@@ -222,31 +219,23 @@ class TestUpdate(ModelTest):
     def test_stable_karma(self):
         update = self.obj
         update.request = None
-        eq_(update.karma, 0)
         eq_(update.request, None)
-        update.comment(u"foo", 1, u'foo')
-        eq_(update.karma, 1)
+        update.comment(u"foo", KarmaType.passes)
         eq_(update.request, None)
-        update.comment(u"foo", 1, u'bar')
-        eq_(update.karma, 2)
+        update.comment(u"foo", KarmaType.passes)
         eq_(update.request, None)
-        update.comment(u"foo", 1, u'biz')
-        eq_(update.karma, 3)
+        update.comment(u"foo", KarmaType.passes)
         eq_(update.request, UpdateRequest.stable)
 
     def test_unstable_karma(self):
         update = self.obj
         update.status = UpdateStatus.testing
-        eq_(update.karma, 0)
         eq_(update.status, UpdateStatus.testing)
-        update.comment(u"foo", -1, u'foo')
+        update.comment(u"foo", KarmaType.new_errors)
         eq_(update.status, UpdateStatus.testing)
-        eq_(update.karma, -1)
-        update.comment(u"bar", -1, u'bar')
+        update.comment(u"bar", KarmaType.new_errors)
         eq_(update.status, UpdateStatus.testing)
-        eq_(update.karma, -2)
-        update.comment(u"biz", -1, u'biz')
-        eq_(update.karma, -3)
+        update.comment(u"biz", KarmaType.new_errors)
         eq_(update.status, UpdateStatus.obsolete)
 
     def test_update_bugs(self):
diff --git a/bodhi/util.py b/bodhi/util.py
index ba18d05..31adb75 100644
--- a/bodhi/util.py
+++ b/bodhi/util.py
@@ -25,7 +25,7 @@ import subprocess
 import urlgrabber
 #import turbogears
 
-from kid import Element
+#from kid import Element
 from yum import repoMDObject
 from yum.misc import checksum
 from os.path import isdir, join, dirname, basename, isfile
-- 
1.7.5.4

