review/web.py @ 78d9f73badd9

web: refactoring, colorbox changes, avatar border-radii
author Steve Losh <steve@stevelosh.com>
date Fri, 18 Jun 2010 20:57:12 -0400
parents 34df61cd0a47
children 63bcbedb9341
from __future__ import with_statement

"""The review extension's web UI."""

import sys, os
from hashlib import md5

from mercurial import commands, hg, templatefilters
from mercurial.node import short
from mercurial.util import email

import api

def unbundle():
    package_path = os.path.split(os.path.realpath(__file__))[0]
    template_path = os.path.join(package_path, 'web_templates')
    media_path = os.path.join(package_path, 'web_media')
    top_path = os.path.split(package_path)[0]
    bundled_path = os.path.join(top_path, 'bundled')
    flask_path = os.path.join(bundled_path, 'flask')
    jinja2_path = os.path.join(bundled_path, 'jinja2')
    werkzeug_path = os.path.join(bundled_path, 'werkzeug')
    simplejson_path = os.path.join(bundled_path, 'simplejson')

    sys.path.insert(0, flask_path)
    sys.path.insert(0, werkzeug_path)
    sys.path.insert(0, jinja2_path)
    sys.path.insert(0, simplejson_path)

unbundle()

from flask import Flask
from flask import abort, g, redirect, render_template, request
app = Flask(__name__)

LOG_PAGE_LEN = 15

def _item_gravatar(item, size=30):
    return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(item.author)).hexdigest(), size)

def _cset_gravatar(cset, size=30):
    return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(cset.user())).hexdigest(), size)

def _line_type(line):
    return 'rem' if line[0] == '-' else 'add' if line[0] == '+' else 'con'

def _categorize_signoffs(signoffs):
    return { 'yes': len(filter(lambda s: s.opinion == 'yes', signoffs)),
             'no': len(filter(lambda s: s.opinion == 'no', signoffs)),
             'neutral': len(filter(lambda s: s.opinion == '', signoffs)),}
utils = {
    'node_short': short,
    'md5': md5,
    'email': email,
    'templatefilters': templatefilters,
    'len': len,
    'item_gravatar': _item_gravatar,
    'cset_gravatar': _cset_gravatar,
    'line_type': _line_type,
    'categorize_signoffs': _categorize_signoffs,
    'map': map,
    'str': str,
    'decode': lambda s: s.decode('utf-8'),
}

def _render(template, **kwargs):
    return render_template(template, read_only=app.read_only,
        allow_anon=app.allow_anon, utils=utils, datastore=g.datastore,
        title=app.title, **kwargs)


@app.before_request
def load_datastore():
    g.datastore = api.ReviewDatastore(app.ui, hg.repository(app.ui, app.repo.root))

@app.route('/')
def index_newest():
    return index(-1)

@app.route('/<int:rev_max>/')
def index(rev_max):
    tip = g.datastore.target['tip'].rev()

    if rev_max > tip or rev_max < 0:
        rev_max = tip

    rev_min = rev_max - LOG_PAGE_LEN if rev_max >= LOG_PAGE_LEN else 0
    if rev_min < 0:
        rev_min = 0

    older = rev_min - 1 if rev_min > 0 else -1
    newer = rev_max + LOG_PAGE_LEN + 1 if rev_max < tip else -1
    if newer > tip:
        newer = tip

    rcsets = [g.datastore[r] for r in xrange(rev_max, rev_min - 1, -1)]
    return _render('index.html', rcsets=rcsets, newer=newer, older=older)


def _handle_signoff(revhash):
    signoff = request.form.get('signoff', None)

    if signoff not in ['yes', 'no', 'neutral']:
        abort(400)

    if signoff == 'neutral':
        signoff = ''

    body = request.form.get('new-signoff-body', '')
    rcset = g.datastore[revhash]
    rcset.add_signoff(body, signoff, force=True)

    return redirect("%s/changeset/%s/" % (app.site_root, revhash))

def _handle_comment(revhash):
    filename = request.form.get('filename', '')
    lines = str(request.form.get('lines', ''))
    if lines:
        lines = filter(None, [l.strip() for l in lines.split(',')])
    body = request.form['new-comment-body']
    
    if body:
        rcset = g.datastore[revhash]
        rcset.add_comment(body, filename, lines)
    
    return redirect("%s/changeset/%s/" % (app.site_root, revhash))

@app.route('/changeset/<revhash>/', methods=['GET', 'POST'])
def changeset(revhash):
    if request.method == 'POST':
        signoff = request.form.get('signoff', None)
        if signoff and not app.read_only:
            return _handle_signoff(revhash)
        elif not app.read_only or app.allow_anon:
            return _handle_comment(revhash)
    
    rcset = g.datastore[revhash]
    rev = rcset.target[revhash]
    
    cu_signoffs = rcset.signoffs_for_current_user()
    cu_signoff = cu_signoffs[0] if cu_signoffs else None
    
    tip = g.datastore.target['tip'].rev()
    newer = rcset.target[rev.rev() + 1] if rev.rev() < tip else None
    older = rcset.target[rev.rev() - 1] if rev.rev() > 0 else None
    
    return _render('changeset.html', rcset=rcset, rev=rev, cu_signoff=cu_signoff,
        newer=newer, older=older)


@app.route('/pull/', methods=['POST'])
def pull():
    if not app.read_only:
        path = request.form['path']
        commands.pull(g.datastore.repo.ui, g.datastore.repo, path, update=True)
    return redirect('%s/' % app.site_root)

@app.route('/push/', methods=['POST'])
def push():
    if not app.read_only:
        path = request.form['path']
        commands.push(g.datastore.repo.ui, g.datastore.repo, path)
    return redirect('%s/' % app.site_root)


def load_interface(ui, repo, read_only=False, allow_anon=False,
        open=False, address='127.0.0.1', port=8080):
    if open:
        import webbrowser
        webbrowser.open('http://localhost:%d/' % port)
        
    app.read_only = read_only
    app.debug = ui.debugflag
    app.allow_anon = allow_anon
    app.site_root = ''

    if app.allow_anon:
        ui.setconfig('ui', 'username', 'Anonymous <anonymous@example.com>')

    app.ui = ui
    app.repo = repo
    app.title = os.path.basename(repo.root)

    if app.debug:
        from flaskext.lesscss import lesscss
        lesscss(app)
    app.run(host=address, port=port)