Commits

Tarek Ziadé committed b042d76

initial commit

  • Participants

Comments (0)

Files changed (34)

+bin
+lib
+include
+bugbro.egg-info
+.*\.pyc
+.*\.swp
+0.0
+---
+
+-  Initial version
+include *.txt *.ini *.cfg *.rst
+recursive-include bugbro *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
+bugbro README
+
+
+
+# -*- coding: utf8 -*-
+import os
+import sys
+import site
+
+# detecting if virtualenv was used in this dir
+_CURDIR = os.path.dirname(os.path.abspath(__file__))
+
+_PY_VER = sys.version.split()[0][:3]
+
+# XXX Posix scheme - need to add others
+_SITE_PKG = os.path.join(_CURDIR, 'lib', 'python' + _PY_VER, 'site-packages')
+
+# adding virtualenv's site-package and ordering paths
+saved = sys.path[:]
+
+if os.path.exists(_SITE_PKG):
+    site.addsitedir(_SITE_PKG)
+
+for path in sys.path:
+    if path not in saved:
+        saved.insert(0, path)
+
+sys.path[:] = saved
+
+# setting up the egg cache to a place where apache can write
+os.environ['PYTHON_EGG_CACHE'] = '/var/local/python-eggs'
+
+# running the app
+from paste.deploy import loadapp
+application = loadapp('config:%s'% os.path.join(_CURDIR, 'development.ini'))
+

File bugbro/__init__.py

+from sqlalchemy import engine_from_config
+
+from pyramid.config import Configurator
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+from bugbro.models import initialize_sql
+from bugbro.security import groupfinder
+
+
+def main(global_config, **settings):
+    """ This function returns a Pyramid WSGI application.
+    """
+    engine = engine_from_config(settings, 'sqlalchemy.')
+    initialize_sql(engine)
+    authn_policy = AuthTktAuthenticationPolicy(
+        'sosecret', callback=groupfinder)
+    authz_policy = ACLAuthorizationPolicy()
+    config = Configurator(settings=settings,
+                          root_factory='bugbro.models.RootFactory',
+                          authentication_policy=authn_policy,
+                          authorization_policy=authz_policy)
+
+    config.add_static_view('static', 'bugbro:static')
+    config.add_route('index', '/',
+                     view='bugbro.views.index',
+                     view_renderer='index.mako')
+
+    config.add_route('reviews', '/reviews',
+                     view='bugbro.views.review.index',
+                     view_renderer='reviews.mako')
+
+    config.add_route('login', '/login',
+                      view='bugbro.views.auth.login',
+                      view_renderer='bugbro:templates/login.mako')
+
+    config.add_route('logout', '/logout',
+                      view='bugbro.views.auth.logout')
+
+    config.add_route('register', '/register',
+                      view='bugbro.views.auth.register')
+
+    config.add_route('account', '/account',
+                     view='bugbro.views.account.index',
+                     view_renderer='bugbro:templates/account.mako',
+                     view_permission='edit')
+
+    config.add_route('add_review', '/review/add',
+                     view='bugbro.views.review.add_review',
+                     view_renderer='bugbro:templates/add_review.mako',
+                     view_permission='edit')
+
+    config.add_route('add_gem', '/gem/add',
+                     view='bugbro.views.gem.add_gem',
+                     view_renderer='bugbro:templates/add_gem.mako',
+                     view_permission='edit')
+
+    config.add_route('gems', '/gems',
+                     view='bugbro.views.gem.index',
+                     view_renderer='gems.mako')
+
+    config.add_route('url', '/url/{url_id}',
+                     view='bugbro.views.url',
+                     view_permission='view')
+
+    config.add_route('take_review', '/review/{review_id}/take',
+                     view='bugbro.views.review.take_review',
+                     view_permission='edit')
+
+    config.add_route('decline_review', '/review/{review_id}/decline',
+                     view='bugbro.views.review.decline_review',
+                     view_permission='edit')
+
+    config.add_route('accept_review', '/review/{review_id}/accept',
+                     view='bugbro.views.review.accept_review',
+                     view_permission='edit')
+
+    config.add_route('thanks', '/thanks',
+                     view='bugbro.views.thanks',
+                     view_renderer='bugbro:templates/thanks.mako')
+
+    config.add_route('review', '/review/{review_id}',
+                     view='bugbro.views.review.review',
+                     view_renderer='bugbro:templates/review.mako',
+                     view_permission='view')
+
+    config.add_route('pending', '/pending',
+                     view='bugbro.views.account.pending',
+                     view_renderer='bugbro:templates/pending.mako',
+                     view_permission='edit')
+
+    config.add_view("bugbro.views.auth.forbidden",
+                    context="pyramid.exceptions.Forbidden")
+
+    config.include('pyramid_formalchemy')
+    config.include('fa.jquery')
+    config.formalchemy_admin('admin', package='bugbro',
+                             view='fa.jquery.pyramid.ModelView',
+                             factory='bugbro.models.Models')
+
+    return config.make_wsgi_app()

File bugbro/emails.py

+import smtplib
+import socket
+from email.mime.text import MIMEText
+from email.header import Header
+
+
+_EMAILS = {'register': """\
+Welcome to Bugbro,
+
+please click on this link to complete your registration process:
+
+  http://%(host)s/register?user=%(user)s&key=%(key)s
+
+Cheers,
+
+--
+The Bug Brothers.
+"""}
+
+
+_EMAILS['review'] = """\
+Dear reviewer,
+
+We have a review that matches your skills !
+
+Come grab it at http://%(body)s/review/%(id)s.
+
+Cheers,
+
+--
+The Bug Brothers.
+"""
+
+_EMAILS['review_accepted'] = """\
+Dear reviewer,
+
+Your review was accepted. You have earned 2 credits.
+
+Come back and spent some credits.
+
+Review visible at: http://%(host)s/review/%(id)s.
+
+Cheers,
+
+--
+The Bug Brothers.
+"""
+
+
+_EMAILS['review_done'] = """\
+Dear reviewee,
+
+We have a review ready for you, check it out
+
+Come grab it at http://%(host)s/review/%(id)s.
+
+Cheers,
+
+--
+The Bug Brothers.
+"""
+
+
+def _get_body(name, **data):
+    return _EMAILS[name] % data
+
+
+_SENDER = 'tarek@ziade.org'
+
+
+def send_email(rcpt, subject, body_tmpl, body_data=None,
+               smtp_host='localhost',
+               smtp_port=25, smtp_user=None, smtp_password=None):
+    """Sends a text/plain email synchronously.
+    """
+    # preparing the message
+    if body_data is None:
+        body_data = {}
+    body = _get_body(body_tmpl, **body_data)
+    msg = MIMEText(body.encode('utf8'), 'plain', 'utf8')
+    msg['From'] = Header(_SENDER, 'utf8')
+    msg['To'] = Header(rcpt, 'utf8')
+    msg['Subject'] = Header(subject, 'utf8')
+
+    try:
+        server = smtplib.SMTP(smtp_host, smtp_port, timeout=5)
+    except (smtplib.SMTPConnectError, socket.error), e:
+        return False, str(e)
+
+    # auth
+    if smtp_user is not None and smtp_password is not None:
+        try:
+            server.login(smtp_user, smtp_password)
+        except (smtplib.SMTPHeloError,
+                smtplib.SMTPAuthenticationError,
+                smtplib.SMTPException), e:
+            return False, str(e)
+
+    # the actual sending
+    try:
+        server.sendmail(_SENDER, [rcpt], msg.as_string())
+    finally:
+        server.quit()
+
+    return True, None

