diff --git a/journal.py b/journal.py index 813d983..91526ba 100644 --- a/journal.py +++ b/journal.py @@ -7,68 +7,99 @@ from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from pyramid.config import Configurator -from pyramid.events import NewRequest, subscriber from pyramid.httpexceptions import HTTPFound, HTTPInternalServerError, HTTPForbidden from pyramid.security import remember, forget from pyramid.session import SignedCookieSessionFactory from pyramid.view import view_config from waitress import serve -from contextlib import closing import markdown +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) +from zope.sqlalchemy import ZopeTransactionExtension + +USER = 'henryhowes' here = os.path.dirname(os.path.abspath(__file__)) -DB_SCHEMA = """ -CREATE TABLE IF NOT EXISTS entries ( - id serial PRIMARY KEY, - title VARCHAR (127) NOT NULL, - text TEXT NOT NULL, - created TIMESTAMP NOT NULL -) -""" -INSERT_ENTRY = """INSERT INTO entries (title, text, created) VALUES (%s, %s, %s) -""" +logging.basicConfig() +log = logging.getLogger(__file__) -DB_ENTRIES_LIST = """SELECT id, title, text, created FROM entries ORDER BY created DESC -""" +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() -DB_ENTRY = """SELECT * FROM entries WHERE id=%s -""" -NEW_ENTRY = """SELECT * FROM entries ORDER BY created DESC LIMIT 1 -""" +class Entry(Base): + __tablename__ = 'entries' + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + title = sa.Column(sa.Unicode(127), nullable=False) + text = sa.Column(sa.UnicodeText, nullable=False) + created = sa.Column( + sa.DateTime, nullable=False, default=datetime.datetime.utcnow + ) -UPDATE_ENTRY = """UPDATE entries SET title=%s, text=%s WHERE id=%s -""" + def __repr__(self): + return u"{}: {}".format(self.__class__.__name__, self.title) -logging.basicConfig() -log = logging.getLogger(__file__) + @classmethod + def all(cls): + return DBSession.query(cls).order_by(cls.created.desc()).all() + + @classmethod + def newest_entry(cls): + return DBSession.query(cls).order_by(cls.created.desc()).first() + + @classmethod + def by_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).one() + + @classmethod + def from_request(cls, request): + title = request.params.get('title', None) + text = request.params.get('text', None) + created = datetime.datetime.utcnow() + new_entry = cls(title=title, text=text, created=created) + DBSession.add(new_entry) + + def update_from_request(self, request): + self.title = request.params.get('title', None) + self.text = request.params.get('text', None) + + def render_markdown(self): + return markdown.markdown( + self.text, extensions=['codehilite', 'fenced_code']) + + def convert_strftime(self): + return self.created.strftime('%b %d, %Y') + + def json(self): + return {'title': self.title, + 'text': self.render_markdown(), + 'created': self.created.strftime('%b %d, %Y'), + 'id': self.id} + + def json_edit_get(self): + return {'title': self.title, + 'text': self.text, + 'created': self.created.strftime('%b %d, %Y'), + 'id': self.id} @view_config(route_name='home', renderer='templates/list2.jinja2') def read_entries(request): """return a list of all entries as dicts""" - cursor = request.db.cursor() - cursor.execute(DB_ENTRIES_LIST) - keys = ('id', 'title', 'text', 'created') - entries = [dict(zip(keys, row)) for row in cursor.fetchall()] - for item in entries: - item['text'] = markdown.markdown( - item['text'], extensions=['codehilite', 'fenced_code']) + entries = Entry.all() return {'entries': entries} @view_config(route_name='detail', renderer='templates/detail.jinja2') def read_entry(request): """return a list of all entries as dicts""" - cursor = request.db.cursor() - cursor.execute(DB_ENTRY, (request.matchdict['id'], )) - keys = ('id', 'title', 'text', 'created') - row = cursor.fetchone() - entry = dict(zip(keys, row)) - entry['text'] = markdown.markdown( - entry['text'], extensions=['codehilite', 'fenced_code']) + entry = Entry.by_id(request.matchdict['id']) return {'entry': entry} @@ -76,77 +107,35 @@ def read_entry(request): def edit_entry_view(request): """return a list of all entries as dicts""" if request.authenticated_userid: + entry = Entry.by_id(request.params.get('id', None)) if request.method == 'GET': - # import pdb; pdb.set_trace(); - cursor = request.db.cursor() - cursor.execute(DB_ENTRY, (request.params.get('id', None), )) - keys = ('id', 'title', 'text', 'created') - row = cursor.fetchone() - entry = dict(zip(keys, row)) - entry['created'] = entry['created'].strftime('%b %d, %Y') - - return entry + return entry.json_edit_get() elif request.method == 'POST': - # import pdb; pdb.set_trace() try: - edit_entry(request) + entry.update_from_request(request) except psycopg2.Error: # this will catch any errors generated by the database return HTTPInternalServerError() - - cursor = request.db.cursor() - cursor.execute(DB_ENTRY, (request.params.get('id', None), )) - keys = ('id', 'title', 'text', 'created') - # import pdb; pdb.set_trace() - row = cursor.fetchone() - entry = dict(zip(keys, row)) - - entry['text'] = markdown.markdown( - entry['text'], extensions=['codehilite', 'fenced_code']) - entry['created'] = entry['created'].strftime('%b %d, %Y') - return entry + return entry.json() else: return HTTPForbidden() -def write_entry(request): - """write a single entry to the database""" - title = request.params.get('title', None) - text = request.params.get('text', None) - created = datetime.datetime.utcnow() - request.db.cursor().execute(INSERT_ENTRY, [title, text, created]) - - -def edit_entry(request): - """write a single entry to the database""" - title = request.params.get('title', None) - text = request.params.get('text', None) - id = request.params.get('id', None) - request.db.cursor().execute(UPDATE_ENTRY, [title, text, id]) - - @view_config(route_name='new', renderer='json') def add_entry(request): if request.authenticated_userid: if request.method == 'POST': try: - write_entry(request) + Entry.from_request(request) except psycopg2.Error: # this will catch any errors generated by the database return HTTPInternalServerError - cursor = request.db.cursor() - cursor.execute(NEW_ENTRY) - keys = ('id', 'title', 'text', 'created') - row = cursor.fetchone() - entry = dict(zip(keys, row)) - entry['text'] = markdown.markdown( - entry['text'], extensions=['codehilite', 'fenced_code']) - entry['created'] = entry['created'].strftime('%b %d, %Y') - return entry + entry = Entry.newest_entry() + return entry.json() else: - return HTTPForbidden + return HTTPForbidden() @view_config(route_name='login', renderer="templates/login.jinja2") @@ -177,48 +166,6 @@ def logout(request): headers=headers) -def connect_db(settings): - """Return a connection to the configured database""" - return psycopg2.connect(settings['db']) - - -def init_db(): - """Create database tables defined by DB_SCHEMA - - Warning: This function will not update existing table definitions - """ - settings = {} - settings['db'] = os.environ.get( - 'DATABASE_URL', 'dbname=learning_journal user=henryhowes' - ) - with closing(connect_db(settings)) as db: - db.cursor().execute(DB_SCHEMA) - db.commit() - - -@subscriber(NewRequest) -def open_connection(event): - request = event.request - settings = request.registry.settings - request.db = connect_db(settings) - request.add_finished_callback(close_connection) - - -def close_connection(request): - """close the database connection for this request - - If there has been an error in the processing of the request, abort any - open transactions. - """ - db = getattr(request, 'db', None) - if db is not None: - if request.exception is not None: - db.rollback() - else: - db.commit() - request.db.close() - - def do_login(request): username = request.params.get('username', None) password = request.params.get('password', None) @@ -237,9 +184,12 @@ def main(): settings = {} settings['reload_all'] = os.environ.get('DEBUG', True) settings['debug_all'] = os.environ.get('DEBUG', True) - settings['db'] = os.environ.get( - 'DATABASE_URL', 'dbname=learning_journal user=henryhowes' + settings['sqlalchemy.url'] = os.environ.get( + 'DATABASE_URL', + 'postgresql://{}:@localhost:5432/learning_journal'.format(USER) ) + engine = sa.engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) settings['auth.username'] = os.environ.get('AUTH_USERNAME', 'admin') manager = BCRYPTPasswordManager() settings['auth.password'] = os.environ.get( @@ -260,6 +210,7 @@ def main(): authorization_policy=ACLAuthorizationPolicy(), ) config.include('pyramid_jinja2') + config.include('pyramid_tm') config.add_static_view('static', os.path.join(here, 'static')) config.add_route('home', '/') config.add_route('new', '/new') diff --git a/requirements.txt b/requirements.txt index b3d363d..5b9da65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,35 @@ -Jinja2==2.7.3 -Markdown==2.5.2 -MarkupSafe==0.23 -PasteDeploy==1.5.2 -Pygments==2.0.2 -WebOb==1.4 -WebTest==2.0.18 -argparse==1.3.0 beautifulsoup4==4.3.2 cryptacular==1.4.1 extras==0.0.3 fuzzywuzzy==0.5.0 +Jinja2==2.7.3 lettuce==0.2.20 +Markdown==2.5.2 +MarkupSafe==0.23 mock==1.0.1 +PasteDeploy==1.5.2 pbkdf2==1.3 psycopg2==2.5.4 py==1.4.26 +Pygments==2.0.2 pyramid==1.5.2 pyramid-jinja2==2.3.3 +pyramid-tm==0.11 pytest==2.6.4 python-mimeparse==0.1.4 python-subunit==1.0.0 repoze.lru==0.6 six==1.9.0 +SQLAlchemy==0.9.8 sure==1.2.9 testtools==1.5.0 +transaction==1.4.3 translationstring==1.3 unittest2==0.8.0 venusian==1.0 waitress==0.8.9 -wsgiref==0.1.2 +WebOb==1.4 +WebTest==2.0.17 zope.deprecation==4.1.2 zope.interface==4.1.2 +zope.sqlalchemy==0.7.5 diff --git a/templates/base.jinja2 b/templates/base.jinja2 deleted file mode 100644 index a8cdaae..0000000 --- a/templates/base.jinja2 +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Python Learning Journal - - - - -
- - -
-
-

