diff --git a/lodgeit/application.py b/lodgeit/application.py index d53128b..3fc22cb 100644 --- a/lodgeit/application.py +++ b/lodgeit/application.py @@ -5,8 +5,7 @@ the WSGI application - :copyright: 2007 by Armin Ronacher, Christopher Grebs. - 2008 by Christopher Grebs. + :copyright: 2007-2009 by Armin Ronacher, Christopher Grebs. :license: BSD """ import os @@ -15,10 +14,12 @@ from babel import Locale from werkzeug import SharedDataMiddleware, ClosingIterator from werkzeug.exceptions import HTTPException, NotFound from sqlalchemy import create_engine -from lodgeit import i18n, local +from lodgeit import i18n +from lodgeit.local import application, ctx, _local_manager from lodgeit.urls import urlmap from lodgeit.utils import COOKIE_NAME, Request, jinja_environment -from lodgeit.database import metadata, session +from lodgeit.database import db +from lodgeit.models import Paste from lodgeit.controllers import get_controller @@ -30,23 +31,24 @@ class LodgeIt(object): #: bind metadata, create engine and create all tables self.engine = engine = create_engine(dburi, convert_unicode=True) - metadata.bind = engine - metadata.create_all(engine) + db.metadata.bind = engine + db.metadata.create_all(engine, [Paste.__table__]) #: jinja_environment update - jinja_environment.globals.update( - i18n_languages=i18n.list_languages() - ) - jinja_environment.filters.update( - datetimeformat=i18n.format_datetime - ) + jinja_environment.globals.update({ + 'i18n_languages': i18n.list_languages()}) + jinja_environment.filters.update({ + 'datetimeformat': i18n.format_datetime}) jinja_environment.install_null_translations() #: bind the application to the current context local self.bind_to_context() + self.cleanup_callbacks = (db.session.close, _local_manager.cleanup, + self.bind_to_context()) + def bind_to_context(self): - local.application = self + ctx.application = application = self def __call__(self, environ, start_response): """Minimal WSGI application for request dispatching.""" @@ -72,7 +74,7 @@ class LodgeIt(object): expires=expires) return ClosingIterator(resp(environ, start_response), - [local._local_manager.cleanup, session.remove]) + self.cleanup_callbacks) def make_app(dburi, secret_key, debug=False, shell=False): @@ -81,9 +83,9 @@ def make_app(dburi, secret_key, debug=False, shell=False): app = LodgeIt(dburi, secret_key) if debug: app.engine.echo = True + app.bind_to_context() if not shell: # we don't need access to the shared data middleware in shell mode app = SharedDataMiddleware(app, { - '/static': static_path - }) + '/static': static_path}) return app diff --git a/lodgeit/controllers/pastes.py b/lodgeit/controllers/pastes.py index 4a814b4..8123cb4 100644 --- a/lodgeit/controllers/pastes.py +++ b/lodgeit/controllers/pastes.py @@ -14,7 +14,8 @@ from lodgeit import local from lodgeit.lib import antispam from lodgeit.i18n import list_languages, _ from lodgeit.utils import render_to_response -from lodgeit.database import session, Paste +from lodgeit.models import Paste +from lodgeit.database import db from lodgeit.lib.highlighting import list_languages, STYLES, get_style from lodgeit.lib.pagination import generate_pagination from lodgeit.lib.captcha import check_hashed_solution, Captcha @@ -57,7 +58,8 @@ class PasteController(object): if code and language and not error: paste = Paste(code, language, parent, req.user_hash, 'private' in req.form) - session.flush() + db.session.add(paste) + db.session.commit() local.request.session['language'] = language return redirect(paste.url) diff --git a/lodgeit/database.py b/lodgeit/database.py index 19fb95f..2e35445 100644 --- a/lodgeit/database.py +++ b/lodgeit/database.py @@ -5,190 +5,56 @@ Database fun :) - :copyright: 2007-2008 by Armin Ronacher, Christopher Grebs. + :copyright: 2007-2010 by Armin Ronacher, Christopher Grebs. :license: BSD """ -import time -import difflib -from datetime import datetime -from werkzeug import cached_property -from sqlalchemy import MetaData, Integer, Text, DateTime, ForeignKey, \ - String, Boolean, Table, Column, select, and_, func -from sqlalchemy.orm import scoped_session, create_session, backref, relation -from sqlalchemy.orm.scoping import ScopedSession -from lodgeit import local -from lodgeit.utils import generate_paste_hash -from lodgeit.lib.highlighting import highlight, preview_highlight, LANGUAGES +import sys +from types import ModuleType +import sqlalchemy +from sqlalchemy import MetaData, create_engine +from sqlalchemy import orm, sql +from sqlalchemy.orm.session import Session +from sqlalchemy.ext.declarative import declarative_base +from lodgeit.local import application, _local_manager -session = scoped_session(lambda: create_session(local.application.engine), - scopefunc=local._local_manager.get_ident) - metadata = MetaData() -pastes = Table('pastes', metadata, - Column('paste_id', Integer, primary_key=True), - Column('code', Text), - Column('parent_id', Integer, ForeignKey('pastes.paste_id'), - nullable=True), - Column('pub_date', DateTime), - Column('language', String(30)), - Column('user_hash', String(40), nullable=True), - Column('handled', Boolean, nullable=False), - Column('private_id', String(40), unique=True, nullable=True) -) + +def session_factory(): + opts = { + 'autoflush': True, + 'transactional': True} + return orm.create_session(application.engine, **opts) +session = orm.scoped_session( + session_factory, scopefunc=_local_manager.get_ident) -class Paste(object): - """Represents a paste.""" +class ModelBase(object): + """Internal baseclass for all models. It provides some syntactic + sugar and maps the default query property. - def __init__(self, code, language, parent=None, user_hash=None, - private=False): - if language not in LANGUAGES: - language = 'text' - self.code = u'\n'.join(code.splitlines()) - self.language = language - 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 - self.private = private - - @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 id in reverse - order. - """ - return Paste.query.filter(Paste.private_id == None) \ - .order_by(Paste.paste_id.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 - - @staticmethod - def fetch_replies(): - """Get the new replies for the ower of a request and flag them - as handled. - """ - s = select([pastes.c.paste_id], - Paste.user_hash == local.request.user_hash - ) - - paste_list = Paste.query.filter(and_( - Paste.parent_id.in_(s), - Paste.handled == False, - Paste.user_hash != local.request.user_hash, - )).order_by(pastes.c.paste_id.desc()).all() - - to_mark = [p.paste_id for p in paste_list] - session.execute(pastes.update(pastes.c.paste_id.in_(to_mark), - values={'handled': True})) - return paste_list - - 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): - """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 #%s' % self.identifier, - tofile='Paste #%s' % other.identifier, - lineterm='', - n=context_lines - )) - if template: - from lodgeit.lib.diff import prepare_udiff - diff, info = prepare_udiff(udiff) - return diff and diff[0] or None - return udiff - - @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.""" - 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, num=5): - """Render a preview for this paste.""" - return preview_highlight(self.code, self.language, num) + We use the declarative model api from sqlalchemy. + """ -session.mapper(Paste, pastes, properties={ - 'children': relation(Paste, - primaryjoin=pastes.c.parent_id==pastes.c.paste_id, - cascade='all', - backref=backref('parent', remote_side=[pastes.c.paste_id]) - ) -}) +# configure the declarative base +Model = declarative_base(name='Model', cls=ModelBase, + mapper=orm.mapper, metadata=metadata) +ModelBase.query = session.query_property() + + +def _make_module(): + db = ModuleType('db') + for mod in sqlalchemy, orm: + for key, value in mod.__dict__.iteritems(): + if key in mod.__all__: + setattr(db, key, value) + + db.session = session + db.metadata = metadata + db.Model = Model + db.NoResultFound = orm.exc.NoResultFound + return db + +sys.modules['lodgeit.database.db'] = db = _make_module() diff --git a/lodgeit/lib/webapi.py b/lodgeit/lib/webapi.py index 32fc5be..241a43f 100644 --- a/lodgeit/lib/webapi.py +++ b/lodgeit/lib/webapi.py @@ -9,7 +9,8 @@ :license: BSD. """ import inspect -from lodgeit.database import session, Paste +from lodgeit.models import Paste +from lodgeit.database import db from lodgeit.lib.xmlrpc import XMLRPCRequestHandler from lodgeit.lib.json import JSONRequestHandler from lodgeit.lib.highlighting import STYLES, LANGUAGES, get_style, \ @@ -77,7 +78,8 @@ def pastes_new_paste(language, code, parent_id=None, raise ValueError('parent paste not found') paste = Paste(code, language, parent, private=private) - session.flush() + db.session.add(paste) + db.session.commit() return paste.identifier diff --git a/lodgeit/utils.py b/lodgeit/utils.py index 05c5904..a49f060 100644 --- a/lodgeit/utils.py +++ b/lodgeit/utils.py @@ -108,7 +108,7 @@ def render_to_response(template_name, **context): adds the current request to the context. This is used for the welcome message. """ - from lodgeit.database import Paste + from lodgeit.models import Paste request = local.request if request.method == 'GET': context['new_replies'] = Paste.fetch_replies() diff --git a/manage.py b/manage.py index 0852399..19031a0 100644 --- a/manage.py +++ b/manage.py @@ -1,12 +1,10 @@ import os -from werkzeug import script -from werkzeug.serving import run_simple -from werkzeug.utils import create_environ, run_wsgi_app +from werkzeug import script, run_simple, create_environ, run_wsgi_app from lodgeit import local from lodgeit.application import make_app -from lodgeit.database import session +from lodgeit.database import db dburi = 'sqlite:////tmp/lodgeit.db' @@ -18,19 +16,19 @@ def run_app(app, path='/'): return run_wsgi_app(app, env) action_runserver = script.make_runserver( - lambda: make_app(dburi, SECRET_KEY), + lambda: make_app(dburi, SECRET_KEY, debug=True), use_reloader=True) action_shell = script.make_shell( lambda: { 'app': make_app(dburi, SECRET_KEY, False, True), 'local': local, - 'session': session, + 'db': db, 'run_app': run_app }, ('\nWelcome to the interactive shell environment of LodgeIt!\n' '\n' - 'You can use the following predefined objects: app, local, session.\n' + 'You can use the following predefined objects: app, local, db.\n' 'To run the application (creates a request) use *run_app*.') ) diff --git a/scripts/make-bootstrap.py b/scripts/make-bootstrap.py index 970c983..0b30ddf 100755 --- a/scripts/make-bootstrap.py +++ b/scripts/make-bootstrap.py @@ -16,7 +16,7 @@ def after_install(options, home_dir): easy_install('Jinja2', home_dir) easy_install('Werkzeug', home_dir) easy_install('Pygments', home_dir) - easy_install('SQLAlchemy', home_dir) + easy_install('SQLAlchemy==0.6', home_dir) easy_install('simplejson', home_dir) easy_install('Babel', home_dir) easy_install('PIL', home_dir)