-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjournal.py
276 lines (224 loc) · 8.08 KB
/
journal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import psycopg2
import os
import logging
import datetime
import markdown
import jinja2
from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory
from pyramid.view import view_config
from pyramid.events import NewRequest, subscriber
from waitress import serve
from contextlib import closing
from pyramid.httpexceptions import HTTPFound, HTTPInternalServerError
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from cryptacular.bcrypt import BCRYPTPasswordManager
from pyramid.security import remember, forget
here = os.path.dirname(os.path.abspath(__file__))
# LOGIN TO WEBSITE
def do_login(request):
username = request.params.get('username', None)
password = request.params.get('password', None)
if not (username and password):
raise ValueError('both username and password are required')
settings = request.registry.settings
manager = BCRYPTPasswordManager()
if username == settings.get('auth.username', ''):
hashed = settings.get('auth.password', '')
return manager.check(hashed, password)
# SQL COMMANDS
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)
"""
UPDATE_ENTRY = """
UPDATE entries SET (title, text) = (%s, %s) WHERE id=%s
"""
DB_ENTRIES_LIST = """
SELECT id, title, text, created FROM entries ORDER BY created DESC
"""
SELECT_SINGLE_ENTRY = """
SELECT * FROM entries WHERE id=%s;
"""
# MARKDOWN FILTER
def mdown(text):
if isinstance(text, str):
text = text.decode('utf-8')
return markdown.markdown(
text, extensions=['codehilite', 'fenced_code'])
# DBASE SETUP AND TEARDOWN
logging.basicConfig()
log = logging.getLogger(__file__)
def connect_db(settings):
"""Return a connection to the configured database"""
return psycopg2.connect(settings['db'])
def init_db():
"""Create database dables 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=JustinKan'
)
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()
# DBASE ENTRIES MANIPULATION
def update_entry(request, id):
"""update an entry already in the database"""
title = request.params.get('title', None)
text = request.params.get('text', None)
request.db.cursor().execute(UPDATE_ENTRY, [title, text, id])
def write_entry(request):
""" add a new 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 get_single_entry(request, mark_down=False):
"""get single entry from dbase- returns markdown if mark_down set to True"""
id = request.matchdict.get('id', -1)
cursor = request.db.cursor()
cursor.execute(SELECT_SINGLE_ENTRY, (id,))
keys = ('id', 'title', 'text', 'created')
entry = dict(zip(keys, cursor.fetchone()))
if isinstance(entry['text'], str):
entry['text'] = entry['text'].decode('utf-8')
if mark_down:
entry['text'] = mdown(entry['text'])
return {'entry': entry}
# ADD ENTRY VIEW
@view_config(route_name='add', request_method='POST')
def add_entry(request):
try:
write_entry(request)
except psycopg2.Error:
# this will catch any errors generated by the database
return HTTPInternalServerError
return HTTPFound(request.route_url('home'))
# HOME PAGE VIEW
@view_config(route_name='home', renderer='templates/list.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 entry in entries:
entry['text'] = mdown(entry['text'])
return {'entries': entries}
# EDIT ENTRY PAGE VIEW
@view_config(route_name='edit', renderer='templates/edit.jinja2')
def edit(request):
"""view for edit single entry"""
if request.authenticated_userid:
if request.method == 'POST':
try:
id = request.matchdict.get('id', -1)
update_entry(request, id)
except psycopg2.Error:
# this will catch any errors generated by the database
return HTTPInternalServerError
return HTTPFound(request.route_url('home'))
return get_single_entry(request)
# DETAIL PAGE VIEW
@view_config(route_name='detail', renderer='templates/detail.jinja2')
def detail_view(request):
"""view for entry (permalink for an entry)."""
return get_single_entry(request, True)
# LOGIN VIEW
@view_config(route_name='login', renderer="templates/login.jinja2")
def login(request):
"""authenticate a user by username/password"""
username = request.params.get('username', '')
error = ''
if request.method == 'POST':
error = "Login Failed"
authenticated = False
try:
authenticated = do_login(request)
except ValueError as e:
error = str(e)
if authenticated:
headers = remember(request, username)
return HTTPFound(request.route_url('home'), headers=headers)
return {'error': error, 'username': username}
# LOGOUT VIEW
@view_config(route_name='logout')
def logout(request):
headers = forget(request)
return HTTPFound(request.route_url('home'), headers=headers)
def main():
"""Create a configured wsgi app"""
# configuraton settings
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=JustinKan'
)
settings['auth.username'] = os.environ.get('AUTH_USERNAME', 'admin')
manager = BCRYPTPasswordManager()
settings['auth.password'] = os.environ.get(
'AUTH_PASSWORD', manager.encode('secret')
)
# secret value for session signing:
secret = os.environ.get('JOURNAL_SESSION_SECRET', 'itsaseekrit')
session_factory = SignedCookieSessionFactory(secret)
# secret value for auth signing
auth_secret = os.environ.get('JOURNAL_AUTH_SECRET', 'anotherseekrit')
# configuration setup
config = Configurator(
settings=settings,
session_factory=session_factory,
authentication_policy=AuthTktAuthenticationPolicy(
secret=auth_secret,
hashalg='sha512'
),
authorization_policy=ACLAuthorizationPolicy(),
)
config.include('pyramid_jinja2')
config.add_static_view('static', os.path.join(here, 'static'))
config.add_route('home', '/')
config.add_route('add', '/add')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('detail', '/detail/{id:\d+}')
config.add_route('edit', '/edit/{id:\d+}')
config.scan()
jinja2.filters.FILTERS['markdown'] = mdown
# serve app
app = config.make_wsgi_app()
return app
if __name__ == '__main__':
app = main()
port = os.environ.get('PORT', 5000)
serve(app, host='0.0.0.0', port=port)