diff --git a/pastebin/__init__.py b/django_version/pastebin/__init__.py similarity index 100% rename from pastebin/__init__.py rename to django_version/pastebin/__init__.py diff --git a/pastebin/manage.py b/django_version/pastebin/manage.py similarity index 100% rename from pastebin/manage.py rename to django_version/pastebin/manage.py diff --git a/pastebin/media/css/style.css b/django_version/pastebin/media/css/style.css similarity index 100% rename from pastebin/media/css/style.css rename to django_version/pastebin/media/css/style.css diff --git a/pastebin/media/js/builder.js b/django_version/pastebin/media/js/builder.js similarity index 100% rename from pastebin/media/js/builder.js rename to django_version/pastebin/media/js/builder.js diff --git a/pastebin/media/js/controls.js b/django_version/pastebin/media/js/controls.js similarity index 100% rename from pastebin/media/js/controls.js rename to django_version/pastebin/media/js/controls.js diff --git a/pastebin/media/js/dragdrop.js b/django_version/pastebin/media/js/dragdrop.js similarity index 100% rename from pastebin/media/js/dragdrop.js rename to django_version/pastebin/media/js/dragdrop.js diff --git a/pastebin/media/js/effects.js b/django_version/pastebin/media/js/effects.js similarity index 100% rename from pastebin/media/js/effects.js rename to django_version/pastebin/media/js/effects.js diff --git a/pastebin/media/js/pastebin.js b/django_version/pastebin/media/js/pastebin.js similarity index 100% rename from pastebin/media/js/pastebin.js rename to django_version/pastebin/media/js/pastebin.js diff --git a/pastebin/media/js/prototype.js b/django_version/pastebin/media/js/prototype.js similarity index 100% rename from pastebin/media/js/prototype.js rename to django_version/pastebin/media/js/prototype.js diff --git a/pastebin/media/js/scriptaculous.js b/django_version/pastebin/media/js/scriptaculous.js similarity index 100% rename from pastebin/media/js/scriptaculous.js rename to django_version/pastebin/media/js/scriptaculous.js diff --git a/pastebin/media/js/slider.js b/django_version/pastebin/media/js/slider.js similarity index 100% rename from pastebin/media/js/slider.js rename to django_version/pastebin/media/js/slider.js diff --git a/pastebin/pastes/__init__.py b/django_version/pastebin/pastes/__init__.py similarity index 100% rename from pastebin/pastes/__init__.py rename to django_version/pastebin/pastes/__init__.py diff --git a/pastebin/pastes/models.py b/django_version/pastebin/pastes/models.py similarity index 100% rename from pastebin/pastes/models.py rename to django_version/pastebin/pastes/models.py diff --git a/pastebin/pastes/views.py b/django_version/pastebin/pastes/views.py similarity index 100% rename from pastebin/pastes/views.py rename to django_version/pastebin/pastes/views.py diff --git a/pastebin/settings.py b/django_version/pastebin/settings.py similarity index 100% rename from pastebin/settings.py rename to django_version/pastebin/settings.py diff --git a/pastebin/static/__init__.py b/django_version/pastebin/static/__init__.py similarity index 100% rename from pastebin/static/__init__.py rename to django_version/pastebin/static/__init__.py diff --git a/pastebin/static/views.py b/django_version/pastebin/static/views.py similarity index 100% rename from pastebin/static/views.py rename to django_version/pastebin/static/views.py diff --git a/pastebin/templates/layout.html b/django_version/pastebin/templates/layout.html similarity index 100% rename from pastebin/templates/layout.html rename to django_version/pastebin/templates/layout.html diff --git a/pastebin/templates/pastes/_autocomplete.html b/django_version/pastebin/templates/pastes/_autocomplete.html similarity index 100% rename from pastebin/templates/pastes/_autocomplete.html rename to django_version/pastebin/templates/pastes/_autocomplete.html diff --git a/pastebin/templates/pastes/_find_thread.html b/django_version/pastebin/templates/pastes/_find_thread.html similarity index 100% rename from pastebin/templates/pastes/_find_thread.html rename to django_version/pastebin/templates/pastes/_find_thread.html diff --git a/pastebin/templates/pastes/all_pastes.html b/django_version/pastebin/templates/pastes/all_pastes.html similarity index 100% rename from pastebin/templates/pastes/all_pastes.html rename to django_version/pastebin/templates/pastes/all_pastes.html diff --git a/pastebin/templates/pastes/compare.html b/django_version/pastebin/templates/pastes/compare.html similarity index 100% rename from pastebin/templates/pastes/compare.html rename to django_version/pastebin/templates/pastes/compare.html diff --git a/pastebin/templates/pastes/new.html b/django_version/pastebin/templates/pastes/new.html similarity index 100% rename from pastebin/templates/pastes/new.html rename to django_version/pastebin/templates/pastes/new.html diff --git a/pastebin/templates/pastes/recent_pastes.html b/django_version/pastebin/templates/pastes/recent_pastes.html similarity index 100% rename from pastebin/templates/pastes/recent_pastes.html rename to django_version/pastebin/templates/pastes/recent_pastes.html diff --git a/pastebin/templates/pastes/show.html b/django_version/pastebin/templates/pastes/show.html similarity index 100% rename from pastebin/templates/pastes/show.html rename to django_version/pastebin/templates/pastes/show.html diff --git a/pastebin/templates/pastes/tagcloud.html b/django_version/pastebin/templates/pastes/tagcloud.html similarity index 100% rename from pastebin/templates/pastes/tagcloud.html rename to django_version/pastebin/templates/pastes/tagcloud.html diff --git a/pastebin/templates/pastes/taglist.html b/django_version/pastebin/templates/pastes/taglist.html similarity index 100% rename from pastebin/templates/pastes/taglist.html rename to django_version/pastebin/templates/pastes/taglist.html diff --git a/pastebin/templates/static/about.html b/django_version/pastebin/templates/static/about.html similarity index 100% rename from pastebin/templates/static/about.html rename to django_version/pastebin/templates/static/about.html diff --git a/pastebin/templates/static/error404.html b/django_version/pastebin/templates/static/error404.html similarity index 100% rename from pastebin/templates/static/error404.html rename to django_version/pastebin/templates/static/error404.html diff --git a/pastebin/templates/static/error500.html b/django_version/pastebin/templates/static/error500.html similarity index 100% rename from pastebin/templates/static/error500.html rename to django_version/pastebin/templates/static/error500.html diff --git a/pastebin/templates/static/help.html b/django_version/pastebin/templates/static/help.html similarity index 100% rename from pastebin/templates/static/help.html rename to django_version/pastebin/templates/static/help.html diff --git a/pastebin/templates/xmlrpc/index.html b/django_version/pastebin/templates/xmlrpc/index.html similarity index 100% rename from pastebin/templates/xmlrpc/index.html rename to django_version/pastebin/templates/xmlrpc/index.html diff --git a/pastebin/urls.py b/django_version/pastebin/urls.py similarity index 100% rename from pastebin/urls.py rename to django_version/pastebin/urls.py diff --git a/pastebin/utils.py b/django_version/pastebin/utils.py similarity index 100% rename from pastebin/utils.py rename to django_version/pastebin/utils.py diff --git a/pastebin/xmlrpc/__init__.py b/django_version/pastebin/xmlrpc/__init__.py similarity index 100% rename from pastebin/xmlrpc/__init__.py rename to django_version/pastebin/xmlrpc/__init__.py diff --git a/pastebin/xmlrpc/views.py b/django_version/pastebin/xmlrpc/views.py similarity index 100% rename from pastebin/xmlrpc/views.py rename to django_version/pastebin/xmlrpc/views.py diff --git a/lodgeit/__init__.py b/lodgeit/__init__.py new file mode 100644 index 0000000..edcd1eb --- /dev/null +++ b/lodgeit/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" + lodgeit + ~~~~~~~ + + The lodgeit pastebin. + + :copyright: 2007 by Armin Ronacher. + :license: BSD +""" diff --git a/lodgeit/application.py b/lodgeit/application.py new file mode 100644 index 0000000..8f922b3 --- /dev/null +++ b/lodgeit/application.py @@ -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 diff --git a/lodgeit/controllers/__init__.py b/lodgeit/controllers/__init__.py new file mode 100644 index 0000000..d45c77e --- /dev/null +++ b/lodgeit/controllers/__init__.py @@ -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) diff --git a/lodgeit/controllers/pastes.py b/lodgeit/controllers/pastes.py new file mode 100644 index 0000000..473c186 --- /dev/null +++ b/lodgeit/controllers/pastes.py @@ -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 diff --git a/lodgeit/controllers/static.py b/lodgeit/controllers/static.py new file mode 100644 index 0000000..3d2aef9 --- /dev/null +++ b/lodgeit/controllers/static.py @@ -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 diff --git a/lodgeit/controllers/xmlrpc.py b/lodgeit/controllers/xmlrpc.py new file mode 100644 index 0000000..f54facf --- /dev/null +++ b/lodgeit/controllers/xmlrpc.py @@ -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 diff --git a/lodgeit/database.py b/lodgeit/database.py new file mode 100644 index 0000000..a49f4db --- /dev/null +++ b/lodgeit/database.py @@ -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('') + code = self.parsed_code[ + self.parsed_code.index('
', start) + 5: + self.parsed_code.rindex('') + ].strip('\n').splitlines() + except IndexError: + code = ''.strip('\n').splitlines() + code = '\n'.join(code[:5] + ['...']) + return '
%s' % 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]) + ) +}) diff --git a/lodgeit/lib/__init__.py b/lodgeit/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lodgeit/lib/diff.py b/lodgeit/lib/diff.py new file mode 100644 index 0000000..5e650b6 --- /dev/null +++ b/lodgeit/lib/diff.py @@ -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() diff --git a/lodgeit/lib/highlighting.py b/lodgeit/lib/highlighting.py new file mode 100644 index 0000000..a857edd --- /dev/null +++ b/lodgeit/lib/highlighting.py @@ -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') diff --git a/lodgeit/lib/pagination.py b/lodgeit/lib/pagination.py new file mode 100644 index 0000000..ba14d56 --- /dev/null +++ b/lodgeit/lib/pagination.py @@ -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='%(page)d', + active='%(page)d', + 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' Next »' % + link_builder(next)) + elif gray_next_link: + result.append(u' Next »') + if prev_link: + if prev is not None: + result.insert(0, u'« Prev ' % + link_builder(prev)) + elif gray_prev_link: + result.insert(0, u'« Prev ') + + return u''.join(result) diff --git a/lodgeit/lib/xmlrpc.py b/lodgeit/lib/xmlrpc.py new file mode 100644 index 0000000..c2bfa1d --- /dev/null +++ b/lodgeit/lib/xmlrpc.py @@ -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 diff --git a/lodgeit/static/cookie.js b/lodgeit/static/cookie.js new file mode 100644 index 0000000..dee2f93 --- /dev/null +++ b/lodgeit/static/cookie.js @@ -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; + } +}; diff --git a/lodgeit/static/jquery.js b/lodgeit/static/jquery.js new file mode 100644 index 0000000..5273931 --- /dev/null +++ b/lodgeit/static/jquery.js @@ -0,0 +1,2245 @@ +/* prevent execution of jQuery if included more than once */ +if(typeof window.jQuery == "undefined") { +/* + * jQuery 1.1.2 - New Wave Javascript + * + * Copyright (c) 2007 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2007-02-28 12:03:00 -0500 (Wed, 28 Feb 2007) $ + * $Rev: 1465 $ + */ + +// Global undefined variable +window.undefined = window.undefined; +var jQuery = function(a,c) { + // If the context is global, return a new object + if ( window == this ) + return new jQuery(a,c); + + // Make sure that a selection was provided + a = a || document; + + // HANDLE: $(function) + // Shortcut for document ready + if ( jQuery.isFunction(a) ) + return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a ); + + // Handle HTML strings + if ( typeof a == "string" ) { + // HANDLE: $(html) -> $(array) + var m = /^[^<]*(<(.|\s)+>)[^>]*$/.exec(a); + if ( m ) + a = jQuery.clean( [ m[1] ] ); + + // HANDLE: $(expr) + else + return new jQuery( c ).find( a ); + } + + return this.setArray( + // HANDLE: $(array) + a.constructor == Array && a || + + // HANDLE: $(arraylike) + // Watch for when an array-like object is passed as the selector + (a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) || + + // HANDLE: $(*) + [ a ] ); +}; + +// Map over the $ in case of overwrite +if ( typeof $ != "undefined" ) + jQuery._$ = $; + +// Map the jQuery namespace to the '$' one +var $ = jQuery; + +jQuery.fn = jQuery.prototype = { + jquery: "1.1.2", + + size: function() { + return this.length; + }, + + length: 0, + + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[num]; + }, + pushStack: function( a ) { + var ret = jQuery(a); + ret.prevObject = this; + return ret; + }, + setArray: function( a ) { + this.length = 0; + [].push.apply( this, a ); + return this; + }, + each: function( fn, args ) { + return jQuery.each( this, fn, args ); + }, + index: function( obj ) { + var pos = -1; + this.each(function(i){ + if ( this == obj ) pos = i; + }); + return pos; + }, + + attr: function( key, value, type ) { + var obj = key; + + // Look for the case where we're accessing a style value + if ( key.constructor == String ) + if ( value == undefined ) + return this.length && jQuery[ type || "attr" ]( this[0], key ) || undefined; + else { + obj = {}; + obj[ key ] = value; + } + + // Check to see if we're setting style values + return this.each(function(index){ + // Set all the styles + for ( var prop in obj ) + jQuery.attr( + type ? this.style : this, + prop, jQuery.prop(this, obj[prop], type, index, prop) + ); + }); + }, + + css: function( key, value ) { + return this.attr( key, value, "curCSS" ); + }, + + text: function(e) { + if ( typeof e == "string" ) + return this.empty().append( document.createTextNode( e ) ); + + var t = ""; + jQuery.each( e || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + t += this.nodeType != 1 ? + this.nodeValue : jQuery.fn.text([ this ]); + }); + }); + return t; + }, + + wrap: function() { + // The elements to wrap the target around + var a = jQuery.clean(arguments); + + // Wrap each of the matched elements individually + return this.each(function(){ + // Clone the structure that we're using to wrap + var b = a[0].cloneNode(true); + + // Insert it before the element to be wrapped + this.parentNode.insertBefore( b, this ); + + // Find the deepest point in the wrap structure + while ( b.firstChild ) + b = b.firstChild; + + // Move the matched element to within the wrap structure + b.appendChild( this ); + }); + }, + append: function() { + return this.domManip(arguments, true, 1, function(a){ + this.appendChild( a ); + }); + }, + prepend: function() { + return this.domManip(arguments, true, -1, function(a){ + this.insertBefore( a, this.firstChild ); + }); + }, + before: function() { + return this.domManip(arguments, false, 1, function(a){ + this.parentNode.insertBefore( a, this ); + }); + }, + after: function() { + return this.domManip(arguments, false, -1, function(a){ + this.parentNode.insertBefore( a, this.nextSibling ); + }); + }, + end: function() { + return this.prevObject || jQuery([]); + }, + find: function(t) { + return this.pushStack( jQuery.map( this, function(a){ + return jQuery.find(t,a); + }), t ); + }, + clone: function(deep) { + return this.pushStack( jQuery.map( this, function(a){ + var a = a.cloneNode( deep != undefined ? deep : true ); + a.$events = null; // drop $events expando to avoid firing incorrect events + return a; + }) ); + }, + + filter: function(t) { + return this.pushStack( + jQuery.isFunction( t ) && + jQuery.grep(this, function(el, index){ + return t.apply(el, [index]) + }) || + + jQuery.multiFilter(t,this) ); + }, + + not: function(t) { + return this.pushStack( + t.constructor == String && + jQuery.multiFilter(t, this, true) || + + jQuery.grep(this, function(a) { + return ( t.constructor == Array || t.jquery ) + ? jQuery.inArray( a, t ) < 0 + : a != t; + }) + ); + }, + + add: function(t) { + return this.pushStack( jQuery.merge( + this.get(), + t.constructor == String ? + jQuery(t).get() : + t.length != undefined && (!t.nodeName || t.nodeName == "FORM") ? + t : [t] ) + ); + }, + is: function(expr) { + return expr ? jQuery.filter(expr,this).r.length > 0 : false; + }, + + val: function( val ) { + return val == undefined ? + ( this.length ? this[0].value : null ) : + this.attr( "value", val ); + }, + + html: function( val ) { + return val == undefined ? + ( this.length ? this[0].innerHTML : null ) : + this.empty().append( val ); + }, + domManip: function(args, table, dir, fn){ + var clone = this.length > 1; + var a = jQuery.clean(args); + if ( dir < 0 ) + a.reverse(); + + return this.each(function(){ + var obj = this; + + if ( table && jQuery.nodeName(this, "table") && jQuery.nodeName(a[0], "tr") ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody")); + + jQuery.each( a, function(){ + fn.apply( obj, [ clone ? this.cloneNode(true) : this ] ); + }); + + }); + } +}; + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0], + a = 1; + + // extend jQuery itself if only one argument is passed + if ( arguments.length == 1 ) { + target = this; + a = 0; + } + var prop; + while (prop = arguments[a++]) + // Extend the base object + for ( var i in prop ) target[i] = prop[i]; + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function() { + if ( jQuery._$ ) + $ = jQuery._$; + return jQuery; + }, + + // This may seem like some crazy code, but trust me when I say that this + // is the only cross-browser way to do this. --John + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + typeof fn[0] == "undefined" && /function/i.test( fn + "" ); + }, + + // check if an element is in a XML document + isXMLDoc: function(elem) { + return elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + // args is for internal usage only + each: function( obj, fn, args ) { + if ( obj.length == undefined ) + for ( var i in obj ) + fn.apply( obj[i], args || [i, obj[i]] ); + else + for ( var i = 0, ol = obj.length; i < ol; i++ ) + if ( fn.apply( obj[i], args || [i, obj[i]] ) === false ) break; + return obj; + }, + + prop: function(elem, value, type, index, prop){ + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, [index] ); + + // exclude the following css properties to add px + var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i; + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test(prop) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, c ){ + jQuery.each( c.split(/\s+/), function(i, cur){ + if ( !jQuery.className.has( elem.className, cur ) ) + elem.className += ( elem.className ? " " : "" ) + cur; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, c ){ + elem.className = c ? + jQuery.grep( elem.className.split(/\s+/), function(cur){ + return !jQuery.className.has( c, cur ); + }).join(" ") : ""; + }, + + // internal only, use is(".class") + has: function( t, c ) { + t = t.className || t; + // escape regex characters + c = c.replace(/([\.\\\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); + return t && new RegExp("(^|\\s)" + c + "(\\s|$)").test( t ); + } + }, + swap: function(e,o,f) { + for ( var i in o ) { + e.style["old"+i] = e.style[i]; + e.style[i] = o[i]; + } + f.apply( e, [] ); + for ( var i in o ) + e.style[i] = e.style["old"+i]; + }, + + css: function(e,p) { + if ( p == "height" || p == "width" ) { + var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"]; + + jQuery.each( d, function(){ + old["padding" + this] = 0; + old["border" + this + "Width"] = 0; + }); + + jQuery.swap( e, old, function() { + if (jQuery.css(e,"display") != "none") { + oHeight = e.offsetHeight; + oWidth = e.offsetWidth; + } else { + e = jQuery(e.cloneNode(true)) + .find(":radio").removeAttr("checked").end() + .css({ + visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0" + }).appendTo(e.parentNode)[0]; + + var parPos = jQuery.css(e.parentNode,"position"); + if ( parPos == "" || parPos == "static" ) + e.parentNode.style.position = "relative"; + + oHeight = e.clientHeight; + oWidth = e.clientWidth; + + if ( parPos == "" || parPos == "static" ) + e.parentNode.style.position = "static"; + + e.parentNode.removeChild(e); + } + }); + + return p == "height" ? oHeight : oWidth; + } + + return jQuery.curCSS( e, p ); + }, + + curCSS: function(elem, prop, force) { + var ret; + + if (prop == "opacity" && jQuery.browser.msie) + return jQuery.attr(elem.style, "opacity"); + + if (prop == "float" || prop == "cssFloat") + prop = jQuery.browser.msie ? "styleFloat" : "cssFloat"; + + if (!force && elem.style[prop]) + ret = elem.style[prop]; + + else if (document.defaultView && document.defaultView.getComputedStyle) { + + if (prop == "cssFloat" || prop == "styleFloat") + prop = "float"; + + prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase(); + var cur = document.defaultView.getComputedStyle(elem, null); + + if ( cur ) + ret = cur.getPropertyValue(prop); + else if ( prop == "display" ) + ret = "none"; + else + jQuery.swap(elem, { display: "block" }, function() { + var c = document.defaultView.getComputedStyle(this, ""); + ret = c && c.getPropertyValue(prop) || ""; + }); + + } else if (elem.currentStyle) { + + var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();}); + ret = elem.currentStyle[prop] || elem.currentStyle[newProp]; + + } + + return ret; + }, + + clean: function(a) { + var r = []; + + jQuery.each( a, function(i,arg){ + if ( !arg ) return; + + if ( arg.constructor == Number ) + arg = arg.toString(); + + // Convert html string into DOM nodes + if ( typeof arg == "string" ) { + // Trim whitespace, otherwise indexOf won't work as expected + var s = jQuery.trim(arg), div = document.createElement("div"), tb = []; + + var wrap = + // option or optgroup + !s.indexOf("