Implemented private pastes and improved some code. Added TODO file.

This commit is contained in:
mitsuhiko 2008-06-25 01:06:42 +02:00
parent dffba5bfbf
commit 790402b5df
11 changed files with 172 additions and 91 deletions

4
TODO Normal file
View File

@ -0,0 +1,4 @@
* Put all cookie stuff into *one* securecookie
* Make it possible to tag and find pastes
* Add a button to find all the personal (and private) pastes
(cookie bound)

View File

@ -17,7 +17,7 @@ from werkzeug import SharedDataMiddleware, ClosingIterator
from werkzeug.exceptions import HTTPException, NotFound
from lodgeit.utils import _local_manager, ctx, jinja_environment, \
Request, generate_user_hash
Request
from lodgeit.database import metadata, session, Paste
from lodgeit.urls import urlmap
from lodgeit.controllers import get_controller

View File

@ -27,18 +27,18 @@ class PasteController(BaseController):
"""The 'create a new paste' view."""
code = error = ''
language = 'text'
pastes = session.query(Paste)
show_captcha = False
show_captcha = private = False
parent = None
getform = ctx.request.form.get
if ctx.request.method == 'POST':
code = getform('code')
language = getform('language')
try:
parent = pastes.filter(Paste.paste_id ==
int(getform('parent'))).first()
except (ValueError, TypeError):
parent = None
parent_id = getform('parent')
if parent_id is not None:
parent = Paste.get(parent_id)
spam = ctx.request.form.get('webpage') or antispam.is_spam(code)
if spam:
error = 'your paste contains spam'
@ -51,16 +51,19 @@ class PasteController(BaseController):
show_captcha = True
if code and language and not error:
paste = Paste(code, language, parent, ctx.request.user_hash)
session.save(paste)
if 'private' in ctx.request.form:
paste.private = True
session.flush()
return redirect(paste.url)
else:
parent = ctx.request.args.get('reply_to')
if parent is not None and parent.isdigit():
parent = pastes.filter(Paste.paste_id == parent).first()
code = parent.code
language = parent.language
parent_id = ctx.request.args.get('reply_to')
if parent_id is not None:
parent = Paste.get(parent_id)
if parent is not None:
code = parent.code
language = parent.language
private = parent.private
return render_template('new_paste.html',
languages=LANGUAGES,
@ -68,14 +71,14 @@ class PasteController(BaseController):
code=code,
language=language,
error=error,
show_captcha=show_captcha
show_captcha=show_captcha,
private=private
)
def show_paste(self, paste_id, raw=False):
def show_paste(self, identifier, raw=False):
"""Show an existing paste."""
linenos = ctx.request.args.get('linenos') != 'no'
pastes = session.query(Paste)
paste = pastes.filter(Paste.c.paste_id == paste_id).first()
paste = Paste.get(identifier)
if paste is None:
raise NotFound()
if raw:
@ -90,18 +93,18 @@ class PasteController(BaseController):
linenos=linenos,
)
def raw_paste(self, paste_id):
def raw_paste(self, identifier):
"""Show an existing paste in raw mode."""
return self.show_paste(paste_id, raw=True)
return self.show_paste(identifier, raw=True)
def show_tree(self, paste_id):
def show_tree(self, identifier):
"""Display the tree of some related pastes."""
paste = Paste.resolve_root(paste_id)
paste = Paste.resolve_root(identifier)
if paste is None:
raise NotFound()
return render_template('paste_tree.html',
paste=paste,
current=paste_id
current=identifier
)
def show_all(self, page=1):
@ -112,31 +115,30 @@ class PasteController(BaseController):
return '/all/'
return '/all/%d' % page
pastes = session.query(Paste).order_by(
Paste.c.pub_date.desc()
).limit(10).offset(10*(page-1)).all()
all = Paste.find_all()
pastes = all.limit(10).offset(10 * (page -1)).all()
if not pastes and page != 1:
raise NotFound()
return render_template('show_all.html',
pastes=pastes,
pagination=generate_pagination(page, 10,
Paste.count(), link),
pagination=generate_pagination(page, 10, all.count(), link),
css=get_style(ctx.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:
if old_id is None:
old_id = ctx.request.form.get('old', '-1').lstrip('#')
new_id = ctx.request.form.get('new', '-1').lstrip('#')
return redirect('/compare/%s/%s' % (old_id, new_id))
pastes = session.query(Paste)
old = pastes.filter(Paste.c.paste_id == old_id).first()
new = pastes.filter(Paste.c.paste_id == new_id).first()
old = Paste.get(old_id)
new = Paste.get(new_id)
if old is None or new is None:
raise NotFound()
return render_template('compare_paste.html',
old=old,
new=new,
@ -145,11 +147,12 @@ class PasteController(BaseController):
def unidiff_paste(self, new_id=None, old_id=None):
"""Render an udiff for the two pastes."""
pastes = session.query(Paste)
old = pastes.filter(Paste.c.paste_id == old_id).first()
new = pastes.filter(Paste.c.paste_id == new_id).first()
old = Paste.get(old_id)
new = Paste.get(new_id)
if old is None or new is None:
raise NotFound()
return Response(old.compare_to(new), mimetype='text/plain')
def set_colorscheme(self):

View File

@ -34,10 +34,15 @@ def pastes_new_paste(language, code, parent_id=None,
"""
if not language:
language = get_language_for(filename or '', mimetype or '')
paste = Paste(code, language, parent_id)
session.save(paste)
parent = None
if parent_id:
parent = Paste.get(parent_id)
if parent is None:
raise ValueError('parent paste not found')
paste = Paste(code, language, parent)
session.flush()
return paste.paste_id
return paste.identifier
@exported('pastes.getPaste')
@ -48,8 +53,7 @@ def pastes_get_paste(paste_id):
`paste_id`, `code`, `parsed_code`, `pub_date`, `language`,
`parent_id`, `url`.
"""
paste = session.query(Paste).filter(Paste.c.paste_id ==
paste_id).first()
paste = Paste.get(paste_id)
if paste is None:
return False
return paste.to_xmlrpc_dict()
@ -58,11 +62,10 @@ def pastes_get_paste(paste_id):
@exported('pastes.getDiff')
def pastes_get_diff(old_id, new_id):
"""Compare the two pastes and return an unified diff."""
pastes = session.query(Paste)
old = pastes.filter(Paste.c.paste_id == old_id).first()
new = pastes.filter(Paste.c.paste_id == new_id).first()
old = Paste.get(old_id)
new = Paste.get(new_id)
if old is None or new is None:
return False
raise ValueError('argument error, paste not found')
return old.compare_to(new)
@ -72,10 +75,7 @@ def pastes_get_recent(amount=5):
`amount` pastes.
"""
amount = min(amount, 20)
return [x.to_xmlrpc_dict() for x in
session.query(Paste).order_by(
Paste.pub_date.desc()
).limit(amount)]
return [x.to_xmlrpc_dict() for x in Paste.find_all().limit(amount)]
@exported('pastes.getLast')

View File

@ -19,7 +19,7 @@ from sqlalchemy.orm import create_session, mapper, backref, relation
from sqlalchemy.orm.scoping import ScopedSession
from werkzeug import cached_property
from lodgeit.utils import _local_manager, ctx
from lodgeit.utils import _local_manager, ctx, generate_paste_hash
from lodgeit.lib.highlighting import highlight, LANGUAGES
@ -29,9 +29,10 @@ metadata = MetaData()
pastes = Table('pastes', metadata,
Column('paste_id', Integer, primary_key=True),
Column('private_id', String(40), unique=True, nullable=True),
Column('code', Text),
Column('parent_id', Integer, ForeignKey('pastes.paste_id'),
nullable=True),
nullable=True),
Column('pub_date', DateTime),
Column('language', String(30)),
Column('user_hash', String(40), nullable=True),
@ -40,6 +41,7 @@ pastes = Table('pastes', metadata,
class Paste(object):
"""Represents a paste."""
def __init__(self, code, language, parent=None, user_hash=None):
if language not in LANGUAGES:
@ -54,16 +56,83 @@ class Paste(object):
self.handled = False
self.user_hash = user_hash
@staticmethod
def get(identifier):
"""Return the paste for an identifier. Private pastes must be loaded
with their unique hash and public with the paste id.
"""
if isinstance(identifier, basestring) and not identifier.isdigit():
return Paste.query.filter(Paste.private_id == identifier).first()
return Paste.query.filter(
(Paste.paste_id == int(identifier)) &
(Paste.private_id == None)
).first()
@staticmethod
def find_all():
"""Return a query for all public pastes ordered by the publication
date in reverse order.
"""
return Paste.query.filter(Paste.private_id == None) \
.order_by(Paste.pub_date.desc())
@staticmethod
def count():
"""COunt all pastes."""
s = select([func.count(pastes.c.paste_id)])
return session.execute(s).fetchone()[0]
@staticmethod
def resolve_root(identifier):
"""Find the root paste for a paste tree."""
paste = Paste.get(identifier)
if paste is None:
return
while paste.parent_id is not None:
paste = paste.parent
return paste
def _get_private(self):
return self.private_id is not None
def _set_private(self, value):
if not value:
self.private_id = None
return
if self.private_id is None:
while 1:
self.private_id = generate_paste_hash()
paste = Paste.query.filter(Paste.private_id ==
self.private_id).first()
if paste is None:
break
private = property(_get_private, _set_private, doc='''
The private status of the paste. If the paste is private it gets
a unique hash as identifier, otherwise an integer.
''')
del _get_private, _set_private
@property
def identifier(self):
"""The paste identifier. This is a string, the same the `get`
method accepts.
"""
if self.private:
return self.private_id
return str(self.paste_id)
@property
def url(self):
return '/show/%d/' % self.paste_id
"""The URL to the paste."""
return '/show/%s/' % self.identifier
def compare_to(self, other, context_lines=4, template=False):
"""Compare the paste with another paste."""
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,
fromfile='Paste #%s' % self.identifier,
tofile='Paste #%s' % other.identifier,
lineterm='',
n=context_lines
))
@ -75,9 +144,11 @@ class Paste(object):
@cached_property
def parsed_code(self):
"""The paste as rendered code."""
return highlight(self.code, self.language)
def to_xmlrpc_dict(self):
"""Convert the paste into a dict for XMLRCP."""
from lodgeit.lib.xmlrpc import strip_control_chars
return {
'paste_id': self.paste_id,
@ -90,6 +161,7 @@ class Paste(object):
}
def render_preview(self):
"""Render a preview for this paste."""
try:
start = self.parsed_code.index('</pre>')
code = self.parsed_code[
@ -110,7 +182,7 @@ class Paste(object):
Paste.user_hash == ctx.request.user_hash
)
paste_list = session.query(Paste).filter(and_(
paste_list = Paste.query.filter(and_(
Paste.parent_id.in_(s),
Paste.handled == False,
Paste.user_hash != ctx.request.user_hash,
@ -121,24 +193,8 @@ class Paste(object):
values={'handled': True}))
return paste_list
@staticmethod
def count():
s = select([func.count(pastes.c.paste_id)])
return session.execute(s).fetchone()[0]
@staticmethod
def resolve_root(paste_id):
pastes = session.query(Paste)
while True:
paste = pastes.filter(Paste.c.paste_id == paste_id).first()
if paste is None:
return
if paste.parent_id is None:
return paste
paste_id = paste.parent_id
mapper(Paste, pastes, properties={
session.mapper(Paste, pastes, properties={
'children': relation(Paste,
primaryjoin=pastes.c.parent_id==pastes.c.paste_id,
cascade='all',

View File

@ -5,7 +5,7 @@
The URL mapping.
:copyright: 2007 by Armin Ronacher.
:copyright: 2007-2008 by Armin Ronacher.
:license: BSD
"""
from werkzeug.routing import Map, Rule
@ -13,12 +13,12 @@ from werkzeug.routing import Map, Rule
urlmap = Map([
# paste interface
Rule('/', endpoint='pastes/new_paste'),
Rule('/show/<int:paste_id>/', endpoint='pastes/show_paste'),
Rule('/raw/<int:paste_id>/', endpoint='pastes/raw_paste'),
Rule('/show/<identifier>/', endpoint='pastes/show_paste'),
Rule('/raw/<identifier>/', endpoint='pastes/raw_paste'),
Rule('/compare/', endpoint='pastes/compare_paste'),
Rule('/compare/<int:new_id>/<int:old_id>/', endpoint='pastes/compare_paste'),
Rule('/unidiff/<int:new_id>/<int:old_id>/', endpoint='pastes/unidiff_paste'),
Rule('/tree/<int:paste_id>/', endpoint='pastes/show_tree'),
Rule('/compare/<new_id>/<old_id>/', endpoint='pastes/compare_paste'),
Rule('/unidiff/<new_id>/<old_id>/', endpoint='pastes/unidiff_paste'),
Rule('/tree/<identifier>/', endpoint='pastes/show_tree'),
# captcha for new paste
Rule('/_captcha.png', endpoint='pastes/show_captcha'),

View File

@ -8,6 +8,7 @@
:copyright: 2007 by Christopher Grebs.
:license: BSD
"""
import re
import time
from os import path
try:
@ -15,6 +16,7 @@ try:
except:
from sha import new as sha1
from random import random
from functools import partial
from werkzeug import Local, LocalManager, LocalProxy, \
Request as RequestBase, Response
@ -30,9 +32,12 @@ jinja_environment = Environment(loader=FileSystemLoader(
path.join(path.dirname(__file__), 'views')))
_word_only = partial(re.compile(r'[^a-zA-Z0-9]').sub, '')
def datetimeformat(obj):
"""Helper filter for the template"""
return obj.strftime('%Y-%m-%d %H:%M')
return obj.strftime('%Y-%m-%d @ %H:%M')
jinja_environment.filters['datetimeformat'] = datetimeformat
@ -42,6 +47,16 @@ def generate_user_hash():
return sha1('%s|%s' % (random(), time.time())).hexdigest()
def generate_paste_hash():
"""Generates a more or less unique-truncated SHA1 hash."""
while 1:
digest = sha1('%s|%s' % (random(), time.time())).digest()
val = _word_only(digest.encode('base64').strip().splitlines()[0])[:20]
# sanity check. number only not allowed (though unlikely)
if not val.isdigit():
return val
class Request(RequestBase):
"""Subclass of the `Request` object. automatically creates a new
`user_hash` and sets `first_visit` to `True` if it's a new user.

View File

@ -4,8 +4,8 @@
{% 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 }}). Download as <a href="/unidiff/{{ old.paste_id }}/{{ new.paste_id }}/">unified diff</a>.
<a href="{{ old.url|e }}">#{{ old.identifier }}</a> ({{ old.pub_date|datetimeformat }})
and <a href="{{ new.url|e }}">#{{ new.identifier }}</a> ({{ new.pub_date|datetimeformat }}). Download as <a href="/unidiff/{{ old.identifier }}/{{ new.identifier }}/">unified diff</a>.
</p>
{% if diff.chunks %}
<table class="diff">

View File

@ -10,7 +10,8 @@
{%- if show_captcha %}
<div class="captcha">
<p>Please fill out the CAPTCHA to proceed:</p>
<img src="/_captcha.png" alt="a captcha you can't see. Sorry :(">
<img src="{{ url_for('pastes/captcha')
}}" alt="a captcha you can't see. Sorry :(">
<input type="text" name="captcha" size="20">
</div>
{%- else %}
@ -19,7 +20,7 @@
</div>
{%- endif %}
{%- if parent %}
<input type="hidden" name="parent" value="{{ parent.paste_id }}">
<input type="hidden" name="parent" value="{{ parent.identifier }}">
{%- endif %}
<textarea name="code" rows="10" cols="80">{{ code|e }}</textarea>
<select name="language">
@ -32,5 +33,7 @@
<input type="submit" value="Paste!">
<input type="button" value="▲" onclick="LodgeIt.resizeTextarea(-100)">
<input type="button" value="▼" onclick="LodgeIt.resizeTextarea(100)">
<input type="checkbox" name="private" id="private"{% if private
%} checked{% endif %}><label for="private">make this paste private</label>
</form>
{% endblock %}

View File

@ -5,7 +5,7 @@
<ul class="paste_list">
{% for paste in pastes %}
<li class="{{ loop.cycle('even', 'odd') }}"><p><a href="{{ paste.url|e
}}">Paste #{{ paste.paste_id }}</a>,
}}">Paste #{{ paste.identifier }}</a>,
pasted on {{ paste.pub_date|datetimeformat }}</p>
{{ paste.render_preview() }}</li>
{%- endfor %}

View File

@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% set page_title = 'Paste #%d'|format(paste.paste_id) %}
{% set page_title = 'Paste #' ~ paste.identifier %}
{% set active_page = 'all' %}
{% block body %}
<div class="related">
@ -7,26 +7,26 @@
<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>
<li><a class="autoclose" href="/?reply_to={{ paste.identifier }}">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="/compare/{{ paste.identifier }}/{{
paste.parent.identifier }}/">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>
<a class="autoclose" href="{{ child.url|e }}">#{{ child.identifier }}</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>
<li><a href="/tree/{{ paste.identifier }}/">show paste tree</a></li>
{% endif %}
<li><a href="/raw/{{ paste.paste_id }}/">download paste</a></li>
<li><a href="/raw/{{ paste.identifier }}/">download paste</a></li>
<li>compare with paste <form action="/compare/" method="post">
<input type="hidden" name="old" value="{{ paste.paste_id }}">
<input type="hidden" name="old" value="{{ paste.identifier }}">
<input type="text" name="new" value="#">
<input type="submit" value="compare">
</form></li>