[svn] checked in incomplete lodgeit in order to install it as private beta on the server

This commit is contained in:
blackbird 2007-04-09 16:21:23 +02:00
parent a0ec7ac5ea
commit b36b5e4106
63 changed files with 4020 additions and 1 deletions

10
lodgeit/__init__.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
lodgeit
~~~~~~~
The lodgeit pastebin.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""

146
lodgeit/application.py Normal file
View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
"""
lodgeit.application
~~~~~~~~~~~~~~~~~~~
the WSGI application
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import os
import sqlalchemy
from datetime import datetime, timedelta
from wsgitk.wrappers import BaseRequest, BaseResponse
from wsgitk.static import StaticExports
from jinja import Environment, PackageLoader
from lodgeit.urls import urlmap
from lodgeit.controllers import get_controller
from lodgeit.database import metadata, generate_user_hash, Paste
#: jinja environment for all the templates
jinja_environment = Environment(loader=PackageLoader('lodgeit', 'views',
use_memcache=True,
cache_folder='/tmp',
auto_reload=True
))
def datetimeformat():
"""
Helper filter for the template
"""
def wrapped(env, ctx, value):
return value.strftime('%Y-%m-%d %H:%M')
return wrapped
jinja_environment.filters['datetimeformat'] = datetimeformat
def render_template(req, template_name, **context):
"""
Render a template to a response. This automatically fetches
the list of new replies for the layout template. It also
adds the current request to the context. This is used for the
welcome message.
"""
if req.method == 'GET':
context['new_replies'] = Paste.fetch_replies(req)
context['request'] = req
t = jinja_environment.get_template(template_name)
return Response(t.render(context), mimetype='text/html; charset=utf-8')
def redirect(url, code=302):
"""
Redirect to somewhere. Returns a nice response object.
"""
return Response('Page Moved to %s' % url,
headers=[('Location', url),
('Content-Type', 'text/plain')],
status=302)
class Request(BaseRequest):
"""
Subclass of the `BaseRequest` object. automatically creates a new
`user_hash` and sets `first_visit` to `True` if it's a new user.
It also stores the engine and dbsession on it.
"""
def __init__(self, environ, engine):
self.engine = engine
self.dbsession = sqlalchemy.create_session(engine)
super(Request, self).__init__(environ)
# check the user hash. an empty cookie is considered
# begin a new session.
self.user_hash = ''
self.first_visit = False
if 'user_hash' in self.cookies:
self.user_hash = self.cookies['user_hash'].value
if not self.user_hash:
self.user_hash = generate_user_hash()
self.first_visit = True
class Response(BaseResponse):
"""
Subclass the response object for later extension.
"""
class PageNotFound(Exception):
"""
Internal exception used to tell the application to show the
error page.
"""
class LodgeIt(object):
"""
The WSGI Application
"""
def __init__(self, dburi):
#: name of the error handler
self.not_found = ('static/not_found', {})
self.engine = sqlalchemy.create_engine(dburi)
#: make sure all tables exist.
metadata.create_all(self.engine)
def __call__(self, environ, start_response):
"""
Minimal WSGI application for request dispatching.
"""
req = Request(environ, self.engine)
rv = urlmap.test(environ.get('PATH_INFO', ''))
try:
if rv is None:
raise PageNotFound()
handler = get_controller(rv[0], req)
response = handler(**rv[1])
except PageNotFound:
handler = get_controller(self.not_found[0], req)
response = handler(**self.not_found[1])
# on first visit we send out the cookie
if req.first_visit:
response.set_cookie('user_hash', req.user_hash,
expires=datetime.utcnow() + timedelta(days=31)
)
# call the response as WSGI app
return response(environ, start_response)
def make_app(dburi):
"""
Apply the used middlewares and create the application.
"""
static_path = os.path.join(os.path.dirname(__file__), 'static')
app = LodgeIt(dburi)
app = StaticExports(app, {
'/static': static_path
})
return app

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""
lodgeit.controllers
~~~~~~~~~~~~~~~~~~~
Module that helds the controllers
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
class BaseController(object):
"""
Base controller. add some stuff to the dict on instanciation
"""
def __init__(self, req):
self.request = req
self.engine = req.engine
self.dbsession = req.dbsession
def get_controller(name, req):
cname, hname = name.split('/')
module = __import__('lodgeit.controllers.' + cname, None, None, [''])
controller = module.controller(req)
return getattr(controller, hname)

View File

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""
lodgeit.controllers.pastes
~~~~~~~~~~~~~~~~~~~~~~~~~~
The paste controller
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import sqlalchemy as meta
from lodgeit.application import render_template, redirect, PageNotFound
from lodgeit.controllers import BaseController
from lodgeit.database import Paste
from lodgeit.lib.highlighting import LANGUAGES, STYLES, get_style
from lodgeit.lib.pagination import generate_pagination
class PasteController(BaseController):
"""
Provides all the handler callback for paste related stuff.
"""
def new_paste(self):
"""
The 'create a new paste' view.
"""
pastes = self.dbsession.query(Paste)
if self.request.method == 'POST':
code = self.request.POST.get('code')
language = self.request.POST.get('language')
parent = self.request.POST.get('parent')
if parent is not None:
parent = pastes.selectfirst(Paste.c.paste_id == parent)
if code and language:
paste = Paste(code, language, parent, self.request.user_hash)
self.dbsession.save(paste)
self.dbsession.flush()
return redirect(paste.url)
parent = self.request.GET.get('reply_to')
if parent is not None:
parent = pastes.selectfirst(Paste.c.paste_id == parent)
return render_template(self.request, 'new_paste.html',
languages=LANGUAGES,
parent=parent
)
def show_paste(self, paste_id):
"""
Show an existing paste.
"""
pastes = self.dbsession.query(Paste)
paste = pastes.selectfirst(Paste.c.paste_id == paste_id)
if paste is None:
raise PageNotFound()
style, css = get_style(self.request)
return render_template(self.request, 'show_paste.html',
paste=paste,
style=style,
css=css,
styles=STYLES
)
def show_tree(self, paste_id):
"""
Display the tree of some related pastes.
"""
paste = Paste.resolve_root(self.dbsession, paste_id)
if paste is None:
raise PageNotFound()
return render_template(self.request, 'paste_tree.html',
paste=paste,
current=paste_id
)
def show_all(self, page=1):
"""
Paginated list of pages.
"""
def link(page):
if page == 1:
return '/all/'
return '/all/%d' % page
pastes = self.dbsession.query(Paste).select(
order_by=[meta.desc(Paste.c.pub_date)],
limit=10,
offset=10 * (page - 1)
)
if not pastes and page != 1:
raise PageNotFound()
return render_template(self.request, 'show_all.html',
pastes=pastes,
pagination=generate_pagination(page, 10,
Paste.count(self.request.engine), link),
css=get_style(self.request)[1]
)
def compare_paste(self, new_id=None, old_id=None):
"""
Render a diff view for two pastes.
"""
# redirect for the compare form box
if old_id is new_id is None:
old_id = self.request.POST.get('old', '-1').lstrip('#')
new_id = self.request.POST.get('new', '-1').lstrip('#')
return redirect('/compare/%s/%s' % (old_id, new_id))
pastes = self.dbsession.query(Paste)
old = pastes.selectfirst(Paste.c.paste_id == old_id)
new = pastes.selectfirst(Paste.c.paste_id == new_id)
if old is None or new is None:
raise PageNotFound()
return render_template(self.request, 'compare_paste.html',
old=old,
new=new,
diff=old.compare_to(new, template=True)
)
def set_colorscheme(self):
"""
Minimal view that updates the style session cookie. Redirects
back to the page the user is coming from.
"""
style_name = self.request.POST.get('style')
resp = redirect(self.request.environ.get('HTTP_REFERER') or '/')
if style_name in STYLES:
resp.set_cookie('style', style_name)
return resp
controller = PasteController

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""
lodgeit.controllers.static
~~~~~~~~~~~~~~~~~~~~~~~~~~
Static stuff.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
from lodgeit.application import render_template
from lodgeit.controllers import BaseController
class StaticController(BaseController):
def not_found(self):
return render_template(self.request, 'not_found.html')
def about(self):
return render_template(self.request, 'about.html')
controller = StaticController

View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
"""
lodgeit.controllers.xmlrpc
~~~~~~~~~~~~~~~~~~~~~~~~~~
The XMLRPC controller
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import sqlalchemy as meta
from lodgeit.application import render_template
from lodgeit.controllers import BaseController
from lodgeit.database import Paste
from lodgeit.lib.xmlrpc import xmlrpc, exported
from lodgeit.lib.highlighting import STYLES, LANGUAGES, get_style
class XmlRpcController(BaseController):
def handle_request(self):
if self.request.method == 'POST':
return xmlrpc.handle_request(self.request)
return render_template(self.request, 'xmlrpc.html',
methods=xmlrpc.get_public_methods(),
interface_url='http://%s/xmlrpc/' %
self.request.environ['SERVER_NAME']
)
@exported('pastes.newPaste')
def pastes_new_paste(request, language, code, parent_id=None):
"""Create a new paste."""
paste = Paste(code, language, parent_id)
request.dbsession.save(paste)
request.dbsession.flush()
return {
'paste_id': paste.paste_id,
'url': paste.url
}
@exported('pastes.getPaste')
def pastes_get_paste(request, paste_id):
"""Get all known information about a paste by a given paste id."""
paste = request.dbsession.query(Paste).selectfirst(Paste.c.paste_id ==
paste_id)
if paste is None:
return False
return paste.to_dict()
@exported('pastes.getRecent')
def pastes_get_recent(request, amount=5):
"""Return the last amount pastes."""
amount = min(amount, 20)
return [x.to_dict() for x in
request.dbsession.query(Paste).select(
order_by=[meta.desc(Paste.c.pub_date)],
limit=amount
)]
@exported('pastes.getLast')
def pastes_get_last(request):
"""Get the most recent paste."""
rv = pastes_get_recent(request, 1)
if rv:
return rv[0]
return {}
@exported('pastes.getLanguages')
def pastes_get_languages(request):
"""Get a list of supported languages."""
return LANGUAGES.items()
@exported('styles.getStyles')
def styles_get_styles(request):
"""Get a list of supported styles."""
return STYLES.items()
@exported('styles.getStylesheet')
def styles_get_stylesheet(request, name):
"""Return the stylesheet for a given style."""
return get_style(name)
controller = XmlRpcController