File bugbro/forms.py

Empty file added.

File bugbro/models.py

+import transaction
+import urllib2
+
+from sqlalchemy import (Column, Integer, String, Text, DateTime,
+                        Boolean, Table, ForeignKey)
+
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.declarative import declarative_base
+
+from sqlalchemy.orm import scoped_session, relationship
+from sqlalchemy.orm import sessionmaker, backref
+
+from zope.sqlalchemy import ZopeTransactionExtension
+from pyramid.security import Allow, Everyone
+from pyramid_formalchemy.resources import Models as FModels
+from bugbro.util import ssha256, validate_password
+
+
+_DEFAULT_ACL = [
+            (Allow, Everyone, 'view'),
+            (Allow, 'group:editors', 'add'),
+            (Allow, 'group:editors', 'edit'),
+            (Allow, 'group:editors', 'new'),
+            (Allow, 'group:admin', 'delete'),
+
+            ]
+
+
+class Models(FModels):
+    __acl__ = _DEFAULT_ACL
+
+
+class RootFactory(object):
+    __acl__ = _DEFAULT_ACL
+
+    def __init__(self, request):
+        pass
+
+
+DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
+Base = declarative_base()
+
+# stages
+OPEN = 0
+ASKED = 1
+TAKEN = 2
+REVIEWED = 3
+CLOSED = 4
+ARCHIVED = 5
+
+
+user_skill = Table('bugbro_user_skill', Base.metadata,
+                  Column('skill_id', Integer, ForeignKey('bugbro_skill.id')),
+                  Column('user_id', String(255), ForeignKey('bugbro_user.id')))
+
+
+class Url(Base):
+    __tablename__ = 'bugbro_url'
+    __acl__ = _DEFAULT_ACL
+    id = Column(Integer, primary_key=True)
+    title = Column(String(255))
+    url = Column(String(255))
+
+    def __init__(self, url):
+        self.url = url
+
+    def __unicode__(self):
+        return self.label
+
+
+class Skill(Base):
+    __tablename__ = 'bugbro_skill'
+    __acl__ = _DEFAULT_ACL
+    id = Column(Integer, primary_key=True)
+    label = Column(String(255))
+
+    def __init__(self, label=''):
+        self.label = label
+
+    def __unicode__(self):
+        return self.label
+
+
+class Team(Base):
+    __tablename__ = 'bugbro_team'
+    __acl__ = _DEFAULT_ACL
+    id = Column(Integer, primary_key=True)
+    name = Column(String(255))
+
+    def __init__(self, name=''):
+        self.name = name
+
+    def __unicode__(self):
+        return self.name
+
+user_team = Table('bugbro_user_team', Base.metadata,
+                  Column('team_id', Integer, ForeignKey('bugbro_team.id')),
+                  Column('user_id', String(255), ForeignKey('bugbro_user.id')))
+
+
+user_group = Table('bugbro_user_group', Base.metadata,
+                Column('group_id', String(255), ForeignKey('bugbro_group.id')),
+                Column('user_id', String(255), ForeignKey('bugbro_user.id')))
+
+
+class Group(Base):
+    __tablename__ = 'bugbro_group'
+    __acl__ = _DEFAULT_ACL
+    id = Column(String(255), primary_key=True)
+
+    def __init__(self, id=''):
+        self.id = id
+
+    def __unicode__(self):
+        return self.id
+
+
+class User(Base):
+    __tablename__ = 'bugbro_user'
+    __acl__ = _DEFAULT_ACL
+    id = Column(String(255), primary_key=True)
+    reviewer = Column(Boolean, default=False)
+    credits = Column(Integer, default=5)
+    password_hash = Column(String(255))
+    confirmation_key = Column(String(255))
+    activated = Column(Boolean, default=True)
+
+    groups = relationship("Group",
+                          secondary=user_group,
+                          backref="members")
+
+    skills = relationship("Skill",
+                          secondary=user_skill,
+                          backref="users")
+
+    teams = relationship("Team", backref="members",
+                         secondary=user_team)
+
+    def __init__(self, id=''):
+        self.id = id
+
+    def set_password(self, password):
+        self.password_hash = ssha256(password)
+
+    def check_password(self, password):
+        return validate_password(password, self.password_hash)
+
+    def __unicode__(self):
+        return self.id
+
+
+review_skill = Table('bugbro_review_skill', Base.metadata,
+                  Column('skill_id', Integer, ForeignKey('bugbro_skill.id')),
+                  Column('review_id', Integer, ForeignKey('bugbro_review.id')))
+
+
+class Review(Base):
+    __tablename__ = 'bugbro_review'
+    __acl__ = _DEFAULT_ACL
+    id = Column(Integer, primary_key=True)
+    reviewee_id = Column(String(255))
+    reviewer_id = Column(String(255), default='')
+    stage = Column(Integer, default=OPEN)
+    description = Column(Text)
+    title = Column(String(255))
+    patch_or_diff_url = Column(String(255))
+    url_is_raw_diff = Column(Boolean, default=False)
+    context_url = Column(String(255))
+    review = Column(Text)
+    creation_date = Column(DateTime)
+    review_date = Column(DateTime)
+    reviewer_in_different_team = Column(Boolean, default=True)
+    diff = Column(Text)
+    skills_required = relationship("Skill",
+                          secondary=review_skill,
+                          backref="reviews")
+
+    def __init__(self, patch_or_diff_url='', description='', reviewee_id=''):
+        self.patch_or_diff_url = patch_or_diff_url
+        self.description = description
+        self.reviewee_id = reviewee_id
+        self.stage = OPEN
+
+    def load_diff(self):
+        # XXX to protect
+        loc = urllib2.urlopen(self.patch_or_diff_url)
+        self.diff = loc.read()
+
+
+class Gem(Base):
+    __tablename__ = 'bugbro_gem'
+    __acl__ = _DEFAULT_ACL
+    id = Column(Integer, primary_key=True)
+    founder_id = Column(String(255), ForeignKey('bugbro_user.id'))
+    founder = relationship("User", backref=backref("gem", uselist=False))
+
+    title = Column(String(255))
+    url = Column(String(255))
+    description = Column(Text)
+    creation_date = Column(DateTime)
+
+    def __init__(self, url='', description='', founder_id=''):
+        self.url = url
+        self.description = description
+        self.founder_id = founder_id
+
+
+def populate():
+    session = DBSession()
+
+    skills = [skill.label for skill in session.query(Skill).all()]
+    for skill in ('python', 'javascript', 'css'):
+        if skill in skills:
+            continue
+        session.add(Skill(skill))
+    session.flush()
+
+    teams = [team.name for team in session.query(Team).all()]
+    for team in ('WebDev', 'Services', 'ReleaseEng'):
+        if team in teams:
+            continue
+        session.add(Team(team))
+    session.flush()
+
+    if session.query(User, User.id == 'admin').first() is None:
+        user = User('admin')
+        user.skills = [session.query(Skill,
+                                     Skill.label == 'python').first()[0]]
+        session.add(user)
+
+    session.flush()
+
+    groups = [group.id for group in session.query(Group).all()]
+
+    for group in ('editors', 'admin'):
+        if group in groups:
+            continue
+        group = Group(group)
+        session.add(group)
+
+    session.flush()
+
+    users = [user.id for user in session.query(User).all()]
+
+    if 'tarek@ziade.org' not in users:
+        admin = User('tarek@ziade.org')
+        admin.set_password('admin')
+        admin.activated = True
+        admin.groups = session.query(Group).all()
+        session.add(admin)
+
+    transaction.commit()
+
+
+def initialize_sql(engine):
+    DBSession.configure(bind=engine)
+    Base.metadata.bind = engine
+    Base.metadata.create_all(engine)
+    try:
+        populate()
+    except IntegrityError:
+        DBSession.rollback()