My Python Journal

-
- {% block body %}{% endblock %} -
-
- - - \ No newline at end of file diff --git a/templates/detail.jinja2 b/templates/detail.jinja2 index e507b3f..151cc2e 100644 --- a/templates/detail.jinja2 +++ b/templates/detail.jinja2 @@ -9,7 +9,7 @@ {% endif %}

{{ entry.created.strftime('%b. %d, %Y') }}

- {{ entry.text|safe }} + {{ entry.render_markdown()|safe }}
Tweet diff --git a/templates/list.jinja2 b/templates/list.jinja2 deleted file mode 100644 index f037f46..0000000 --- a/templates/list.jinja2 +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base.jinja2" %} -{% block body %} - {% if request.authenticated_userid %} - - {% endif %} -

Entries

- {% for entry in entries %} -
-

{{ entry.title }}

-

{{ entry.created.strftime('%b. %d, %Y') }} -

- {{ entry.text|safe }} -
-
- {% else %} -
-

No entries here so far

-
- {% endfor %} -{% endblock %} \ No newline at end of file diff --git a/templates/list2.jinja2 b/templates/list2.jinja2 index 1251fcf..d68e09c 100644 --- a/templates/list2.jinja2 +++ b/templates/list2.jinja2 @@ -2,8 +2,6 @@ {% block body %} {% if request.authenticated_userid %}
-
@@ -25,7 +23,7 @@

{{ entry.title }}

- {{ entry.text|safe }} + {{ entry.render_markdown()|safe }}
{% else %}