146
lodgeit/database.py Normal file
View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
"""
lodgeit.database
~~~~~~~~~~~~~~~~
Database fun :)
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import time
import difflib
import sqlalchemy as meta
from random import random
from time import time
from hashlib import sha1
from datetime import datetime
from lodgeit.lib.highlighting import highlight, LANGUAGES
metadata = meta.MetaData()
pastes = meta.Table('pastes', metadata,
meta.Column('paste_id', meta.Integer, primary_key=True),
meta.Column('code', meta.Unicode),
meta.Column('parsed_code', meta.Unicode),
meta.Column('parent_id', meta.Integer, meta.ForeignKey('pastes.paste_id'),
nullable=True),
meta.Column('pub_date', meta.DateTime),
meta.Column('language', meta.Unicode(30)),
meta.Column('user_hash', meta.Unicode(40), nullable=True),
meta.Column('handled', meta.Boolean, nullable=False)
)
def generate_user_hash():
return sha1('%s|%s' % (random(), time())).hexdigest()
class Paste(object):
def __init__(self, code, language, parent=None, user_hash=None):
if language not in LANGUAGES:
raise ValueError('unsupported language %r' % language)
self.code = code
self.language = language
self.rehighlight()
if isinstance(parent, Paste):
self.parent = parent
elif parent is not None:
self.parent_id = parent
self.pub_date = datetime.now()
self.handled = False
self.user_hash = user_hash
@property
def url(self):
return '/show/%d' % self.paste_id
def compare_to(self, other, context_lines=4, template=False):
udiff = u'\n'.join(difflib.unified_diff(
self.code.splitlines(),
other.code.splitlines(),
fromfile='Paste #%d' % self.paste_id,
tofile='Paste #%d' % other.paste_id,
lineterm='',
n=context_lines
))
if template:
from lodgeit.lib.diff import prepare_udiff
rv = prepare_udiff(udiff)
return rv and rv[0] or None
return udiff
def rehighlight(self):
self.parsed_code = highlight(self.code, self.language)
def to_dict(self):
return {
'paste_id': self.paste_id,
'code': self.code,
'parsed_code': self.parsed_code,
'pub_date': int(time.mktime(self.pub_date.timetuple())),
'language': self.language,
'parent_id': self.parent_id,
'url': self.url
}
def render_preview(self):
try:
start = self.parsed_code.index('</pre>')
code = self.parsed_code[
self.parsed_code.index('<pre>', start) + 5:
self.parsed_code.rindex('</pre>')
].strip('\n').splitlines()
except IndexError:
code = ''.strip('\n').splitlines()
code = '\n'.join(code[:5] + ['...'])
return '<pre class="syntax">%s</pre>' % code
@staticmethod
def fetch_replies(req):
"""
Get the new replies for the owern of a request and flag them
as handled.
"""
s = meta.select([pastes.c.paste_id],
pastes.c.user_hash == req.user_hash
)
paste_list = req.dbsession.query(Paste).select(
(Paste.c.parent_id.in_(s)) &
(Paste.c.handled == False) &
(Paste.c.user_hash != req.user_hash),
order_by=[meta.desc(Paste.c.pub_date)]
)
to_mark = [p.paste_id for p in paste_list]
req.engine.execute(pastes.update(pastes.c.paste_id.in_(*to_mark)),
handled=True
)
return paste_list
@staticmethod
def count(con):
s = meta.select([meta.func.count(pastes.c.paste_id)])
return con.execute(s).fetchone()[0]
@staticmethod
def resolve_root(sess, paste_id):
q = sess.query(Paste)
while True:
paste = q.selectfirst(Paste.c.paste_id == paste_id)
if paste is None:
return
if paste.parent_id is None:
return paste
paste_id = paste.parent_id
meta.mapper(Paste, pastes, properties={
'children': meta.relation(Paste,
primaryjoin=pastes.c.parent_id==pastes.c.paste_id,
cascade='all',
backref=meta.backref('parent', remote_side=[pastes.c.paste_id])
)
})

0
lodgeit/lib/__init__.py Normal file
View File

170
lodgeit/lib/diff.py Normal file
View File

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""
lodgeit.lib.diff
~~~~~~~~~~~~~~~~
Render a nice diff between two things.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import re
from lodgeit.application import jinja_environment
from cgi import escape
def prepare_udiff(udiff):
"""
Prepare an udiff for a template
"""
renderer = DiffRenderer(udiff)
return renderer.prepare()
class DiffRenderer(object):
"""
Give it a unified diff and it renders you a beautiful
html diff :-)
"""
_chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
def __init__(self, udiff):
"""
:param udiff: a text in udiff format
"""
self.lines = [escape(line) for line in udiff.splitlines()]
def _extract_rev(self, line1, line2):
try:
if line1.startswith('--- ') and line2.startswith('+++ '):
filename, old_rev = line1[4:].split(None, 1)
new_rev = line2[4:].split(None, 1)[1]
return filename, 'Old', 'New'
except (ValueError, IndexError):
pass
return None, None, None
def _highlight_line(self, line, next):
"""
Highlight inline changes in both lines.
"""
start = 0
limit = min(len(line['line']), len(next['line']))
while start < limit and line['line'][start] == next['line'][start]:
start += 1
end = -1
limit -= start
while -end <= limit and line['line'][end] == next['line'][end]:
end -= 1
end += 1
if start or end:
def do(l):
last = end + len(l['line'])
if l['action'] == 'add':
tag = 'ins'
else:
tag = 'del'
l['line'] = u'%s<%s>%s</%s>%s' % (
l['line'][:start],
tag,
l['line'][start:last],
tag,
l['line'][last:]
)
do(line)
do(next)
def _parse_udiff(self):
"""
Parse the diff an return data for the template.
"""
lineiter = iter(self.lines)
files = []
try:
line = lineiter.next()
while True:
# continue until we found the old file
if not line.startswith('--- '):
line = lineiter.next()
continue
chunks = []
filename, old_rev, new_rev = \
self._extract_rev(line, lineiter.next())
files.append({
'filename': filename,
'old_revision': old_rev,
'new_revision': new_rev,
'chunks': chunks
})
line = lineiter.next()
while line:
match = self._chunk_re.match(line)
if not match:
break
lines = []
chunks.append(lines)
old_line, old_end, new_line, new_end = \
[int(x or 1) for x in match.groups()]
old_line -= 1
new_line -= 1
old_end += old_line
new_end += new_line
line = lineiter.next()
while old_line < old_end or new_line < new_end:
if line:
command, line = line[0], line[1:]
else:
command = ' '
affects_old = affects_new = False
if command == ' ':
affects_old = affects_new = True
action = 'unmod'
elif command == '+':
affects_new = True
action = 'add'
elif command == '-':
affects_old = True
action = 'del'
else:
raise RuntimeError()
old_line += affects_old
new_line += affects_new
lines.append({
'old_lineno': affects_old and old_line or u'',
'new_lineno': affects_new and new_line or u'',
'action': action,
'line': line
})
line = lineiter.next()
except StopIteration:
pass
# highlight inline changes
for file in files:
for chunk in chunks:
lineiter = iter(chunk)
first = True
try:
while True:
line = lineiter.next()
if line['action'] != 'unmod':
nextline = lineiter.next()
if nextline['action'] == 'unmod' or \
nextline['action'] == line['action']:
continue
self._highlight_line(line, nextline)
except StopIteration:
pass
return files
def prepare(self):
return self._parse_udiff()

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
lodgeit.lib.highlighting
~~~~~~~~~~~~~~~~~~~~~~~~
Highlighting helpers.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import pygments
from pygments.util import ClassNotFound
from pygments.lexers import get_lexer_by_name
from pygments.styles import get_all_styles
from pygments.formatters import HtmlFormatter
#: we use a hardcoded list here because we want to keep the interface
#: simple
LANGUAGES = {
'text': 'Text',
'python': 'Python',
'pycon': 'Python Console Sessions',
'pytb': 'Python Tracebacks',
'html+php': 'PHP',
'html+django': 'Django / Jinja Templates',
'html+mako': 'Mako Templates',
'html+myghty': 'Myghty Templates',
'apache': 'Apache Config (.htaccess)',
'bash': 'Bash',
'bat': 'Batch (.bat)',
'c': 'C',
'cpp': 'C++',
'csharp': 'C#',
'css': 'CSS',
'smarty': 'Smarty',
'html+php': 'PHP',
'html+genshi': 'Genshi Templates',
'js': 'JavaScript',
'java': 'Java',
'jsp': 'JSP',
'lua': 'Lua',
'haskell': 'Haskell',
'scheme': 'Scheme',
'ruby': 'Ruby',
'rhtml': 'eRuby / rhtml',
'tex': 'TeX / LaTeX',
'xml': 'XML'
}
STYLES = dict((x, x.title()) for x in get_all_styles())
def highlight(code, language):
"""
Highlight a given code to HTML
"""
lexer = get_lexer_by_name(language)
return pygments.highlight(code, lexer, formatter)
def get_style(request):
"""
Style for a given request
"""
if isinstance(request, basestring):
style_name = request
else:
style_name = request.cookies.get('style')
if style_name:
style_name = style_name.value
else:
style_name = 'pastie'
try:
f = HtmlFormatter(style=style_name)
except ClassNotFound:
return style_name, ''
return style_name, f.get_style_defs(('#paste', '.syntax'))
formatter = HtmlFormatter(linenos=True, cssclass='syntax', style='pastie')

84
lodgeit/lib/pagination.py Normal file
View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
lodgeit.lib.pagination
~~~~~~~~~~~~~~~~~~~~~~
Fancy Pagination.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import math
def generate_pagination(page, per_page, total, link_builder=None,
normal='<a href="%(url)s">%(page)d</a>',
active='<strong>%(page)d</strong>',
commata=',\n', ellipsis=' ...\n', threshold=3,
prev_link=True, next_link=True,
gray_prev_link=True, gray_next_link=True):
"""
Generates a pagination.
:param page: current page number
:param per_page: items per page
:param total: total number of items
:param link_builder: a function which is called with a page number
and has to return the link to a page. Per
default it links to ``?page=$PAGE``
:param normal: template for normal (not active) link
:param active: template for active link
:param commata: inserted into the output to separate two links
:param ellipsis: inserted into the output to display an ellipsis
:param threshold: number of links next to each node (left end,
right end and current page)
:param prev_link: display back link
:param next_link: dipslay next link
:param gray_prev_link: the back link is displayed as span class disabled
if no backlink exists. otherwise it's not
displayed at all
:param gray_next_link: like `gray_prev_link` just for the next page link
"""
page = int(page or 1)
if link_builder is None:
link_builder = lambda page: '?page=%d' % page
was_ellipsis = False
result = []
pages = int(math.ceil(total / float(per_page)))
prev = None
next = None
for num in xrange(1, pages + 1):
if num - 1 == page:
next = num
if num + 1 == page:
prev = num
if num <= threshold or num > pages - threshold or \
abs(page - num) < math.ceil(threshold / 2.0):
if result and result[-1] != ellipsis:
result.append(commata)
was_space = False
link = link_builder(num)
template = num == page and active or normal
result.append(template % {
'url': link,
'page': num
})
elif not was_ellipsis:
was_ellipsis = True
result.append(ellipsis)
if next_link:
if next is not None:
result.append(u' <a href="%s">Next &raquo;</a>' %
link_builder(next))
elif gray_next_link:
result.append(u' <span class="disabled">Next &raquo;</span>')
if prev_link:
if prev is not None:
result.insert(0, u'<a href="%s">&laquo; Prev</a> ' %
link_builder(prev))
elif gray_prev_link:
result.insert(0, u'<span class="disabled">&laquo; Prev</span> ')
return u''.join(result)

60
lodgeit/lib/xmlrpc.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""
lodgeit.lib.xmlrpc
~~~~~~~~~~~~~~~~~~
XMLRPC helper stuff.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
import inspect
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
from lodgeit.application import Response
class XMLRPCRequestHandler(SimpleXMLRPCDispatcher):
def __init__(self):
SimpleXMLRPCDispatcher.__init__(self, True, 'utf-8')
def handle_request(self, request):
def dispatch(method_name, params):
method = self.funcs[method_name]
if method_name.startswith('system.'):
return method(*params)
return method(request, *params)
response = self._marshaled_dispatch(request.data, dispatch)
return Response(response, mimetype='text/xml')
def get_public_methods(self):
if not hasattr(self, '_public_methods'):
result = []
for name, f in self.funcs.iteritems():
if name.startswith('system.'):
continue
args, varargs, varkw, defaults = inspect.getargspec(f)
result.append({
'name': name,
'doc': inspect.getdoc(f) or '',
'signature': inspect.formatargspec(
args, varargs, varkw, defaults,
formatvalue=lambda o: '=' + repr(o)
)
})
result.sort(key=lambda x: x['name'].lower())
self._public_methods = result
return self._public_methods
xmlrpc = XMLRPCRequestHandler()
xmlrpc.register_introspection_functions()
def exported(name):
"""Make a function external available via xmlrpc."""
def proxy(f):
xmlrpc.register_function(f, name)
return f
return proxy

21
lodgeit/static/cookie.js Normal file
View File

@ -0,0 +1,21 @@
/**
* add very basic Cookie features to jquery
*/
jQuery.cookie = function(name, value) {
if (typeof value != 'undefined') {
document.cookie = name + '=' + encodeURIComponent(value);
}
else {
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
if (cookie.substring(0, name.length + 1) == (name + '=')) {
return decodeURIComponent(cookie.substring(name.length + 1));
}
}
}
return null;
}
};

2245
lodgeit/static/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

127
lodgeit/static/lodgeit.js Normal file
View File

@ -0,0 +1,127 @@
/**
* LodgeIt JavaScript Module
*
* addes fancy and annoying javascript effects to that page
* but hey. now it's web2.0!!!!111
*/
var LodgeIt = {
init : function() {
/**
* make textarea 1px height and save the value for resizing
* in a variable.
*/
var textarea = $('textarea');
var submitform = $('form.submitform');
var textareaHeight = $.cookie('ta_height');
if (textareaHeight) {
textareaHeight = parseInt(textareaHeight);
}
else {
textareaHeight = textarea.height();
}
submitform.hide();
textarea.css('height', '1px');
/**
* links marked with "autoclose" inside the related div
* use some little magic to get an auto hide animation on
* click, before the actual request is sent to the browser.
*/
$('div.related div.content a.autoclose').each(function() {
this.onclick = function() {
var href = this.getAttribute('href');
$('div.related div.content').slideUp(300, function() {
document.location.href = href;
});
return false;
};
});
/**
* and here we do something similar for the forms. block
* submitting until the close animation is done.
*/
$('div.related form').each(function() {
var submit = false;
var self = this;
this.onsubmit = function() {
if (submit)
return true;
$('div.related div.content').slideUp(300, function() {
submit = true;
self.submit();
});
return false;
};
});
/**
* now where everything is done resize the textarea
* we do this at the end to speed things up on slower systems
* this code is only used for the frontpage.
*/
textarea.animate({
height: textareaHeight
}, textareaHeight * 1.2, 'linear', function() {
textarea[0].focus();
});
submitform.fadeIn(textareaHeight, function() {
// small workaround in order to not slow firefox down
submitform.css('opacity', 'inherit');
});
},
/**
* slide-toggle the related links box
*/
toggleRelatedBox : function() {
$('div.related div.content').slideToggle(500);
},
/**
* fade the line numbers in and out
*/
toggleLineNumbers : function() {
$('#paste td.linenos').each(function() {
var state = $(this).is(':hidden') ? 'show' : 'hide';
$(this).animate({
opacity: state
}, 200);
});
},
/**
* Textarea resizer helper
*/
resizeTextarea : function(step) {
var textarea = $('textarea');
var oldHeight = textarea.height();
var newHeight = oldHeight + step;
if (newHeight >= 100) {
$.cookie('ta_height', newHeight);
textarea.animate({
height: newHeight
}, 200);
}
},
/**
* hide the notification box
*/
hideNotification : function() {
$('div.notification').slideUp(300);
},
/**
* remove user hash cookie
*/
removeCookie : function() {
if (confirm('Do really want to remove your cookie?')) {
$.cookie('user_hash', '');
alert('Your cookie was resetted!');
}
}
};
$(document).ready(LodgeIt.init);

321
lodgeit/static/style.css Normal file
View File

@ -0,0 +1,321 @@
/**
* New Lodge It Style
*/
body {
background-color: #333;
margin: 0;
padding: 0;
font-family: 'Trebuchet MS', sans-serif;
font-size: 15px;
}
div.page {
margin: 30px;
padding: 10px;
background-color: white;
}
h1 {
font-size: 40px;
margin: 0;
color: #cd0000;
}
h2 {
font-size: 24px;
margin: -5px 0 20px 20px;
color: #e18f00;
}
h3 {
font-size: 20px;
margin: 20px 0 0 0;
color: #cd0000;
}
h2 a {
text-decoration: none;
}
a {
color: #cd0000;
}
a:hover {
color: #e18f00;
}
div.text {
max-width: 700px;
text-align: justify;
padding: 10px;
}
#navigation {
float: right;
list-style: none;
margin: -50px 0 0 0;
padding: 0;
}
#navigation li {
float: left;
}
#navigation li a {
display: block;
padding: 5px 10px 5px 10px;
background-color: #333;
text-decoration: none;
color: white;
font-size: 16px;
font-weight: bold;
}
#navigation li a:hover {
background-color: #f89e00;
}
#navigation li.active a {
background-color: #cd0000;
}
#paste {
margin: 0;
padding: 0;
border: 1px solid #333;
}
#paste table, #paste tbody {
margin: 0;
padding: 0;
border-collapse: collapse;
border-spacing: 0;
}
#paste td.linenos {
background-color: #333;
padding-right: 5px;
padding-left: 20px;
text-align: right;
color: #eee;
}
#paste td.code {
padding-left: 10px;
}
#paste pre {
margin: 0;
padding: 5px 0 5px 0;
font-family: 'Bitstream Vera Sans Mono', monospace;
font-size: 13px;
}
div.related,
div.notification {
margin: 0 0 10px 0;
border: 1px solid #cd0000;
background-color: #333;
color: #fff;
}
div.related h3,
div.notification h3 {
margin: 0;
padding: 0;
background-color: #cd0000;
font-size: 18px;
}
div.notification h3,
div.related h3 a {
display: block;
padding: 5px;
color: white;
text-decoration: none;
}
div.related h3 a:hover {
background-color: #c41200;
color: white;
}
div.related h3 a:focus {
outline: none;
}
div.related p,
div.notification p {
padding: 5px 10px 5px 10px;
margin: 0;
}
div.related a,
div.notification a {
color: #ccc;
}
div.related a:hover,
div.notification a:hover {
color: #aaa;
}
div.related ul {
margin: 0 0 10px 30px;
padding: 0 0 0 0;
}
div.related div.content {
display: none;
}
input, select, textarea {
border: 1px solid #333;
font-family: 'Trebuchet MS', sans-serif;
font-size: 15px;
color: black;
background-color: #f2f2f2;
}
textarea {
font-family: 'Bitstream Vera Sans Mono', monospace;
font-size: 13px;
width: 100%;
height: 300px;
margin: 0 0 10px 0;
}
input[type="submit"],
input[type="button"] {
background-color: #cd0000;
color: white;
cursor: pointer;
}
table.diff {
width: 100%;
border: 2px solid #ccc;
border-collapse: collapse;
empty-cells: show;
margin: 0;
}
table.diff th.old_rev,
table.diff th.new_rev {
width: 3em;
}
table.diff tr.line th.old_rev,
table.diff tr.line th.new_rev {
padding: 0.2em 0.5em 0.2em 0;
text-align: right;
font-weight: normal;
font-size: 0.8em;
background-color: #eee;
color: #444;
border: 1px solid #ccc;
vertical-align: top;
}
table.diff tr.ellipsis th {
border: 1px solid #ccc;
background-color: #dfdfdf;
}
table.diff tr.line td.code {
padding: 0.1em 0.4em 0.1em 0.4em;
font-family: monospace;
font-size: 13px;
border: 1px solid #ddd;
white-space: -moz-pre-wrap;
white-space: -o-pre-wrap;
white-space: -pre-wrap;
white-space: pre-wrap;
}
table.diff tr.add td.code {
background-color: #dfd;
}
table.diff tr.del td.code {
background-color: #fcc;
}
table.diff tr.add td.code ins {
background-color: #9e9;
text-decoration: none;
}
table.diff tr.del td.code del {
background-color: #e99;
text-decoration: none;
}
ul.paste_list {
margin: 0;
padding: 0;
list-style: none;
}
ul.paste_list p {
margin: 0;
padding: 0;
}
ul.paste_list pre {
margin: 4px 10px 4px 30px;
padding: 4px;
font-family: 'Bitstream Vera Sans Mono', monospace;
font-size: 13px;
}
ul.paste_list li {
margin: 10px 0 0 0;
padding: 5px 0 5px 10px;
}
ul.paste_list li.even {
background-color: #eee;
}
ul.paste_list li.odd {
background-color: #ddd;
}
div.pagination {
margin: 10px;
text-align: right;
}
ul.paste_tree {
margin: 0 0 0 20px;
padding: 0;
}
ul.paste_tree li {
padding: 0;
}
ul.paste_tree li.highlighted {
background-color: #eee;
}
ul.paste_tree li.highlighted ul {
background-color: white;
}
ul.paste_tree li.highlighted > a {
font-weight: bold;
}
ul.paste_tree ul {
padding: 0 0 0 24px;
margin: 0;
}
ul.paste_tree,
ul.paste_tree ul {
list-style: circle;
}

34
lodgeit/urls.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""
lodgeit.urls
~~~~~~~~~~~~
The URL mapping.
:copyright: 2007 by Armin Ronacher.
:license: BSD
"""
from wsgitk.routing import automap
@automap
def urlmap():
# paste interface
root > 'pastes/new_paste'
root / 'show' / int('paste_id') > 'pastes/show_paste'
root / 'compare' / int('new_id') / int('old_id') > 'pastes/compare_paste'
root / 'tree' / int('paste_id') > 'pastes/show_tree'
# paste list
root / 'all' > 'pastes/show_all'
root / 'all' / int('page') > 'pastes/show_all'
# xmlrpc
root / 'xmlrpc' > 'xmlrpc/handle_request'
# static pages
root / 'about' > 'static/about'
# redirect pages
root / 'compare' > 'pastes/compare_paste'
root / 'colorscheme' > 'pastes/set_colorscheme'