File bugbro/security.py

+from pyramid.security import authenticated_userid
+from bugbro.models import DBSession, User
+
+
+def get_user(request, session=None):
+    logged_in = authenticated_userid(request)
+    if session is None:
+        session = DBSession()
+    q = session.query(User).filter(User.id == logged_in)
+    user = q.first()
+    if user is None or not user.activated:
+        return None
+    return user
+
+
+def groupfinder(userid, request):
+    dbsession = DBSession()
+    q = dbsession.query(User).filter(User.id == userid)
+    user = q.first()
+    if user is None or not user.activated:
+        return None
+    return ['group:%s' % group.id for group in user.groups]

File bugbro/static/pygments.css

+
+div.highlight {
+ padding: 0.5em;
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+ border-bottom: 1px solid grey;
+ border-right: 1px solid grey;
+ margin-bottom: 0.5em;
+ border-radius: 10px;
+ background-color: whitesmoke;
+}
+
+.highlight span {
+ width: 100%;
+ display: inline-block;
+ margin: none;
+ background-color: whitesmoke;
+}
+
+.highlight pre { background-color: whitesmoke;}
+.highlight .c{color:#998;font-style:italic;}
+.highlight .err{color:#a61717;background-color:#e3d2d2;}
+.highlight .k{font-weight:bold;}
+.highlight .o{font-weight:bold;}
+.highlight .cm{color:#998;font-style:italic;}
+.highlight .cp{color:#999;font-weight:bold;}
+.highlight .c1{color:#998;font-style:italic;}
+.highlight .cs{color:#999;font-weight:bold;font-style:italic;}
+.highlight .gd{color:#000;background-color:#fdd;}
+.highlight .gd .x{color:#000;background-color:#faa;}
+.highlight .ge{font-style:italic;}
+.highlight .gr{color:#a00;}
+.highlight .gh{color:#999;}
+.highlight .gi{color:#000;background-color:#dfd;}
+.highlight .gi .x{color:#000;background-color:#afa;}
+.highlight .gc{color:#999;background-color:#EAF2F5;}
+.highlight .go{color:#888;}
+.highlight .gp{color:#555;}
+.highlight .gs{font-weight:bold;}
+.highlight .gu{color:#000;}
+.highlight .gt{color:#a00;}
+

File bugbro/static/style.css

+body {
+ background-color: #fff;
+ color: #333;
+ font-family: Baskerville, Times, "Times New Roman", serif;
+}
+
+#header {
+ height: 3em;
+ margin-left: auto;
+ margin-right: auto;
+ width: 1000px;
+ text-align: left;
+ padding-bottom: 0.5em;
+ /*background-color: grey;*/
+ border-bottom: 3px solid grey;
+}
+
+#title {
+ float: left;
+ font-size: 180%;
+ margin-top: 0.5em;
+}
+
+#title a {
+ color: #2966B8;
+ font-weight: bold;
+}
+
+#warning {
+ float: right;
+ color: red;
+ font-weight: bold;
+
+}
+
+#toolbar {
+ margin-top: 1.5em;
+ text-align: right;
+ float: right;
+}
+
+
+
+
+a {
+ text-decoration: none;
+ color: black;
+}
+
+a:visited {
+ text-decoration: none;
+ color: black;
+}
+
+a:hover {
+ text-decoration: dotted;
+ color: #2F74D0;
+}
+
+.submit:hover {
+ color: #2F74D0;
+ cursor: pointer;
+ cursor: hand;
+}
+
+.submit {
+ margin-top: 1em;
+ padding: 0.5em;
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+ border-bottom: 1px solid grey;
+ border-right: 1px solid grey;
+ background-color: white;
+}
+
+a.button {
+ /*background-color: blue;*/
+ padding: 0.5em;
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+ border-bottom: 1px solid grey;
+ border-right: 1px solid grey;
+}
+
+div.item {
+ min-height: 4em;
+ padding: 0.5em;
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+ border-bottom: 1px solid grey;
+ border-right: 1px solid grey;
+ margin-bottom: 0.5em;
+ border-radius: 10px;
+ background-color: #F2FFE1;
+}
+
+div.open {
+ background-color: #FF9797;
+}
+
+div.reviewed {
+ background-color: #FFA8A8;
+}
+
+
+div.item a {
+ font-size: 110%;
+ font-weight: bold;
+}
+
+div.item p {
+ font-size: 140%;
+ font-style: italic;
+}
+
+div.author {
+ float: right;
+ font-size: 80%;
+ font-style: italic;
+ color: #dddd;
+}
+
+a.take {
+ color: red;
+}
+
+
+#header a {
+ padding: 0.5em;
+}
+
+#content {
+ margin-left: auto;
+ margin-right: auto;
+ width: 1000px;
+ padding: 0.5em;
+ /*background-color: grey;*/
+ min-height:30em;
+}
+
+#content h1 {
+ color: #2F74D0;
+ font-size: 150%;
+}
+
+
+#content h2 {
+ color: #2F74D0;
+ font-size: 120%;
+}
+
+#content div.label {
+ color: #2F74D0;
+ font-size: 110%;
+ font-weight: bold;
+ font-style: italic;
+}
+
+#content div.field_input {
+ margin-bottom: 0.5em;
+}
+
+#content input[type=text], input[type=password] {
+ width: 100%;
+}
+
+#content textarea {
+ width: 100%;
+ min-height: 100px;
+}
+
+#content p.description {
+ font-style: italic;
+ padding: 0.2em;
+ color: grey;
+ font-size: 110%
+}
+
+
+.fa_instructions { 
+ font-style: italic;
+ padding: 0.2em;
+ color: grey;
+} 
+
+#footer {
+ height: 3em;
+ margin-left: auto;
+ margin-right: auto;
+ width: 1000px;
+ margin-top: 2em;
+ padding-top: 1em;
+ text-align: center;
+ padding: 0.5em;
+ border-top: 3px solid grey;
+
+ /*background-color: grey;*/
+}
+
+div.list {
+ margin-bottom: 2em;
+}
+
+a.description {
+ font-size: 150%;
+}
+
+div.actions {
+ margin-top: 2em;
+}

File bugbro/templates/account.mako

+<%inherit file="base.mako"/>
+
+<h1>Profile information</h1>
+<form action="account" method="POST">
+    ${user}
+    <input type="hidden" name="form.submitted"></input>
+    <input class="submit" type="submit"></input>
+</form>
+
+

File bugbro/templates/add_gem.mako

+<%inherit file="base.mako"/>
+<h1>Add a Gem</h1>
+<p class="description">You found a cool code snippet ? share it and earn a credit !<p>
+
+<form action="/gem/add" method="POST">
+    ${form}
+    <input type="hidden" name="form.submitted"></input>
+    <input class="submit" type="submit"></input>
+</form>
+

File bugbro/templates/add_review.mako

+<%inherit file="base.mako"/>
+
+<h1>Add a new review request</h1>
+
+%if enough_credits:
+<p class="description">Fill in this form, and we'll look for a reviewer.</p>
+<form action="/review/add" method="POST">
+  ${form}
+  <input type="hidden" name="form.submitted"></input>
+  <input class="submit" type="submit"></input>
+</form>
+%endif
+
+%if not enough_credits:
+<p class="description">No credits !</p>
+
+<p>
+I am sorry dude, you're out of credits. You need one credit
+to get a review.
+
+If you want people to review your code, earn some credits by
+reviewing people's code first.
+
+One review = 2 credits.
+</p>
+%endif
+

File bugbro/templates/base.mako

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+ <head>
+  <title>The Bug Brothers</title>
+  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
+  <link rel="stylesheet" href="/static/style.css" type="text/css" />
+  <link rel="stylesheet" href="/static/pygments.css" type="text/css" />
+ </head>
+ <body>
+  <div id="header">
+      <div id="title"><a href="/">The Bug Brothers</a></div>
+   <div id="toolbar">
+    <a href="${request.application_url}/reviews">Reviews</a>
+    <a href="${request.application_url}/gems">Gems</a>
+    %if logged_in:
+    <a href="${request.application_url}/pending">Pending tasks</a>
+    <a href="${request.application_url}/account">Account</a>
+    <a href="${request.application_url}/logout">Logout</a>
+    %if is_admin:
+    <a href="${request.application_url}/admin/">Admin</a>
+    %endif
+    %endif
+    %if not logged_in:
+    <a href="${request.application_url}/login">Login</a>
+    %endif
+   </div>
+  </div>
+
+  <div id="content">
+   %if warning:
+   <div id="warning">
+     ${warning}
+   </div>
+   %endif
+    ${self.body()}
+  </div>
+  <div id="footer">
+    <div class="footer">&copy; Copyright 2008-2011, Bug Brothers Consulting.</div>
+  </div>
+</body>
+</html>

File bugbro/templates/gems.mako

+<%inherit file="base.mako"/>
+
+<h1>Last Hidden Gems</h1>
+
+<div class="list">
+%for gem in gems:
+<div class="item">
+ <a href="${gem.url}">${gem.title}</a>
+ <p>${gem.description}</p>
+ <div class="author">
+   found by ${gem.founder_id} on ${gem.creation_date}
+ </div>
+ <div style="clear: both"></div>
+</div>
+%endfor
+</div>
+
+<a class="button" href="/gem/add">Add a Gem</a>

File bugbro/templates/index.mako

+<%inherit file="base.mako"/>
+
+<h1>Welcome to the Bug Brothers</h1>
+<p class="description">Ask for a review of your code, or review some code yourself. Also, share the gems you've found along the way<p>
+
+<div>
+ <a class="button" href="/review/add">Request a review</a>
+ <a class="button" href="/gem/add">Add a gem</a>
+ %if not reviewer:
+ <a class="button" href="/account?warning=Check the reviewer checkbox">Become a reviewer</a>
+ %endif
+ %if reviewer:
+ <a class="button" href="/account">Options</a>
+ %endif
+</div>
+

File bugbro/templates/login.mako

+<%inherit file="base.mako"/>
+
+<h1>Please log in</h1>
+<p class="description">
+To register it's quite simple: check the box and enter a login and a password
+</p>
+
+<form action="${url}" method="post">
+ <input type="hidden" name="came_from" value="${came_from}"/>
+
+ <div class="label">
+  <label for="login">Email</label>
+ </div>
+ <div>
+  <input type="text" name="login" value="${login}"/><br/>
+ </div>
+
+ <div class="label">
+  <label for="password">Password</label>
+ </div>
+ <div>
+  <input type="password" name="password"
+        value="${password}"/>
+ </div>
+
+
+     <input id="registering" name="registering" value="False" type="checkbox"/>
+ <div class="label">
+     <label class="field_opt" for="registering">Registering</label>
+  <div class="fa_instructions ui-corner-all">Check this box if you are registering</div>
+ </div>
+
+ <input class="submit" type="submit" name="form.submitted" value="Log In"/>
+</form>

File bugbro/templates/pending.mako

+<%inherit file="base.mako"/>
+
+<h1>Your pending tasks</h1>
+<p class="description">Find here your pending reviews and requests.</p>
+
+<h2>Pending reviews</h2>
+<p class="description">The red reviews are the ones you did not accept yet. The
+orange ones are waiting
+for the reviewee to validate you review, but you can still change them !
+</p>
+
+
+<div class="list">
+%if not reviews:
+No reviews yet.
+
+<a href="/reviews" class="button">Pick some !</a>
+%endif
+
+%for review in reviews:
+%if review.stage == 1:
+<div class="item open">
+%endif
+%if review.stage == 3:
+<div class="item reviewed">
+%endif
+%if review.stage not in (1, 3):
+<div class="item">
+%endif
+ <a class="description" href="/review/${review.id}">${review.title}</a>
+ <p>${review.description}</p>
+ <div class="author">
+  ${review.creation_date} asked by ${review.reviewee_id}.
+ </div>
+ <div style="clear: both"></div>
+</div>
+%endfor
+</div>
+
+<h2>Pending requests</h2>
+<p class="description">The red requests are the review done. Have a look
+!</p>
+
+<div class="list">
+%if not requests:
+No request yet.
+
+<a href="/review/add" class="button">Request for a review !</a>
+%endif
+
+
+%for review in requests:
+
+%if review.stage == 1:
+<div class="item open">
+%endif
+%if review.stage == 3:
+<div class="item reviewed">
+%endif
+%if review.stage not in (1, 3):
+<div class="item">
+%endif
+<a class="description" href="/review/${review.id}">${review.title}</a>
+ <p>${review.description}</p>
+ <div class="author">
+  ${review.creation_date} asked by ${review.reviewee_id}.
+ </div>
+ <div style="clear: both"></div>
+</div>
+%endfor
+</div>
+

File bugbro/templates/review.mako

+<%inherit file="base.mako"/>
+<h1>${review.title}</h1>
+
+%if review.stage == 1:
+<p class="description">Someone has been asked to do this review, but has not answered yet.
+
+If she declines, anyone will be able to take the review.
+</p>
+%endif
+
+%if review.stage == 0:
+<p class="description">
+This review is open !
+</p>
+%endif
+
+%if review.stage == 2:
+<p class="description">
+This review is taken, and the review is pending.
+</p>
+%endif
+
+%if review.stage == 3:
+<p class="description">
+This review was done. The reviewee needs to accept or reject the review.
+</p>
+%endif
+
+%if review.stage == 4:
+<p class="description">
+This review is now closed !
+</p>
+%endif
+
+%if review.stage == 5:
+<p class="description">
+This review is now archived. How did you get here ? ;)
+</p>
+%endif
+
+<form action="/review/${review.id}" method="POST">
+ ${rendered_review}
+ %if is_reviewer and review.stage in (2, 3):
+ <input type="submit" class="submit"/>
+ <input type="hidden" name="form.submitted"/>
+ %endif
+ <div class="actions">
+ %if not is_reviewer and not is_reviewee and review.stage == 0:
+ <a class="button" href="/review/${review.id}/take">Take it !</a>
+ %endif
+ %if is_reviewer and review.stage == 1:
+ <a class="button" href="/review/${review.id}/take">Accept the job !</a>
+ <a class="button" href="/review/${review.id}/decline">No, thanks.</a>
+ %endif
+ %if is_reviewee and review.stage == 3:
+ <a class="button" href="/review/${review.id}/accept">I accept the review, give
+the reviewer some credits !</a>
+ %endif
+
+ %if diff:
+ <h2>Patch</h2>
+ ${diff}
+ %endif
+
+ </div>
+</form>

File bugbro/templates/reviews.mako

+<%inherit file="base.mako"/>
+
+<h1>Last Reviews</h1>
+<p class="description">Find all reviews here. You can pick any orphaned review unless you're the reviewee
+-- The red ones.</p>
+
+<div class="list">
+%for review in reviews:
+%if review.stage == 0:
+<div class="item open">
+%endif
+%if review.stage != 0:
+<div class="item">
+%endif
+ <div class="author">
+  Required skills:
+ %for skill in review.skills_required:
+ ${skill}
+ %endfor
+ </div>
+
+ <a class="description" href="/review/${review.id}">${review.title}</a>
+ %if review.description: 
+ <p>${review.description}</p>
+ %endif
+ 
+ %if review.stage == 0 and logged_in != review.reviewee_id:
+ <a class="take" href="/review/${review.id}/take">Take it !</a>
+ %endif
+ %if review.reviewer_id:
+   %if review.stage == 2:
+     Currently Reviewed
+   %endif
+   %if review.stage > 2:
+     was Reviewed
+   %endif
+
+   by ${review.reviewer_id}
+ %endif
+
+ <div class="author">
+  ${review.creation_date} asked by ${review.reviewee_id}.
+ </div>
+ <div style="clear: both"></div>
+</div>
+%endfor
+</div>
+
+

File bugbro/templates/thanks.mako

+<%inherit file="base.mako"/>
+<h1>Thanks !</h1>
+
+Congrats, you have ${credits} credits now !

File bugbro/tests.py

+import unittest
+from pyramid.config import Configurator
+from pyramid import testing
+
+def _initTestingDB():
+    from sqlalchemy import create_engine
+    from bugbro.models import initialize_sql
+    session = initialize_sql(create_engine('sqlite://'))
+    return session
+
+class TestMyView(unittest.TestCase):
+    def setUp(self):
+        self.config = testing.setUp()
+        _initTestingDB()
+
+    def tearDown(self):
+        testing.tearDown()
+
+    def test_it(self):
+        from bugbro.views import my_view
+        request = testing.DummyRequest()
+        info = my_view(request)
+        self.assertEqual(info['root'].name, 'root')
+        self.assertEqual(info['project'], 'bugbro')

File bugbro/util.py

+from hashlib import sha256
+import base64
+import os
+import string
+
+from pyramid.security import authenticated_userid
+
+
+def randkey(login):
+    key = login + ''.join([randchar() for char in range(100)])
+    return base64.b64encode(key)
+
+
+def randchar(chars=string.digits + string.letters):
+    pos = int(float(ord(os.urandom(1))) * 256. / 255.)
+    return chars[pos % len(chars)]
+
+
+def is_admin(request):
+    userid = authenticated_userid(request)
+    if userid is None:
+        return False
+
+    from bugbro.security import groupfinder
+    groups = groupfinder(userid, request)
+    return 'group:admin' in groups
+
+
+def is_anonymous(request):
+    return authenticated_userid(request) is None
+
+
+_SALT_LEN = 8
+
+
+def _gensalt():
+    """Generates a salt"""
+    return ''.join([randchar() for i in range(_SALT_LEN)])
+
+
+def ssha256(password, salt=None):
+    """Returns a Salted-SHA256 password"""
+    if salt is None:
+        salt = _gensalt()
+    ssha = base64.b64encode(sha256(password + salt).digest()
+                               + salt).strip()
+    return "{SSHA-256}%s" % ssha
+
+
+def validate_password(clear, hash):
+    real_hash = hash.split('{SSHA-256}')[-1]
+    hash_meth = ssha256
+    salt = base64.decodestring(real_hash)[-_SALT_LEN:]
+    password = hash_meth(clear, salt)
+    return password == hash
+
+
+def cgi_escape(s):
+    if '&' in s:
+        s = s.replace("&", "&amp;")
+    if '<' in s:
+        s = s.replace("<", "&lt;")
+    if '>' in s:
+        s = s.replace(">", "&gt;")
+    s = s.replace('"', "&quot;")
+    s = s.replace("'", "&apos;")
+    return s

File bugbro/views/__init__.py

+from pyramid.security import authenticated_userid
+from pyramid.httpexceptions import HTTPFound
+from webob.exc import HTTPNotFound
+
+from bugbro.models import User, DBSession, Url
+from bugbro.util import cgi_escape, is_admin, is_anonymous
+from bugbro.security import get_user
+
+
+def add_credit(request, credit=1, user_id=None):
+    session = DBSession()
+    if user_id is None:
+        user_id = authenticated_userid(request)
+    q = session.query(User).filter(User.id == user_id)
+    user = q.first()
+    if user is None:
+        return
+    user.credits += credit
+
+
+def default_context(request):
+    context = {'logged_in': authenticated_userid(request),
+               'is_admin': is_admin(request),
+               'is_anonymous': is_anonymous(request)}
+    if 'warning' in request.params:
+        warning = cgi_escape(request.params['warning'])
+        context['warning'] = warning
+    return context
+
+
+def url(request):
+    dbsession = DBSession()
+    url_id = int(request['bfg.routes.matchdict']['url_id'])
+    url = dbsession.query(Url).filter(Url.id == url_id).first()
+    if url is None:
+        raise HTTPNotFound()
+    return HTTPFound(location=url.url)
+
+
+def thanks(request):
+    user = get_user(request)
+    context = default_context(request)
+    context['credits'] = user.credits
+    return context
+
+
+def index(request):
+    logged_in = authenticated_userid(request)
+    reviewer = False
+
+    if logged_in:
+        user = get_user(request)
+        if user is not None:
+            reviewer = user.reviewer
+
+    context = default_context(request)
+    context['reviewer'] = reviewer
+    return context

File bugbro/views/account.py

+from pyramid.url import route_url
+from pyramid.security import authenticated_userid
+from pyramid.httpexceptions import HTTPFound
+
+from formalchemy import FieldSet
+from sqlalchemy.sql import and_
+
+from bugbro.security import  get_user
+from bugbro.models import Review, ASKED, TAKEN, REVIEWED
+from bugbro.models import DBSession
+from bugbro.views import default_context
+
+
+def pending(request):
+    logged_in = authenticated_userid(request)
+    dbsession = DBSession()
+    context = default_context(request)
+
+    reviews = dbsession.query(Review).filter(and_(
+        Review.reviewer_id == logged_in,
+        Review.stage.in_((ASKED, TAKEN, REVIEWED))))
+
+    context['reviews'] = reviews.all()
+
+    requests = dbsession.query(Review).filter(Review.reviewee_id ==
+            logged_in)
+    requests = requests.filter(Review.stage.in_((TAKEN, REVIEWED)))
+    context['requests'] = requests.all()
+
+    return context
+
+
+def index(request):
+    dbsession = DBSession()
+    user = get_user(request, dbsession)
+
+    fs = FieldSet(user, session=dbsession)
+    fs.configure(include=[fs.reviewer, fs.skills,
+                          fs.teams, fs.credits.readonly()])
+
+    if 'form.submitted' in request.params:
+        fs.data = request.params
+        if fs.validate():
+            fs.sync()
+
+        return HTTPFound(
+                location=route_url('account', request) +
+                 '?warning=Preferences changed.')
+
+    context = default_context(request)
+    context['user'] = fs.render()
+    return context

File bugbro/views/auth.py

+from pyramid.url import route_url
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import remember, forget
+
+from bugbro.models import DBSession, User, Group
+from bugbro.emails import send_email
+from bugbro.util import randkey
+
+
+def _send_confirmation(host, user):
+    body_data = {'host': host,
+                 'user': user.id,
+                 'key': user.confirmation_key}
+
+    send_email(user.id, 'Bugbro registration',
+               'register', body_data)
+
+
+def login(request):
+    login_url = route_url('login', request)
+    referrer = request.url
+    if referrer == login_url:
+        referrer = '/'
+    came_from = request.params.get('came_from', referrer)
+    warning = ''
+    login = ''
+    password = ''
+    url = request.application_url + '/login'
+
+    if 'form.submitted' in request.params:
+        login = request.params['login']
+        password = request.params['password']
+        dbsession = DBSession()
+
+        # are we registering ?
+        if 'registering' in request.params:
+            # is this e-mail not taken already ?
+            user = dbsession.query(User).filter(User.id == login).first()
+            if user is not None:
+                if not user.activated:
+                    # sending again a new code ? XXX
+                    user.confirmation_key = randkey(user.id)
+
+                    # and send him a mail to finish his registration
+                    _send_confirmation(request.host, user)
+
+                    warning = "We sent a new registration e-mail"
+                    return HTTPFound(location='/' + '?warning=' + warning)
+
+                else:
+                    warning = 'E-mail already registered'
+
+            else:
+                # ok good, let's add that guy
+                user = User(login)
+                user.set_password(password)
+                user.activated = False
+                user.confirmation_key = randkey(login)
+                editors = dbsession.query(Group).filter(Group.id == 'editors')
+                editors = editors.one()
+                user.groups = [editors]
+                dbsession.add(user)
+
+                # and send him a mail to finish his registration
+                _send_confirmation(request.host, user)
+
+                # and tell the user
+                warning = 'Check your mails for a registration e-mail.'
+                return HTTPFound(location='/' + '?warning=' + warning)
+
+        else:
+            # check the password
+            user = dbsession.query(User).filter(User.id == login).first()
+
+            if user is not None and user.check_password(password):
+                headers = remember(request, login)
+                return HTTPFound(location=came_from, headers=headers)
+
+            warning = 'Failed login'
+
+    return dict(warning=warning,
+                url=url,
+                came_from=came_from,
+                login=login,
+                password=password)
+
+
+def logout(request):
+    headers = forget(request)
+    return HTTPFound(location=route_url('index', request),
+                     headers=headers)
+
+
+def forbidden(request):
+    param = '?came_from=%s' % request.path.lstrip('/')
+    return HTTPFound(location=route_url('login', request) + param)
+
+
+def register(request):
+    key = request.GET.get('key', '')
+    user = request.GET.get('user', '')
+    dbsession = DBSession()
+
+    user = dbsession.query(User).filter(User.id == user).first()
+    if user is None:
+        return HTTPFound(location='/' + '?warning=wrong token.')
+    if user.confirmation_key != key:
+        return HTTPFound(location='/' + '?warning=wrong token.')
+    user.activated = True
+    headers = remember(request, user.id)
+    return HTTPFound(location='/' + '?warning=Thanks and welcome.',
+                     headers=headers)

File bugbro/views/gem.py

+from datetime import datetime
+import urllib2
+import re
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.url import route_url
+from pyramid.security import authenticated_userid
+
+from formalchemy import FieldSet
+from formalchemy.fields import TextAreaFieldRenderer
+
+from bugbro.security import  get_user
+from bugbro.models import Gem, Url, DBSession
+from bugbro.views import default_context, add_credit
+
+
+_RE_TITLE = re.compile('<title>(.*?)</title>', re.I | re.S | re.M)
+
+
+def _get_title(location):
+    res = urllib2.urlopen(location)
+    match = _RE_TITLE.search(res.read())
+    if match is None:
+        return ''
+    return match.group(1).strip()
+
+
+def index(request):
+    dbsession = DBSession()
+    gems = dbsession.query(Gem).order_by(Gem.creation_date).all()
+    context = default_context(request)
+    context['gems'] = gems
+    return context
+
+
+def add_gem(request):
+    logged_in = authenticated_userid(request)
+    dbsession = DBSession()
+    gem = Gem()
+    gem.title = ''
+    title_ins = "Leave blank and we'll get the page title"
+
+    fs = FieldSet(gem, session=dbsession)
+    fs.configure(include=[fs.url,
+                 fs.title.with_metadata(
+                         instructions=title_ins),
+                 fs.description.with_renderer(
+                     TextAreaFieldRenderer)])
+
+    if 'form.submitted' in request.params:
+        fs.data = request.params
+        if fs.validate():
+            fs.sync()
+
+        gem.founder = get_user(request, dbsession)
+        gem.founder_id = logged_in
+        gem.creation_date = datetime.now()
+        if not gem.title:
+            gem.title = _get_title(gem.url)
+
+        url = Url(gem.url)
+        dbsession.add(url)
+        dbsession.flush()
+        gem.url = '/url/%d' % url.id
+
+        add_credit(request, 1)
+        return HTTPFound(
+                location=route_url('thanks', request) +
+                 '?warning=Gem added.')
+
+    context = default_context(request)
+    context['form'] = fs.render()
+    return context

File bugbro/views/review.py

+from datetime import datetime
+import random
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.url import route_url
+from pyramid.security import authenticated_userid
+
+from pygments import highlight
+from pygments.lexers import DiffLexer
+from pygments.formatters import HtmlFormatter
+from pygments.styles.colorful import ColorfulStyle
+
+from sqlalchemy import not_
+from formalchemy import FieldSet
+from formalchemy.fields import TextAreaFieldRenderer
+
+from bugbro.security import  get_user
+from bugbro.models import (Review, User, OPEN, TAKEN, Url,
+                           REVIEWED, CLOSED, Team, Skill, ASKED)
+from bugbro.models import DBSession
+from bugbro.emails import send_email
+from bugbro.views import add_credit, default_context
+
+_SENDER = 'tarek@ziade.org'
+
+
+def accept_review(request):
+    review_id = int(request['bfg.routes.matchdict']['review_id'])
+    logged_in = authenticated_userid(request)
+    dbsession = DBSession()
+    review = dbsession.query(Review).filter(Review.id == review_id).first()
+    review.stage = CLOSED
+    add_credit(request, 2, review.reviewer_id)
+    add_credit(request, -1, logged_in)
+    bodydata = {'host': request.host, 'id': review.id}
+    send_email(review.reviewer_id, 'Bugbro review accepted',
+               'review_accepted', bodydata)
+
+    return HTTPFound(
+                location=route_url('review', request,
+                  review_id=review.id) +
+                 '?warning=Hope this helped.')
+
+
+def take_review(request):
+    review_id = int(request['bfg.routes.matchdict']['review_id'])
+    dbsession = DBSession()
+    review = dbsession.query(Review).filter(Review.id == review_id).first()
+    review.reviewer_id = authenticated_userid(request)
+    review.stage = TAKEN
+    return HTTPFound(
+                location=route_url('review', request, review_id=review.id) +
+                 '?warning=The review is now yours.')
+
+
+def decline_review(request):
+    review_id = int(request['bfg.routes.matchdict']['review_id'])
+    dbsession = DBSession()
+    review = dbsession.query(Review).filter(Review.id == review_id).first()
+    review.reviewer_id = ''
+    review.stage = OPEN
+    return HTTPFound(
+                location=route_url('review', request, review_id=review.id) +
+                 '?warning=Ok, no worries !')
+
+
+def add_review(request):
+    logged_in = authenticated_userid(request)
+    user = get_user(request)
+    enough_credits = user.credits > 0
+
+    dbsession = DBSession()
+    review = Review()
+    url_inst = ("If you point a page that's a raw diff, and check "
+                "the box below, we will import it")
+
+    reviewer_inst = ("Check this if you want Bugbro to look for "
+                     "a reviewer in a diffent team.")
+
+    title_inst = "Describe the feature or bugfix to be reviewed"
+    diff_inst = ("Did you know that both github.com and bitbucket.org "
+                 "offer raw diff views ?")
+    ctx_inst = "Give the reviewer some context !"
+
+    fs = FieldSet(review, session=dbsession)
+    reviewer = fs.reviewer_in_different_team.with_metadata(
+                    instructions=reviewer_inst)
+
+    fs.configure(include=[
+                  fs.title.with_metadata(instructions=title_inst),
+                  fs.patch_or_diff_url.with_metadata(instructions=url_inst),
+                  fs.url_is_raw_diff.with_metadata(instructions=diff_inst),
+                  fs.context_url.with_metadata(instructions=ctx_inst),
+                  fs.description.with_renderer(TextAreaFieldRenderer),
+                  fs.skills_required, reviewer])
+
+    if 'form.submitted' in request.params and enough_credits:
+
+        review.reviewee_id = logged_in
+        review.creation_date = datetime.now()
+        review.stage = OPEN
+
+        fs = FieldSet(review, data=request.params, session=dbsession)
+        fs.configure(include=[fs.title, fs.patch_or_diff_url,
+                            fs.url_is_raw_diff,
+                            fs.context_url,
+                            fs.description,
+                            fs.skills_required,
+                            fs.reviewer_in_different_team])
+
+        if fs.validate():
+            fs.sync()
+
+        # url shortener
+
+        # XXX async ?
+        if review.url_is_raw_diff:
+            review.load_diff()
+
+        if review.patch_or_diff_url:
+            urlob = Url(review.patch_or_diff_url)
+            dbsession.add(urlob)
+            dbsession.flush()
+            review.patch_or_diff_url = '/url/%d' % urlob.id
+
+        if review.context_url:
+            urlob = Url(review.context_url)
+            dbsession.add(urlob)
+            dbsession.flush()
+            review.context_url = '/url/%d' % urlob.id
+
+        # the review was added, let's try to find someone
+        # that matches the request
+        if review.reviewer_in_different_team:
+            # we want to find someone from another team
+            avoid_teams = user.teams
+        else:
+            # any team will work
+            avoid_teams = []
+
+        # what are the necessary skills ?
+        skills = review.skills_required
+
+        # ok let's look for some matches
+        matches = dbsession.query(User).join((User.teams, Team))
+        matches = matches.filter(User.reviewer == True)
+        matches = matches.filter(not_(Team.id.in_(
+                [t.id for t in avoid_teams])))
+        matches = matches.join((User.skills, Skill))
+        matches = matches.filter(Skill.id.in_([s.id for s in skills]))
+
+        matches = matches.all()
+
+        message = "Request added."
+        if len(matches) == 0:
+            # no one was found, making it an ORPHAN
+            review.stage = OPEN
+            message += (" We were unable to find a match, "
+                        "so wait for someone to pick it.")
+        else:
+            # we find some folks, let's pick one
+            match = random.choice(matches)
+            review.stage = ASKED
+            review.reviewer_id = match.id
+            bodydata = {'host': request.host, 'id': review.id}
+            send_email(user.id, 'Bugbro review request', 'review',
+                       bodydata)
+
+        url = route_url('review', request, review_id=review.id)
+        return HTTPFound(location=url + '?warning=%s' % message)
+
+    context = default_context(request)
+    context['form'] = fs.render()
+    context['enough_credits'] = enough_credits
+    return context
+
+
+def review(request):
+    logged_in = authenticated_userid(request)
+    dbsession = DBSession()
+    review_id = int(request['bfg.routes.matchdict']['review_id'])
+    review = dbsession.query(Review).filter(Review.id == review_id).first()
+    context = default_context(request)
+
+    fs = FieldSet(review, session=dbsession)
+
+    if 'form.submitted' in request.params:
+        fs.data = request.params
+        fs.configure(include=[fs.review])
+
+        if fs.validate():
+            fs.sync()
+
+        review.stage = REVIEWED
+        bodydata = {'host': request.host, 'id': review.id}
+        send_email(review.reviewee_id, 'Bugbro review done !',
+                   'review_done', bodydata)
+
+        return HTTPFound(
+                location=route_url('review', request, review_id=review.id) +
+                 '?warning=Thanks !!.')
+
+    fields = [fs.patch_or_diff_url.readonly(),
+              fs.context_url.readonly()]
+
+    if review.description:
+        fields.append(fs.description.readonly())
+
+    rev_field = fs.review.with_renderer(TextAreaFieldRenderer)
+
+    if logged_in == review.reviewer_id:
+        if review.stage not in (TAKEN, REVIEWED):
+            rev_field = rev_field.readonly()
+
+        fields.append(rev_field)
+        context['is_reviewer'] = True
+    else:
+        context['is_reviewer'] = False
+        if review.stage in (CLOSED, REVIEWED):
+            fields.append(rev_field.readonly())
+
+    context['is_reviewee'] = logged_in == review.reviewee_id
+    fs.configure(include=fields)
+    context['rendered_review'] = fs.render()
+    context['review'] = review
+
+    # XXX to include in FA
+    if review.url_is_raw_diff and review.diff is not None:
+        context['diff'] = _diff2html(review.diff)
+
+    return context
+
+
+def _diff2html(diff):
+    return highlight(diff, DiffLexer(), HtmlFormatter(style=ColorfulStyle))
+
+
+def index(request):
+    dbsession = DBSession()
+    reviews = dbsession.query(Review).filter(
+            Review.stage != ASKED).order_by(Review.stage,
+            Review.creation_date).limit(20).all()
+    context = default_context(request)
+    context['reviews'] = reviews
+    return context

File development.ini

+[app:bugbro]
+use = egg:bugbro
+reload_templates = true
+debug_authorization = true
+debug_notfound = true
+debug_routematch = true
+debug_templates = true
+default_locale_name = en
+sqlalchemy.url = mysql://bugbro:bugbro@localhost/bugbro
+#sqlite:///%(here)s/bugbro.db
+mako.directories = bugbro:templates
+
+[pipeline:main]
+pipeline =
+    tm
+    bugbro
+
+[filter:tm]
+use = egg:repoze.tm2#tm
+commit_veto = repoze.tm:default_commit_veto
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, bugbro, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_bugbro]
+level = DEBUG
+handlers =
+qualname = bugbro
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither.  (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration

File production.ini

+[app:bugbro]
+use = egg:bugbro
+reload_templates = false
+debug_authorization = false
+debug_notfound = false
+debug_routematch = false
+debug_templates = false
+default_locale_name = en
+sqlalchemy.url = sqlite:///%(here)s/bugbro.db
+
+[filter:weberror]
+use = egg:WebError#error_catcher
+debug = false
+;error_log = 
+;show_exceptions_in_wsgi_errors = true
+;smtp_server = localhost
+;error_email = janitor@example.com
+;smtp_username = janitor
+;smtp_password = "janitor's password"
+;from_address = paste@localhost
+;error_subject_prefix = "Pyramid Error"
+;smtp_use_tls =
+;error_message =
+
+[filter:tm]
+use = egg:repoze.tm2#tm
+commit_veto = repoze.tm:default_commit_veto
+
+[pipeline:main]
+pipeline =
+    weberror
+    tm
+    bugbro
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, bugbro, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_bugbro]
+level = WARN
+handlers =
+qualname = bugbro
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither.  (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration
+[nosetests]
+match=^test
+nocapture=1
+cover-package=bugbro
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = bugbro/locale
+domain = bugbro
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = bugbro/locale/bugbro.pot
+width = 80
+
+[init_catalog]
+domain = bugbro
+input_file = bugbro/locale/bugbro.pot
+output_dir = bugbro/locale
+
+[update_catalog]
+domain = bugbro
+input_file = bugbro/locale/bugbro.pot
+output_dir = bugbro/locale
+previous = true
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+    'pyramid',
+    'SQLAlchemy',
+    'FormAlchemy',
+    'pyramid_formalchemy',
+    'pygments',
+    'fa.jquery',
+    'transaction',
+    'repoze.tm2>=1.0b1', # default_commit_veto
+    'zope.sqlalchemy',
+    'WebError',
+    ]
+
+if sys.version_info[:3] < (2,5,0):
+    requires.append('pysqlite')
+
+setup(name='bugbro',
+      version='0.0',
+      description='bugbro',
+      long_description=README + '\n\n' +  CHANGES,
+      classifiers=[
+        "Programming Language :: Python",
+        "Framework :: Pylons",
+        "Topic :: Internet :: WWW/HTTP",
+        "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+        ],
+      author='',
+      author_email='',
+      url='',
+      keywords='web wsgi bfg pylons pyramid',
+      packages=find_packages(),
+      include_package_data=True,
+      zip_safe=False,
+      test_suite='bugbro',
+      install_requires = requires,
+      entry_points = """\
+      [paste.app_factory]
+      main = bugbro:main
+      """,
+      paster_plugins=['pyramid'],
+      )
+