62
lodgeit/views/about.html Normal file
View File

@ -0,0 +1,62 @@
{% extends "layout.html" %}
{% set page_title = 'About LodgeIt' %}
{% set active_page = 'about' %}
{% block body %}
<div class="text">
<h3 id="why-the-hell-another-pastebin">Why the hell another pastebin?</h3>
<p>
Good question. Basically the world doesn't need another pastebin.
There is <a href="http://pastie.caboo.se/">pastie</a> and
<a href="http://dpaste.com/">dpaste.com</a> which
both use kick-ass highlighting libraries for highlighting the
code and both have an initiutive user interface. Nevertheless there
are some features which are unique to lodgeit.
</p>
<h3 id="features">Features</h3>
<ul>
<li>clean user interface</li>
<li>different color schemes for the sourcecode</li>
<li>reply to pastes</li>
<li>diffs of different pastes</li>
<li>support for many python template languages</li>
<li>support for many scripting languages like Python and Ruby, even with
weird syntax (ruby *cough*)</li>
<li><a href="/xmlrpc/">XMLRPC support</a></li>
<li>persistent pastes</li>
<li>reply notification</li>
<li>valid HTML 4.0</li>
</ul>
<h3 id="request-more-languages">Request More Languages</h3>
<p>
A language is missing in the list? File a ticket in the
<a href="http://trac.pocoo.org/">pocoo trac</a> and we add that as soon
as possible.
</p>
<h3 id="software-used">Software Used</h3>
<ul>
<li><a href="http://www.sqlite.org/">sqlite3</a> as database</li>
<li><a href="http://pygments.pocoo.org/">pygments</a> for syntax highlighting</li>
<li><a href="http://www.python.org/">python</a> as scripting language</li>
<li><a href="http://jinja.pocoo.org/">Jinja</a> for templating</li>
<li><a href="http://wsgitk.pocoo.org/">wsgitk</a> for the WSGI implementation</li>
<li><a href="http://www.sqlalchemy.org">SQLAlchemy</a> as database layer</li>
<li><a href="http://www.jquery.com/">jQuery</a> for scripting</li>
</ul>
<h3 id="who">Who?</h3>
<p>
<a href="http://lucumr.pocoo.org/">mitsuhiko</a> from the pocoo
team is responsible for the pastebin. Pygments is a pocoo project
led by Georg Brandl.
</p>
<h3 id="piracy">Piracy</h3>
<p>
LodgeIt does not use user accounts because it's logging in for using a
pastebin is useles. However this pastebin creates unique user ids for you
and for 31 days. Whenever you return to the pastebin it will notify you
about replies to your pastes. If you don't want to have that feature you
can let lodgeit forget about you by
<a href="javascript:LodgeIt.removeCookie()">removing the cookie</a>.
Please note that on the next request you will get a new id.
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "layout.html" %}
{% set page_title = 'Compare Pastes' %}
{% block body %}
<p>
Differences between the pastes
<a href="{{ old.url|e }}">#{{ old.paste_id }}</a> ({{ old.pub_date|datetimeformat }})
and <a href="{{ new.url|e }}">#{{ new.paste_id }}</a> ({{ new.pub_date|datetimeformat }}).
</p>
{% if diff.chunks %}
<table class="diff">
{%- for chunk in diff.chunks -%}
{% if not loop.first -%}
<tr class="ellipsis">
<th colspan="3">...</th>
</tr>
{%- endif -%}
{% for line in chunk %}
<tr class="line {{ line.action }}">
<th class="old_rev">{{ line.old_lineno }}</th>
<th class="new_rev">{{ line.new_lineno }}</th>
<td class="code">{{ line.line }}</td>
</tr>
{%- endfor -%}
{% endfor %}
</table>
{% else %}
<p>The two pastes are identical.</p>
{% endif %}
{% endblock %}

62
lodgeit/views/layout.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>{{ page_title|e }} | LodgeIt!</title>
<link rel="stylesheet" href="/static/style.css" type="text/css">
<script type="text/javascript" src="/static/jquery.js"></script>
<script type="text/javascript" src="/static/cookie.js"></script>
<script type="text/javascript" src="/static/lodgeit.js"></script>
{%- if css %}
<style type="text/css">
{{ css|e }}
</style>
{%- endif %}
</head>
<body>
<div class="page">
<div id="header"><h1>Lodge It</h1></div>
<ul id="navigation">
{%- for href, id, caption in [
('/', 'new', 'New'),
('/all/', 'all', 'All'),
('/about', 'about', 'About')
] %}
<li{% if active_page == id %} class="active"{%
endif %}><a href="{{ href|e }}">{{ caption|e }}</a></li>
{%- endfor %}
</ul>
<div class="content">
<h2>{{ page_title|e }}</h2>
{%- if new_replies %}
<div class="notification">
<h3>Someone Replied To Your Paste</h3>
{% for paste in new_replies %}
<p>
on {{ paste.pub_date|datetimeformat }} someone replied to
your paste <a href="{{ paste.parent.url|e }}">#{{ paste.parent.paste_id }}</a>,
in paste <a href="{{ paste.url|e }}">#{{ paste.paste_id }}</a>. Click here to
<a href="/compare/{{ paste.paste_id }}/{{ paste.parent.paste_id }}">compare
those two pastes</a>.
</p>
{% endfor %}
<p><a href="javascript:LodgeIt.hideNotification()">hide this notification</a></p>
</div>
{% elif request.first_visit %}
<div class="notification">
<h3>Welcome On LodgeIt</h3>
<p>
Welcome on the LodgeIt pastebin. In order to use the notification feature
a 31 day cookie with an unique ID was created for you. The lodgeit database
does not store any information about you, it's just used for an advanced
pastebin experience :-). Read more on the <a href="/about#piracy">about
lodgeit</a> page. Have fun :-)
</p>
<p><a href="javascript:LodgeIt.hideNotification()">hide this notification</a></p>
</div>
{% endif -%}
{% block body %}{% endblock -%}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% set page_title = 'New Paste' %}
{% set active_page = 'new' %}
{% block body %}
<form action="/" method="post" class="submitform">
{% if parent %}
<input type="hidden" name="parent" value="{{ parent.paste_id }}">
{% endif %}
<textarea name="code" rows="10" cols="80">{{ parent.code|e }}</textarea>
<select name="language">
{% for key, caption in languages|dictsort(true, 'value') -%}
<option value="{{ key }}"{% if parent.language == key
%} selected="selected"{% endif %}>{{ caption|e }}</option>
{% endfor %}
</select>
<input type="submit" value="Paste!">
<input type="button" value="▲" onclick="LodgeIt.resizeTextarea(-100)">
<input type="button" value="▼" onclick="LodgeIt.resizeTextarea(100)">
</form>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% set page_title = 'Page Not Found' %}
{% block body %}
<p>
Sorry, but the page you requested was not found on this server.
</p>
<p>
We've recently updated this pastebin. While it is out goal for nothing to get
lost, you may have found a page that was mis-placed. Check your URL to ensure
you have gone where you intended. If everything looks OK and you still see
this error page, please consider <a href="/about">conacting us</a>.
</p>
<p>
Click <a href="/">here</a> to add a new paste.
</p>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "layout.html" %}
{% set page_title = 'Paste Tree' %}
{% set active_page = 'all' %}
{% block body %}
<p>
Here you can see the requested tree of paste replies. The paste you're
coming from is highlighted.
</p>
<ul class="paste_tree">
{%- for paste in [paste] recursive %}
<li{% if paste.paste_id == current
%} class="highlighted"{% endif %}><a href="{{ paste.url|e
}}">Paste #{{ paste.paste_id }}</a> &mdash; {{
paste.pub_date|datetimeformat }}
{%- if paste.children -%}
<ul>{{ loop(paste.children) }}</ul>
{%- endif -%}
</li>
{%- endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% set page_title = 'All Pastes' %}
{% set active_page = 'all' %}
{% block body %}
<ul class="paste_list">
{% for paste in pastes %}
<li class="{% cycle 'even', 'odd' %}"><p><a href="{{ paste.url|e
}}">Paste #{{ paste.paste_id }}</a>,
pasted on {{ paste.pub_date|datetimeformat }}</p>
{{ paste.render_preview() }}</li>
{%- endfor %}
</ul>
<div class="pagination">
{{ pagination }}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "layout.html" %}
{% set page_title = 'Paste #%d'|format(paste.paste_id) %}
{% set active_page = 'all' %}
{% block body %}
<div class="related">
<h3><a href="javascript:LodgeIt.toggleRelatedBox()">Paste Details</a></h3>
<div class="content">
<p>posted on {{ paste.pub_date|datetimeformat }}</p>
<ul>
<li><a class="autoclose" href="/?reply_to={{ paste.paste_id }}">reply to this paste</a></li>
{% if paste.parent %}
<li><a class="autoclose" href="/compare/{{ paste.paste_id }}/{{
paste.parent.paste_id }}">compare it with the parent paste</a></li>
<li><a class="autoclose" href="{{ paste.parent.url|e }}">look at the parent paste</a></li>
{% endif %}
{% if paste.children %}
<li>the following pastes replied to this paste:
{% for child in paste.children %}
<a class="autoclose" href="{{ child.url|e }}">#{{ child.paste_id }}</a>
{%- if not loop.last %},{% endif -%}
{% endfor %}
</li>
{% endif %}
{% if paste.parent or paste.children %}
<li><a href="/tree/{{ paste.paste_id }}">show paste tree</a></li>
{% endif %}
<li>compare with paste <form action="/compare" method="post">
<input type="hidden" name="old" value="{{ paste.paste_id }}">
<input type="text" name="new" value="#">
<input type="submit" value="compare">
</form></li>
<li>select different colorscheme <form action="/colorscheme" method="post">
<select name="style">
{% for key, caption in styles|dictsort %}
<option value="{{ key }}"{% if key == style
%} selected="selected"{% endif %}>{{ caption }}</option>
{% endfor %}
</select>
<input type="submit" value="change">
</form></li>
<li><a href="javascript:LodgeIt.toggleLineNumbers()">toggle line numbers</a></li>
</ul>
</div>
</div>
<div id="paste">
{{ paste.parsed_code }}
</div>
{% endblock %}

25
lodgeit/views/xmlrpc.html Normal file
View File

@ -0,0 +1,25 @@
{% extends "layout.html" %}
{% set page_title = "XMLRPC" %}
{% set active_page = 'about' %}
{% block body %}
<h3>Connecting To The XMLRPC Interface</h3>
<p>
The XMLRPC Interface is available at
<tt>{{ interface_url|escape }}</tt>
</p>
<p>
From python you can connect to it using the following
sourcecode:
</p>
<pre>from xmlrpclib import ServerProxy
s = ServerProxy('{{ interface_url|escape }}')
s.pastes.method('parameter')</pre>
<h3>Public Methods</h3>
<ul>
{% for method in methods %}
<li><strong>{{ method.name|escape }}</strong>
<em>{{ method.signature|escape }}</em>
<p>{{ method.doc|e }}</p></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -190,7 +190,7 @@ def download_paste(uid):
paste = xmlrpc.pastes.getPaste(uid) paste = xmlrpc.pastes.getPaste(uid)
if not paste: if not paste:
fail('Paste "%s" does not exist' % uid, 5) fail('Paste "%s" does not exist' % uid, 5)
print paste['code'] print paste['code'].encode('utf-8')
def create_paste(code, title, author, language, private, tags): def create_paste(code, title, author, language, private, tags):