home » zplus/freepost.git
Author zPlus <zplus@peers.community> 2019-03-18 10:44:42
Committer zPlus <zplus@peers.community> 2019-03-18 10:44:42
Commit f3e0c86 (patch)
Tree 6605bde
Parent(s)

Move from mysql to sqlite. Fix #54 Fix #89


commits diff: e7488d0..f3e0c86
9 files changed, 741 insertions, 879 deletionsdownload


Diffstat
-rwxr-xr-x .gitignore 1
-rwxr-xr-x README.md 26
-rw-r--r-- database.schema.sql 103
-rwxr-xr-x freepost/__init__.py 20
-rw-r--r-- freepost/database.py 1183
-rw-r--r-- freepost/templates/posts.html 2
?--------- freepost_freepost.sql 272
-rw-r--r-- requirements.txt 3
-rw-r--r-- settings.yaml 10

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+1/-0 M   .gitignore
index 4b9d04d..a3430b3
old size: 74B - new size: 91B
@@ -3,4 +3,5 @@ venv/
3 3 *.pyc
4 4
5 5 /freepost/static/css/
6 + /database.sqlite
6 7 /settings.production.yaml

+18/-8 M   README.md
index 1db55cf..3ec4d7f
old size: 949B - new size: 2K
@@ -8,13 +8,12 @@ users can read and comment.
8 8
9 9 ## Setup Python3 virtual environment
10 10
11 - mkdir venv
12 - virtualenv -p python3 venv
13 - -> alternative: python3 -m venv venv
11 + cd freepost-directory
12 + python3 -m venv venv (if this doesn't work, try `virtualenv -p python3 venv`)
14 13 source venv/bin/activate
15 14 pip3 install -r requirements.txt
16 15
17 - ## Run dev server
16 + ## Run test server
18 17
19 18 source venv/bin/activate
20 19 python3 -m bottle --debug --reload --bind 127.0.0.1:8000 freepost
@@ -25,10 +24,21 @@ Build CSS files
25 24
26 25 stylus --watch --compress --disable-cache --out freepost/static/css/ freepost/static/stylus/freepost.styl
27 26
28 - ## Deploy
29 -
30 - - Rename `.htaccess.wsgi` or `.htaccess.cgi` to `.htaccess`
31 - - Change settings in `settings.yaml`
27 + # Deployment
28 +
29 + - Build CSS stylesheets (see `Development` above)
30 + - Copy all files to your `public_html` folder
31 + - Make sure `settings.yaml` has restricted permissions, for instance `0600`
32 + - If the SQLite database is located in the same HTML folder, make sure this too has
33 + restricted access
34 + - Rename `.htaccess.wsgi` or `.htaccess.cgi` to `.htaccess` (if you use CGI or WSGI)
35 + - Change settings in `settings.yaml` if needed
36 + - Create Python virtual environment
37 + For tuxfamily only: run `newgrp freepost` before creating the virtenv, for quota reasons
38 + - Create a new empty SQLite database: `cat database.schema.sql | sqlite3 database.sqlite`
39 +
40 + Everything should be setup and working. Make sure your CGI or WSGI server is
41 + configured correctly.
32 42
33 43 ## License
34 44

+103/-0 A   database.schema.sql
index 0000000..24dbe9d
old size: 0B - new size: 4K
new file mode: -rw-r--r--
@@ -0,0 +1,103 @@
1 + BEGIN TRANSACTION;
2 + CREATE TABLE `vote_post` (
3 + `vote` integer NOT NULL
4 + , `datetime` datetime NOT NULL
5 + , `postId` integer NOT NULL
6 + , `userId` integer NOT NULL
7 + , PRIMARY KEY (`postId`,`userId`)
8 + , CONSTRAINT `FK_EDE89DBC64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
9 + , CONSTRAINT `FK_EDE89DBCE094D20D` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
10 + );
11 + CREATE TABLE `vote_comment` (
12 + `vote` integer NOT NULL
13 + , `datetime` datetime NOT NULL
14 + , `commentId` integer NOT NULL
15 + , `userId` integer NOT NULL
16 + , PRIMARY KEY (`commentId`,`userId`)
17 + , CONSTRAINT `FK_1FC60DF464B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
18 + , CONSTRAINT `FK_1FC60DF46690C3F5` FOREIGN KEY (`commentId`) REFERENCES `comment` (`id`)
19 + );
20 + CREATE TABLE `user` (
21 + `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
22 + , `hashId` varchar(32) NOT NULL
23 + , `email` varchar(255) DEFAULT NULL
24 + , `email_notifications` integer NOT NULL DEFAULT '1'
25 + , `isActive` integer NOT NULL
26 + , `password` varchar(255) NOT NULL
27 + , `passwordResetToken` varchar(255) DEFAULT NULL
28 + , `passwordResetTokenExpire` datetime DEFAULT NULL
29 + , `registered` datetime NOT NULL
30 + , `salt` varchar(255) NOT NULL
31 + , `username` varchar(255) NOT NULL
32 + , `about` varchar(10000) NOT NULL DEFAULT ''
33 + , `session` varchar(255) DEFAULT NULL
34 + , `preferred_feed` varchar(64) NOT NULL DEFAULT 'hot'
35 + , UNIQUE (`hashId`)
36 + , UNIQUE (`username`)
37 + , UNIQUE (`email`)
38 + , UNIQUE (`passwordResetToken`)
39 + , UNIQUE (`session`)
40 + );
41 + CREATE TABLE `topic` (
42 + `post_id` integer NOT NULL
43 + , `name` varchar(45) NOT NULL
44 + , PRIMARY KEY (`post_id`,`name`)
45 + , CONSTRAINT `fk_topic_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
46 + );
47 + CREATE TABLE `remember_me` (
48 + `token` char(128) NOT NULL
49 + , `userId` integer NOT NULL
50 + , `expires` datetime NOT NULL
51 + , PRIMARY KEY (`token`)
52 + , CONSTRAINT `FK_userId` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
53 + );
54 + CREATE TABLE `post` (
55 + `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
56 + , `hashId` varchar(32) NOT NULL
57 + , `created` datetime NOT NULL
58 + , `dateCreated` date NOT NULL
59 + , `title` varchar(255) NOT NULL
60 + , `link` text COLLATE BINARY
61 + , `text` longtext NOT NULL
62 + , `vote` integer NOT NULL
63 + , `commentsCount` integer NOT NULL
64 + , `userId` integer DEFAULT NULL
65 + , UNIQUE (`hashId`)
66 + , CONSTRAINT `FK_5A8A6C8D64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
67 + );
68 + CREATE TABLE `comment` (
69 + `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
70 + , `hashId` varchar(32) NOT NULL
71 + , `created` datetime NOT NULL
72 + , `dateCreated` date NOT NULL
73 + , `read` integer NOT NULL
74 + , `text` longtext NOT NULL
75 + , `vote` integer NOT NULL
76 + , `parentId` integer DEFAULT NULL
77 + , `parentUserId` integer DEFAULT NULL
78 + , `postId` integer DEFAULT NULL
79 + , `userId` integer DEFAULT NULL
80 + , UNIQUE (`hashId`)
81 + , CONSTRAINT `FK_9474526C10EE4CEE` FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`)
82 + , CONSTRAINT `FK_9474526C251330C5` FOREIGN KEY (`parentUserId`) REFERENCES `user` (`id`)
83 + , CONSTRAINT `FK_9474526C64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
84 + , CONSTRAINT `FK_9474526CE094D20D` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
85 + );
86 + CREATE INDEX "idx_vote_post_IDX_EDE89DBCE094D20D" ON "vote_post" (`postId`);
87 + CREATE INDEX "idx_vote_post_IDX_EDE89DBC64B64DCC" ON "vote_post" (`userId`);
88 + CREATE INDEX "idx_vote_comment_IDX_1FC60DF46690C3F5" ON "vote_comment" (`commentId`);
89 + CREATE INDEX "idx_vote_comment_IDX_1FC60DF464B64DCC" ON "vote_comment" (`userId`);
90 + CREATE INDEX "idx_remember_me_userId" ON "remember_me" (`userId`);
91 + CREATE INDEX "idx_post_vote" ON "post" (`vote`);
92 + CREATE INDEX "idx_post_dateCreated" ON "post" (`dateCreated`);
93 + CREATE INDEX "idx_post_created" ON "post" (`created`);
94 + CREATE INDEX "idx_post_IDX_5A8A6C8D64B64DCC" ON "post" (`userId`);
95 + CREATE INDEX "idx_comment_vote" ON "comment" (`vote`);
96 + CREATE INDEX "idx_comment_isRead" ON "comment" (`read`);
97 + CREATE INDEX "idx_comment_dateCreated" ON "comment" (`dateCreated`);
98 + CREATE INDEX "idx_comment_created" ON "comment" (`created`);
99 + CREATE INDEX "idx_comment_IDX_9474526CE094D20D" ON "comment" (`postId`);
100 + CREATE INDEX "idx_comment_IDX_9474526C64B64DCC" ON "comment" (`userId`);
101 + CREATE INDEX "idx_comment_IDX_9474526C251330C5" ON "comment" (`parentUserId`);
102 + CREATE INDEX "idx_comment_IDX_9474526C10EE4CEE" ON "comment" (`parentId`);
103 + COMMIT;

+12/-8 M   freepost/__init__.py
index c23627d..8104457
old size: 27K - new size: 27K
@@ -3,7 +3,9 @@
3 3 import bleach
4 4 import bottle
5 5 import configparser
6 + import dateutil.parser
6 7 import functools
8 + import hashlib
7 9 import importlib
8 10 import json
9 11 import markdown
@@ -30,10 +32,10 @@ template = functools.partial (
30 32 template,
31 33 template_settings = {
32 34 'filters': {
33 - 'ago': lambda date: timeago.format (date),
34 - 'datetime': lambda date: date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'),
35 + 'ago': lambda date: timeago.format(date),
36 + 'datetime': lambda date: date,# date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'),
35 37 # TODO this should be renamed. It's only a way to pretty print dates
36 - 'title': lambda date: date.strftime ('%b %-d, %Y - %H:%M%z%Z'),
38 + 'title': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
37 39 # Convert markdown to plain text
38 40 'md2txt': lambda text: bleach.clean (markdown.markdown (text),
39 41 tags=[], attributes={}, styles=[], strip=True),
@@ -179,7 +181,7 @@ def login ():
179 181 """
180 182 The login page.
181 183 """
182 -
184 +
183 185 return template ('login.html')
184 186
185 187 @post ('/login')
@@ -212,7 +214,7 @@ def login_check ():
212 214
213 215 # Start new session
214 216 session.start (user['id'], remember)
215 -
217 + print(user)
216 218 # Redirect logged in user to preferred feed
217 219 if user['preferred_feed'] == 'new':
218 220 redirect (application.get_url ('homepage') + '?sort=new')
@@ -241,7 +243,9 @@ def register_new_account ():
241 243 # Normalize username
242 244 username = username.strip ()
243 245
244 - if len (username) == 0 or database.username_exists (username):
246 + # Check if username already exists.
247 + # Use case-insensitive match to prevent two similar usernames.
248 + if len (username) == 0 or database.username_exists (username, case_sensitive=False):
245 249 return template (
246 250 'register.html',
247 251 flash='Name taken, please choose another.')
@@ -486,12 +490,12 @@ def post_thread (hash_id):
486 490 if 0 not in comments_tree:
487 491 comments_tree[0] = []
488 492
489 - comments_tree[0].append (comment)
493 + comments_tree[0].append(dict(comment))
490 494 else:
491 495 if comment['parentId'] not in comments_tree:
492 496 comments_tree[comment['parentId']] = []
493 497
494 - comments_tree[comment['parentId']].append (comment)
498 + comments_tree[comment['parentId']].append(dict(comment))
495 499
496 500 # Build ordered list of comments (recourse tree)
497 501 def children (parent_id = 0, depth = 0):

+602/-581 M   freepost/database.py
index b853725..d1c0ef2
old size: 19K - new size: 21K
@@ -1,79 +1,101 @@
1 - import MySQLdb
1 + import hashlib
2 2 import re
3 + import sqlite3
3 4 from freepost import random, settings
4 5
5 - db = MySQLdb.connect (
6 - host = settings['mysql']['host'],
7 - port = settings['mysql']['port'],
8 - db = settings['mysql']['schema'],
9 - charset = settings['mysql']['charset'],
10 - user = settings['mysql']['username'],
11 - passwd = settings['mysql']['password'],
12 - autocommit = True)
6 + db = sqlite3.connect(settings['sqlite']['database'])
13 7
14 - # Since the "db" object is reused for the entire life of website,
15 - # this ping should avoid MySQL timing out if no requests are sent in a while.
16 - db.ping (True)
8 + # Returns SQLite rows as dictionaries instead of tuples.
9 + # https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.row_factory
10 + db.row_factory = sqlite3.Row
11 +
12 + # A custom function to compute SHA-512 because it's not built into SQLite
13 + db.create_function('SHA512', 1, lambda text:
14 + None if text is None else hashlib.sha512(text.encode('UTF-8')).hexdigest())
15 +
16 + # The REGEXP operator is a special syntax for the regexp() user function. No
17 + # regexp() user function is defined by default and so use of the REGEXP operator
18 + # will normally result in an error message. If an application-defined SQL
19 + # function named "regexp" is added at run-time, then the "X REGEXP Y" operator
20 + # will be implemented as a call to "regexp(Y,X)".
21 + db.create_function('REGEXP', 2, lambda pattern, string:
22 + re.match(pattern, string, flags=re.IGNORECASE) is not None)
17 23
18 24 # Store a new session_id for a user that has logged in
19 25 # The session token is stored in the user cookies during login, here
20 26 # we store the hash value of that token.
21 - def new_session (user_id, session_token):
22 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
23 -
24 - cursor.execute (
25 - """
26 - UPDATE user
27 - SET session = SHA2(%(session)s, 512)
28 - WHERE id = %(user)s
29 - """,
30 - {
31 - 'user': user_id,
32 - 'session': session_token
33 - }
34 - )
27 + def new_session(user_id, session_token):
28 + with db:
29 + db.execute(
30 + """
31 + UPDATE user
32 + SET session = SHA512(:session)
33 + WHERE id = :user
34 + """,
35 + {
36 + 'user': user_id,
37 + 'session': session_token
38 + }
39 + )
35 40
36 41 # Delete user session token on logout
37 42 def delete_session (user_id):
38 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
39 -
40 - cursor.execute (
41 - """
42 - UPDATE user
43 - SET session = NULL
44 - WHERE id = %(user)s
45 - """,
46 - {
47 - 'user': user_id
48 - }
49 - )
43 + with db:
44 + db.execute (
45 + """
46 + UPDATE user
47 + SET session = NULL
48 + WHERE id = :user
49 + """,
50 + {
51 + 'user': user_id
52 + }
53 + )
50 54
51 55 # Check user login credentials
52 56 #
53 57 # @return None if bad credentials, otherwise return the user
54 58 def check_user_credentials (username, password):
55 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
56 -
57 - cursor.execute (
58 - """
59 - SELECT *
60 - FROM user
61 - WHERE
62 - username = %(username)s AND
63 - password = SHA2(CONCAT(%(password)s, salt), 512) AND
64 - isActive = 1
65 - """,
66 - {
67 - 'username': username,
68 - 'password': password
69 - }
70 - )
71 -
72 - return cursor.fetchone ()
59 + with db:
60 + cursor = db.execute (
61 + """
62 + SELECT *
63 + FROM user
64 + WHERE username = :username
65 + AND password = SHA512(:password || salt)
66 + AND isActive = 1
67 + """,
68 + {
69 + 'username': username,
70 + 'password': password
71 + }
72 + )
73 +
74 + return cursor.fetchone ()
73 75
74 76 # Check if username exists
75 - def username_exists (username):
76 - return get_user_by_username (username) is not None
77 + def username_exists (username, case_sensitive = True):
78 + if not username:
79 + return None
80 +
81 + if case_sensitive:
82 + where = 'WHERE username = :username'
83 + else:
84 + where = 'WHERE LOWER(username) = LOWER(:username)'
85 +
86 + with db:
87 + cursor = db.execute(
88 + """
89 + SELECT *
90 + FROM user
91 + """ +
92 + where,
93 + {
94 + 'username': username
95 + }
96 + )
97 +
98 + return cursor.fetchone() is not None
77 99
78 100 # Create new user account
79 101 def new_user (username, password):
@@ -84,20 +106,19 @@ def new_user (username, password):
84 106 salt = random.ascii_string (16)
85 107
86 108 # Add user to database
87 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
88 -
89 - cursor.execute (
90 - """
91 - INSERT INTO user (hashId, isActive, password, registered, salt, username)
92 - VALUES (%(hash_id)s, 1, SHA2(CONCAT(%(password)s, %(salt)s), 512), NOW(), %(salt)s, %(username)s)
93 - """,
94 - {
95 - 'hash_id': hash_id,
96 - 'password': password,
97 - 'salt': salt,
98 - 'username': username
99 - }
100 - )
109 + with db:
110 + db.execute (
111 + """
112 + INSERT INTO user (hashId, isActive, password, registered, salt, username)
113 + VALUES (:hash_id, 1, SHA512(:password || :salt), DATE(), :salt, :username)
114 + """,
115 + {
116 + 'hash_id': hash_id,
117 + 'password': password,
118 + 'salt': salt,
119 + 'username': username
120 + }
121 + )
101 122
102 123 # Check if session token exists
103 124 def is_valid_session (token):
@@ -105,57 +126,54 @@ def is_valid_session (token):
105 126
106 127 # Return the number of unread replies
107 128 def count_unread_messages (user_id):
108 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
109 -
110 - cursor.execute (
111 - """
112 - SELECT COUNT(1) as new_messages
113 - FROM comment
114 - WHERE parentUserId = %(user)s AND userId != %(user)s AND `read` = 0
115 - """,
116 - {
117 - 'user': user_id
118 - }
119 - )
129 + with db:
130 + cursor = db.execute (
131 + """
132 + SELECT COUNT(1) AS new_messages
133 + FROM comment
134 + WHERE parentUserId = :user AND userId != :user AND `read` = 0
135 + """,
136 + {
137 + 'user': user_id
138 + }
139 + )
120 140
121 - return cursor.fetchone ()['new_messages']
141 + return cursor.fetchone ()['new_messages']
122 142
123 143 # Retrieve a user
124 144 def get_user_by_username (username):
125 145 if not username:
126 146 return None
127 147
128 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
129 -
130 - cursor.execute (
131 - """
132 - SELECT *
133 - FROM user
134 - WHERE username = %(username)s
135 - """,
136 - {
137 - 'username': username
138 - }
139 - )
140 -
141 - return cursor.fetchone ()
148 + with db:
149 + cursor = db.execute(
150 + """
151 + SELECT *
152 + FROM user
153 + WHERE username = :username
154 + """,
155 + {
156 + 'username': username
157 + }
158 + )
159 +
160 + return cursor.fetchone()
142 161
143 162 # Retrieve a user from a session cookie
144 - def get_user_by_session_token (session_token):
145 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
146 -
147 - cursor.execute (
148 - """
149 - SELECT *
150 - FROM user
151 - WHERE session = SHA2(%(session)s, 512)
152 - """,
153 - {
154 - 'session': session_token
155 - }
156 - )
163 + def get_user_by_session_token(session_token):
164 + with db:
165 + cursor = db.execute(
166 + """
167 + SELECT *
168 + FROM user
169 + WHERE session = SHA512(:session)
170 + """,
171 + {
172 + 'session': session_token
173 + }
174 + )
157 175
158 - return cursor.fetchone ()
176 + return cursor.fetchone()
159 177
160 178 # Get posts by date (for homepage)
161 179 def get_posts (page = 0, session_user_id = None, sort = 'hot', topic = None):
@@ -165,179 +183,170 @@ def get_posts (page = 0, session_user_id = None, sort = 'hot', topic = None):
165 183 sort = 'ORDER BY P.dateCreated DESC, P.vote DESC, P.commentsCount DESC'
166 184
167 185 if topic:
168 - topic_name = 'WHERE T.name = %(topic)s'
186 + topic_name = 'WHERE T.name = :topic'
169 187 else:
170 188 topic_name = ''
171 189
172 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
173 -
174 - cursor.execute (
175 - """
176 - SELECT
177 - P.*,
178 - U.username,
179 - V.vote AS user_vote,
180 - GROUP_CONCAT(DISTINCT T.name ORDER BY T.name SEPARATOR " ") AS topics
181 - FROM post AS P
182 - JOIN user AS U ON P.userId = U.id
183 - LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = %(user)s
184 - LEFT JOIN topic as T ON T.post_id = P.id
185 - {topic}
186 - GROUP BY P.id
187 - {order}
188 - LIMIT %(limit)s
189 - OFFSET %(offset)s
190 - """.format (topic=topic_name, order=sort),
191 - {
192 - 'user': session_user_id,
193 - 'limit': settings['defaults']['items_per_page'],
194 - 'offset': page * settings['defaults']['items_per_page'],
195 - 'topic': topic
196 - }
197 - )
198 -
199 - return cursor.fetchall ()
190 + with db:
191 + cursor = db.execute (
192 + """
193 + SELECT P.*,
194 + U.username,
195 + V.vote AS user_vote,
196 + GROUP_CONCAT(T.name, " ") AS topics
197 + FROM post AS P
198 + JOIN user AS U ON P.userId = U.id
199 + LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = :user
200 + LEFT JOIN topic as T ON T.post_id = P.id
201 + {topic}
202 + GROUP BY P.id
203 + {order}
204 + LIMIT :limit
205 + OFFSET :offset
206 + """.format (topic=topic_name, order=sort),
207 + {
208 + 'user': session_user_id,
209 + 'limit': settings['defaults']['items_per_page'],
210 + 'offset': page * settings['defaults']['items_per_page'],
211 + 'topic': topic
212 + }
213 + )
214 +
215 + return cursor.fetchall ()
200 216
201 217 # Retrieve user's own posts
202 218 def get_user_posts (user_id):
203 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
204 -
205 - cursor.execute (
206 - """
207 - SELECT *
208 - FROM post
209 - WHERE userId = %(user)s
210 - ORDER BY created DESC
211 - LIMIT 50
212 - """,
213 - {
214 - 'user': user_id
215 - }
216 - )
217 -
218 - return cursor.fetchall ()
219 + with db:
220 + cursor = db.execute (
221 + """
222 + SELECT *
223 + FROM post
224 + WHERE userId = :user
225 + ORDER BY created DESC
226 + LIMIT 50
227 + """,
228 + {
229 + 'user': user_id
230 + }
231 + )
232 +
233 + return cursor.fetchall()
219 234
220 235 # Retrieve user's own comments
221 236 def get_user_comments (user_id):
222 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
223 -
224 - cursor.execute (
225 - """
226 - SELECT
227 - C.*,
228 - P.title AS postTitle,
229 - P.hashId AS postHashId
230 - FROM comment AS C
231 - JOIN post AS P ON P.id = C.postId
232 - WHERE C.userId = %(user)s
233 - ORDER BY C.created DESC
234 - LIMIT 50
235 - """,
236 - {
237 - 'user': user_id
238 - }
239 - )
240 -
241 - return cursor.fetchall ()
237 + with db:
238 + cursor = db.execute (
239 + """
240 + SELECT C.*,
241 + P.title AS postTitle,
242 + P.hashId AS postHashId
243 + FROM comment AS C
244 + JOIN post AS P ON P.id = C.postId
245 + WHERE C.userId = :user
246 + ORDER BY C.created DESC
247 + LIMIT 50
248 + """,
249 + {
250 + 'user': user_id
251 + }
252 + )
253 +
254 + return cursor.fetchall()
242 255
243 256 # Retrieve user's own replies to other people
244 257 def get_user_replies (user_id):
245 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
246 -
247 - cursor.execute (
248 - """
249 - SELECT
250 - C.*,
251 - P.title AS postTitle,
252 - P.hashId AS postHashId,
253 - U.username AS username
254 - FROM comment AS C
255 - JOIN post AS P ON P.id = C.postId
256 - JOIN user AS U ON U.id = C.userId
257 - WHERE C.parentUserId = %(user)s AND C.userId != %(user)s
258 - ORDER BY C.created DESC
259 - LIMIT 50
260 - """,
261 - {
262 - 'user': user_id
263 - }
264 - )
265 -
266 - return cursor.fetchall ()
258 + with db:
259 + cursor = db.execute(
260 + """
261 + SELECT C.*,
262 + P.title AS postTitle,
263 + P.hashId AS postHashId,
264 + U.username AS username
265 + FROM comment AS C
266 + JOIN post AS P ON P.id = C.postId
267 + JOIN user AS U ON U.id = C.userId
268 + WHERE C.parentUserId = :user AND C.userId != :user
269 + ORDER BY C.created DESC
270 + LIMIT 50
271 + """,
272 + {
273 + 'user': user_id
274 + }
275 + )
276 +
277 + return cursor.fetchall()
267 278
268 279 # Update user information
269 280 def update_user (user_id, about, email, email_notifications, preferred_feed):
270 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
271 -
272 - # Update user info, but not email address
273 - cursor.execute (
274 - """
275 - UPDATE user
276 - SET about = %(about)s,
277 - email_notifications = %(notifications)s,
278 - preferred_feed = %(preferred_feed)s
279 - WHERE id = %(user)s
280 - """,
281 - {
282 - 'about': about,
283 - 'notifications': email_notifications,
284 - 'user': user_id,
285 - 'preferred_feed': preferred_feed
286 - }
287 - )
281 + with db:
282 + # Update user info, but not email address
283 + db.execute(
284 + """
285 + UPDATE user
286 + SET about = :about,
287 + email_notifications = :notifications,
288 + preferred_feed = :preferred_feed
289 + WHERE id = :user
290 + """,
291 + {
292 + 'about': about,
293 + 'notifications': email_notifications,
294 + 'user': user_id,
295 + 'preferred_feed': preferred_feed
296 + }
297 + )
288 298
289 - # Update email address
290 - # IGNORE update if the email address is already specified. This is
291 - # necessary to avoid an "duplicate key" exception when updating value.
292 - cursor.execute (
293 - """
294 - UPDATE IGNORE user
295 - SET email = %(email)s
296 - WHERE id = %(user)s
297 - """,
298 - {
299 - 'email': email,
300 - 'user': user_id
301 - }
302 - )
299 + # Update email address. Convert all addresses to LOWER() case. This
300 + # prevents two users from using the same address with different case.
301 + # IGNORE update if the email address is already specified. This is
302 + # necessary to avoid an "duplicate key" exception when updating value.
303 + db.execute (
304 + """
305 + UPDATE OR IGNORE user
306 + SET email = LOWER(:email)
307 + WHERE id = :user
308 + """,
309 + {
310 + 'email': email,
311 + 'user': user_id
312 + }
313 + )
303 314
304 315 # Set user replies as read
305 316 def set_replies_as_read (user_id):
306 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
307 -
308 - cursor.execute (
309 - """
310 - UPDATE comment
311 - SET `read` = 1
312 - WHERE parentUserId = %(user)s AND `read` = 0
313 - """,
314 - {
315 - 'user': user_id
316 - }
317 - )
317 + with db:
318 + db.execute(
319 + """
320 + UPDATE comment
321 + SET `read` = 1
322 + WHERE parentUserId = :user AND `read` = 0
323 + """,
324 + {
325 + 'user': user_id
326 + }
327 + )
318 328
319 329 # Submit a new post/link
320 330 def new_post (title, link, text, user_id):
321 331 # Create a hash_id for the new post
322 332 hash_id = random.alphanumeric_string (10)
323 333
324 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
325 -
326 - cursor.execute (
327 - """
328 - INSERT INTO post (hashId, created, dateCreated, title,
329 - link, text, vote, commentsCount, userId)
330 - VALUES (%(hash_id)s, NOW(), CURDATE(), %(title)s, %(link)s,
331 - %(text)s, 0, 0, %(user)s)
332 - """,
333 - {
334 - 'hash_id': hash_id,
335 - 'title': title,
336 - 'link': link,
337 - 'text': text,
338 - 'user': user_id
339 - }
340 - )
334 + with db:
335 + db.execute(
336 + """
337 + INSERT INTO post (hashId, created, dateCreated, title,
338 + link, text, vote, commentsCount, userId)
339 + VALUES (:hash_id, DATETIME(), DATE(), :title, :link,
340 + :text, 0, 0, :user)
341 + """,
342 + {
343 + 'hash_id': hash_id,
344 + 'title': title,
345 + 'link': link,
346 + 'text': text,
347 + 'user': user_id
348 + }
349 + )
341 350
342 351 return hash_id
343 352
@@ -358,106 +367,110 @@ def replace_post_topics (post_id, topics = ''):
358 367 # Remove extra topics if the list is too long
359 368 topics = topics[:settings['defaults']['topics_per_post']]
360 369
361 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
362 -
363 - # First we delete the existing topics
364 - cursor.execute (
365 - """
366 - DELETE FROM topic WHERE post_id = %(post)s
367 - """,
368 - {
369 - 'post': post_id
370 - }
371 - )
372 -
373 - # Now insert the new topics
374 - cursor.executemany (
375 - """
376 - INSERT INTO topic (post_id, name)
377 - VALUES (%s, %s)
378 - """,
379 - [ (post_id, topic) for topic in topics ]
380 - )
370 + with db:
371 + # First we delete the existing topics
372 + db.execute (
373 + """
374 + DELETE
375 + FROM topic
376 + WHERE post_id = :post
377 + """,
378 + {
379 + 'post': post_id
380 + }
381 + )
382 +
383 + # Now insert the new topics.
384 + # IGNORE duplicates that trigger UNIQUE constraint.
385 + db.executemany (
386 + """
387 + INSERT OR IGNORE INTO topic (post_id, name)
388 + VALUES (?, ?)
389 + """,
390 + [ (post_id, topic) for topic in topics ]
391 + )
381 392
382 393 # Retrieve a post
383 394 def get_post (hash, session_user_id = None):
384 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
385 -
386 - cursor.execute (
387 - """
388 - SELECT P.*, U.username, V.vote AS user_vote
389 - FROM post AS P
390 - JOIN user AS U ON P.userId = U.id
391 - LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = %(user)s
392 - WHERE P.hashId = %(post)s
393 - """,
394 - {
395 - 'user': session_user_id,
396 - 'post': hash
397 - }
398 - )
399 -
400 - return cursor.fetchone ()
395 + with db:
396 + cursor = db.execute (
397 + """
398 + SELECT P.*,
399 + U.username,
400 + V.vote AS user_vote
401 + FROM post AS P
402 + JOIN user AS U ON P.userId = U.id
403 + LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = :user
404 + WHERE P.hashId = :post
405 + """,
406 + {
407 + 'user': session_user_id,
408 + 'post': hash
409 + }
410 + )
411 +
412 + return cursor.fetchone ()
401 413
402 414 # Update a post
403 415 def update_post (title, link, text, post_hash_id, user_id):
404 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
405 -
406 - cursor.execute (
407 - """
408 - UPDATE post
409 - SET title = %(title)s,
410 - link = %(link)s,
411 - text = %(text)s
412 - WHERE hashId = %(hash_id)s AND userId = %(user)s
413 - """,
414 - {
415 - 'title': title,
416 - 'link': link,
417 - 'text': text,
418 - 'hash_id': post_hash_id,
419 - 'user': user_id
420 - }
421 - )
416 + with db:
417 + db.execute (
418 + """
419 + UPDATE post
420 + SET title = :title,
421 + link = :link,
422 + text = :text
423 + WHERE hashId = :hash_id
424 + AND userId = :user
425 + """,
426 + {
427 + 'title': title,
428 + 'link': link,
429 + 'text': text,
430 + 'hash_id': post_hash_id,
431 + 'user': user_id
432 + }
433 + )
422 434
423 435 # Retrieve all comments for a specific post
424 436 def get_post_comments (post_id, session_user_id = None):
425 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
426 -
427 - cursor.execute (
428 - """
429 - SELECT C.*, U.username, V.vote AS user_vote
430 - FROM comment AS C
431 - JOIN user AS U ON C.userId = U.id
432 - LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = %(user)s
433 - WHERE C.postId = %(post)s
434 - ORDER BY C.vote DESC, C.created ASC
435 - """,
436 - {
437 - 'user': session_user_id,
438 - 'post': post_id
439 - }
440 - )
441 -
442 - return cursor.fetchall ()
437 + with db:
438 + cursor = db.execute (
439 + """
440 + SELECT C.*,
441 + U.username,
442 + V.vote AS user_vote
443 + FROM comment AS C
444 + JOIN user AS U ON C.userId = U.id
445 + LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = :user
446 + WHERE C.postId = :post
447 + ORDER BY C.vote DESC,
448 + C.created ASC
449 + """,
450 + {
451 + 'user': session_user_id,
452 + 'post': post_id
453 + }
454 + )
455 +
456 + return cursor.fetchall ()
443 457
444 458 # Retrieve all topics for a specific post
445 459 def get_post_topics (post_id):
446 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
447 -
448 - cursor.execute (
449 - """
450 - SELECT T.name
451 - FROM topic AS T
452 - WHERE T.post_id = %(post)s
453 - ORDER BY T.name ASC
454 - """,
455 - {
456 - 'post': post_id
457 - }
458 - )
459 -
460 - return cursor.fetchall ()
460 + with db:
461 + cursor = db.execute (
462 + """
463 + SELECT T.name
464 + FROM topic AS T
465 + WHERE T.post_id = :post
466 + ORDER BY T.name ASC
467 + """,
468 + {
469 + 'post': post_id
470 + }
471 + )
472 +
473 + return cursor.fetchall ()
461 474
462 475 # Submit a new comment to a post
463 476 def new_comment (comment_text, post_hash_id, user_id, parent_user_id = None, parent_comment_id = None):
@@ -467,170 +480,182 @@ def new_comment (comment_text, post_hash_id, user_id, parent_user_id = None, par
467 480 # Retrieve post
468 481 post = get_post (post_hash_id)
469 482
470 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
471 -
472 - cursor.execute (
473 - """
474 - INSERT INTO comment (hashId, created, dateCreated, `read`, text, vote,
475 - parentId, parentUserId, postId, userId)
476 - VALUES (%(hash_id)s, NOW(), CURDATE(), 0, %(text)s, 0, %(parent_id)s,
477 - %(parent_user_id)s, %(post_id)s, %(user)s)
478 - """,
479 - {
480 - 'hash_id': hash_id,
481 - 'text': comment_text,
482 - 'parent_id': parent_comment_id,
483 - 'parent_user_id': parent_user_id,
484 - 'post_id': post['id'],
485 - 'user': user_id
486 - }
487 - )
488 -
489 - # Increase comments count for post
490 - cursor.execute (
491 - """
492 - UPDATE post
493 - SET commentsCount = commentsCount + 1
494 - WHERE id = %(post)s
495 - """,
496 - {
497 - 'post': post['id']
498 - }
499 - )
483 + with db:
484 + db.execute (
485 + """
486 + INSERT INTO comment (hashId, created, dateCreated, `read`, text, vote,
487 + parentId, parentUserId, postId, userId)
488 + VALUES (:hash_id, DATETIME(), DATE(), 0, :text, 0, :parent_id,
489 + :parent_user_id, :post_id, :user)
490 + """,
491 + {
492 + 'hash_id': hash_id,
493 + 'text': comment_text,
494 + 'parent_id': parent_comment_id,
495 + 'parent_user_id': parent_user_id,
496 + 'post_id': post['id'],
497 + 'user': user_id
498 + }
499 + )
500 +
501 + # Increase comments count for post
502 + db.execute (
503 + """
504 + UPDATE post
505 + SET commentsCount = commentsCount + 1
506 + WHERE id = :post
507 + """,
508 + {
509 + 'post': post['id']
510 + }
511 + )
500 512
501 513 return hash_id
502 514
503 515 # Retrieve a single comment
504 516 def get_comment (hash_id, session_user_id = None):
505 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
506 -
507 - cursor.execute (
508 - """
509 - SELECT
510 - C.*,
511 - P.hashId AS postHashId,
512 - P.title AS postTitle,
513 - U.username,
514 - V.vote AS user_vote
515 - FROM comment AS C
516 - JOIN user AS U ON C.userId = U.id
517 - JOIN post AS P ON P.id = C.postId
518 - LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = %(user)s
519 - WHERE C.hashId = %(comment)s
520 - """,
521 - {
522 - 'user': session_user_id,
523 - 'comment': hash_id
524 - }
525 - )
526 -
527 - return cursor.fetchone ()
517 + with db:
518 + cursor = db.execute(
519 + """
520 + SELECT C.*,
521 + P.hashId AS postHashId,
522 + P.title AS postTitle,
523 + U.username,
524 + V.vote AS user_vote
525 + FROM comment AS C
526 + JOIN user AS U ON C.userId = U.id
527 + JOIN post AS P ON P.id = C.postId
528 + LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = :user
529 + WHERE C.hashId = :comment
530 + """,
531 + {
532 + 'user': session_user_id,
533 + 'comment': hash_id
534 + }
535 + )
536 +
537 + return cursor.fetchone()
528 538
529 539 # Retrieve last N newest comments
530 540 def get_latest_comments ():
531 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
532 -
533 - cursor.execute (
534 - """
535 - SELECT
536 - C.*,
537 - P.hashId AS postHashId,
538 - P.title AS postTitle,
539 - U.username
540 - FROM comment AS C
541 - JOIN user AS U ON C.userId = U.id
542 - JOIN post AS P ON P.id = C.postId
543 - ORDER BY C.id DESC
544 - LIMIT 50
545 - """,
546 - {
547 - }
548 - )
549 -
550 - return cursor.fetchall ()
541 + with db:
542 + cursor = db.execute (
543 + """
544 + SELECT C.*,
545 + P.hashId AS postHashId,
546 + P.title AS postTitle,
547 + U.username
548 + FROM comment AS C
549 + JOIN user AS U ON C.userId = U.id
550 + JOIN post AS P ON P.id = C.postId
551 + ORDER BY C.id DESC
552 + LIMIT 50
553 + """,
554 + {
555 + }
556 + )
557 +
558 + return cursor.fetchall ()
551 559
552 560 # Update a comment
553 561 def update_comment (text, comment_hash_id, user_id):
554 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
555 -
556 - cursor.execute (
557 - """
558 - UPDATE comment
559 - SET text = %(text)s
560 - WHERE hashId = %(comment)s AND userId = %(user)s
561 - """,
562 - {
563 - 'text': text,
564 - 'comment': comment_hash_id,
565 - 'user': user_id
566 - }
567 - )
562 + with db:
563 + db.execute (
564 + """
565 + UPDATE comment
566 + SET text = :text
567 + WHERE hashId = :comment AND userId = :user
568 + """,
569 + {
570 + 'text': text,
571 + 'comment': comment_hash_id,
572 + 'user': user_id
573 + }
574 + )
568 575
569 576 # Add or update vote to a post
570 577 def vote_post (post_id, user_id, vote):
571 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
572 -
573 - # Insert or update the user vote
574 - cursor.execute (
575 - """
576 - INSERT INTO vote_post (vote, datetime, postId, userId)
577 - VALUES (%(vote)s, NOW(), %(post)s, %(user)s)
578 - ON DUPLICATE KEY UPDATE
579 - vote = vote + %(vote)s,
580 - datetime = NOW()
581 - """,
582 - {
583 - 'vote': vote,
584 - 'post': post_id,
585 - 'user': user_id
586 - }
587 - )
588 -
589 - # Update vote counter for post
590 - cursor.execute (
591 - """
592 - UPDATE post
593 - SET vote = vote + %(vote)s
594 - WHERE id = %(post)s
595 - """,
596 - {
597 - 'vote': vote,
598 - 'post': post_id
599 - }
600 - )
578 + with db:
579 + # Create a new vote for this post, if one doesn't already exist
580 + db.execute(
581 + """
582 + INSERT OR IGNORE INTO vote_post (vote, datetime, postId, userId)
583 + VALUES (0, DATETIME(), :post, :user)
584 + """,
585 + {
586 + 'post': post_id,
587 + 'user': user_id
588 + }
589 + )
590 +
591 + # Update user vote (+1 or -1)
592 + db.execute(
593 + """
594 + UPDATE vote_post
595 + SET vote = vote + :vote
596 + WHERE postId = :post AND userId = :user
597 + """,
598 + {
599 + 'vote': vote,
600 + 'post': post_id,
601 + 'user': user_id
602 + }
603 + )
604 +
605 + # Update post's total
606 + db.execute (
607 + """
608 + UPDATE post
609 + SET vote = vote + :vote
610 + WHERE id = :post
611 + """,
612 + {
613 + 'vote': vote,
614 + 'post': post_id
615 + }
616 + )
601 617
602 618 # Add or update vote to a comment
603 619 def vote_comment (comment_id, user_id, vote):
604 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
605 -
606 - # Insert or update the user vote
607 - cursor.execute (
608 - """
609 - INSERT INTO vote_comment (vote, datetime, commentId, userId)
610 - VALUES (%(vote)s, NOW(), %(comment)s, %(user)s)
611 - ON DUPLICATE KEY UPDATE
612 - vote = vote + %(vote)s,
613 - datetime = NOW()
614 - """,
615 - {
616 - 'vote': vote,
617 - 'comment': comment_id,
618 - 'user': user_id
619 - }
620 - )
621 -
622 - # Update vote counter for comment
623 - cursor.execute (
624 - """
625 - UPDATE comment
626 - SET vote = vote + %(vote)s
627 - WHERE id = %(comment)s
628 - """,
629 - {
630 - 'vote': vote,
631 - 'comment': comment_id
632 - }
633 - )
620 + with db:
621 + # Create a new vote for this post, if one doesn't already exist
622 + db.execute (
623 + """
624 + INSERT INTO vote_comment (vote, datetime, commentId, userId)
625 + VALUES (0, DATETIME(), :comment, :user)
626 + """,
627 + {
628 + 'comment': comment_id,
629 + 'user': user_id
630 + }
631 + )
632 +
633 + # Update user vote (+1 or -1)
634 + db.execute (
635 + """
636 + UPDATE vote_comment
637 + SET vote = vote + :vote
638 + WHERE commentId = :comment AND userId = :user
639 + """,
640 + {
641 + 'vote': vote,
642 + 'comment': comment_id,
643 + 'user': user_id
644 + }
645 + )
646 +
647 + # Update comment's total
648 + db.execute (
649 + """
650 + UPDATE comment
651 + SET vote = vote + :vote
652 + WHERE id = :comment
653 + """,
654 + {
655 + 'vote': vote,
656 + 'comment': comment_id
657 + }
658 + )
634 659
635 660 # Search posts
636 661 def search (query, sort='newest', page=0):
@@ -643,114 +668,110 @@ def search (query, sort='newest', page=0):
643 668 if len (query) == 0:
644 669 return []
645 670
646 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
647 -
648 671 if sort == 'newest':
649 672 sort = 'P.created DESC'
650 673 if sort == 'points':
651 674 sort = 'P.vote DESC'
652 675
653 - cursor.execute (
654 - """
655 - SELECT P.*, U.username
656 - FROM post AS P
657 - JOIN user AS U ON P.userId = U.id
658 - WHERE P.title REGEXP %(query)s
659 - ORDER BY {sort}
660 - LIMIT %(limit)s
661 - OFFSET %(offset)s
662 - """.format (sort=sort),
663 - {
664 - 'query': query,
665 - 'sort': sort,
666 - 'limit': settings['defaults']['search_results_per_page'],
667 - 'offset': page * settings['defaults']['search_results_per_page']
668 - }
669 - )
670 -
671 - return cursor.fetchall ()
676 + with db:
677 + cursor = db.execute (
678 + """
679 + SELECT P.*,
680 + U.username
681 + FROM post AS P
682 + JOIN user AS U ON P.userId = U.id
683 + WHERE P.title REGEXP :query
684 + ORDER BY {sort}
685 + LIMIT :limit
686 + OFFSET :offset
687 + """.format (sort=sort),
688 + {
689 + 'query': query,
690 + 'sort': sort,
691 + 'limit': settings['defaults']['search_results_per_page'],
692 + 'offset': page * settings['defaults']['search_results_per_page']
693 + }
694 + )
695 +
696 + return cursor.fetchall ()
672 697
673 698 # Set reset token for user email
674 699 def set_password_reset_token (user_id = None, token = None):
675 700 if not user_id or not token:
676 701 return
677 702
678 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
679 -
680 - cursor.execute (
681 - """
682 - UPDATE user
683 - SET passwordResetToken = SHA2(%(token)s, 512),
684 - passwordResetTokenExpire = NOW() + INTERVAL 1 HOUR
685 - WHERE id = %(user)s
686 - """,
687 - {
688 - 'user': user_id,
689 - 'token': token
690 - }
691 - )
703 + with db:
704 + db.execute (
705 + """
706 + UPDATE user
707 + SET passwordResetToken = SHA512(:token),
708 + passwordResetTokenExpire = NOW() + INTERVAL 1 HOUR
709 + WHERE id = :user
710 + """,
711 + {
712 + 'user': user_id,
713 + 'token': token
714 + }
715 + )
692 716
693 717 # Delete the password reset token for a user
694 718 def delete_password_reset_token (user_id = None):
695 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
696 -
697 - cursor.execute (
698 - """
699 - UPDATE user
700 - SET passwordResetToken = NULL,
701 - passwordResetTokenExpire = NULL
702 - WHERE id = %(user)s
703 - """,
704 - {
705 - 'user': user_id
706 - }
707 - )
719 + with db:
720 + db.execute (
721 + """
722 + UPDATE user
723 + SET passwordResetToken = NULL,
724 + passwordResetTokenExpire = NULL
725 + WHERE id = :user
726 + """,
727 + {
728 + 'user': user_id
729 + }
730 + )
708 731
709 732 # Check if a reset token has expired.
710 733 def is_password_reset_token_valid (user_id = None):
711 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
712 -
713 - cursor.execute (
714 - """
715 - SELECT COUNT(1) AS valid
716 - FROM user
717 - WHERE id = %(user)s AND
718 - passwordResetToken IS NOT NULL AND
719 - passwordResetTokenExpire IS NOT NULL AND
720 - passwordResetTokenExpire > NOW()
721 - """,
722 - {
723 - 'user': user_id
724 - }
725 - )
726 -
727 - return cursor.fetchone ()['valid'] == 1
734 + with db:
735 + cursor = db.execute(
736 + """
737 + SELECT COUNT(1) AS valid
738 + FROM user
739 + WHERE id = :user
740 + AND passwordResetToken IS NOT NULL
741 + AND passwordResetTokenExpire IS NOT NULL
742 + AND passwordResetTokenExpire > DATE()
743 + """,
744 + {
745 + 'user': user_id
746 + }
747 + )
748 +
749 + return cursor.fetchone()['valid'] == 1
728 750
729 751 # Reset user password
730 752 def reset_password (username = None, email = None, new_password = None, secret_token = None):
731 753 if not new_password:
732 754 return
733 755
734 - cursor = db.cursor (MySQLdb.cursors.DictCursor)
735 -
736 - cursor.execute (
737 - """
738 - UPDATE user
739 - SET password = SHA2(CONCAT(%(password)s, `salt`), 512),
740 - passwordResetToken = NULL,
741 - passwordResetTokenExpire = NULL
742 - WHERE username = %(user)s AND
743 - email = %(email)s AND
744 - passwordResetToken = SHA2(%(token)s, 512) AND
745 - passwordResetTokenExpire > NOW()
746 - """,
747 - {
748 - 'password': new_password,
749 - 'user': username,
750 - 'email': email,
751 - 'token': secret_token
752 - }
753 - )
756 + with db:
757 + db.execute (
758 + """
759 + UPDATE user
760 + SET password = SHA512(:password || `salt`),
761 + passwordResetToken = NULL,
762 + passwordResetTokenExpire = NULL
763 + WHERE username = :user
764 + AND email = :email
765 + AND passwordResetToken = SHA512(:token)
766 + AND passwordResetTokenExpire > DATE()
767 + """,
768 + {
769 + 'password': new_password,
770 + 'user': username,
771 + 'email': email,
772 + 'token': secret_token
773 + }
774 + )
754 775
755 776
756 777

+1/-1 M   freepost/templates/posts.html
index 5871745..25a33ab
old size: 5K - new size: 5K
@@ -45,7 +45,7 @@
45 45
46 46 <em class="username">
47 47 <a href="{{ url ('post', hash_id=post.hashId) }}">
48 - <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}">
48 + <time title="{{ post.created|title }}" datetime="{{ post.created }}">
49 49 {{ post.created|ago }}
50 50 </time>
51 51 </a>

+0/-272 D   freepost_freepost.sql
index 13fcd26..0000000
old size: 8K - new size: 0B
deleted file mode: -rw-r--r--
@@ -1,272 +0,0 @@
1 - -- phpMyAdmin SQL Dump
2 - -- version 4.3.9
3 - -- http://www.phpmyadmin.net
4 - --
5 - -- Host: sql
6 - -- Generation Time: Mar 02, 2019 at 10:25 AM
7 - -- Server version: 5.5.60-0+deb8u1
8 - -- PHP Version: 5.6.38-0+deb8u1
9 -
10 - SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
11 - SET AUTOCOMMIT = 0;
12 - START TRANSACTION;
13 - SET time_zone = "+00:00";
14 -
15 -
16 - /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
17 - /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
18 - /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
19 - /*!40101 SET NAMES utf8 */;
20 -
21 - --
22 - -- Database: `freepost_freepost`
23 - --
24 - CREATE DATABASE IF NOT EXISTS `freepost_freepost` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
25 - USE `freepost_freepost`;
26 -
27 - -- --------------------------------------------------------
28 -
29 - --
30 - -- Table structure for table `comment`
31 - --
32 - -- Creation: Jul 03, 2017 at 03:39 PM
33 - --
34 -
35 - DROP TABLE IF EXISTS `comment`;
36 - CREATE TABLE IF NOT EXISTS `comment` (
37 - `id` int(11) NOT NULL,
38 - `hashId` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
39 - `created` datetime NOT NULL,
40 - `dateCreated` date NOT NULL,
41 - `read` tinyint(1) NOT NULL,
42 - `text` longtext COLLATE utf8_unicode_ci NOT NULL,
43 - `vote` int(11) NOT NULL,
44 - `parentId` int(11) DEFAULT NULL,
45 - `parentUserId` int(11) DEFAULT NULL,
46 - `postId` int(11) DEFAULT NULL,
47 - `userId` int(11) DEFAULT NULL
48 - ) ENGINE=InnoDB AUTO_INCREMENT=11235 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
49 -
50 - -- --------------------------------------------------------
51 -
52 - --
53 - -- Table structure for table `post`
54 - --
55 - -- Creation: Nov 25, 2017 at 04:35 AM
56 - --
57 -
58 - DROP TABLE IF EXISTS `post`;
59 - CREATE TABLE IF NOT EXISTS `post` (
60 - `id` int(11) NOT NULL,
61 - `hashId` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
62 - `created` datetime NOT NULL,
63 - `dateCreated` date NOT NULL,
64 - `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
65 - `link` text COLLATE utf8_unicode_ci,
66 - `text` longtext COLLATE utf8_unicode_ci NOT NULL,
67 - `vote` int(11) NOT NULL,
68 - `commentsCount` int(11) NOT NULL,
69 - `userId` int(11) DEFAULT NULL
70 - ) ENGINE=InnoDB AUTO_INCREMENT=13821 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
71 -
72 - -- --------------------------------------------------------
73 -
74 - --
75 - -- Table structure for table `remember_me`
76 - --
77 - -- Creation: Jul 03, 2017 at 03:39 PM
78 - --
79 -
80 - DROP TABLE IF EXISTS `remember_me`;
81 - CREATE TABLE IF NOT EXISTS `remember_me` (
82 - `token` char(128) COLLATE utf8_unicode_ci NOT NULL,
83 - `userId` int(10) NOT NULL,
84 - `expires` datetime NOT NULL
85 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Used for user authentication (keep session alive for returning user)';
86 -
87 - -- --------------------------------------------------------
88 -
89 - --
90 - -- Table structure for table `topic`
91 - --
92 - -- Creation: Jul 17, 2018 at 05:50 AM
93 - --
94 -
95 - DROP TABLE IF EXISTS `topic`;
96 - CREATE TABLE IF NOT EXISTS `topic` (
97 - `post_id` int(11) NOT NULL,
98 - `name` varchar(45) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Topic name'
99 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
100 -
101 - -- --------------------------------------------------------
102 -
103 - --
104 - -- Table structure for table `user`
105 - --
106 - -- Creation: Jan 11, 2019 at 08:29 PM
107 - --
108 -
109 - DROP TABLE IF EXISTS `user`;
110 - CREATE TABLE IF NOT EXISTS `user` (
111 - `id` int(11) NOT NULL,
112 - `hashId` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
113 - `email` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
114 - `email_notifications` tinyint(1) unsigned NOT NULL DEFAULT '1',
115 - `isActive` tinyint(1) NOT NULL,
116 - `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
117 - `passwordResetToken` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
118 - `passwordResetTokenExpire` datetime DEFAULT NULL,
119 - `registered` datetime NOT NULL,
120 - `salt` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
121 - `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
122 - `about` varchar(10000) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
123 - `session` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Stores the hash value of the session_id random string that was assigned to a user (and saved to a cookie) when logging in.',
124 - `preferred_feed` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'hot'
125 - ) ENGINE=InnoDB AUTO_INCREMENT=341 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
126 -
127 - -- --------------------------------------------------------
128 -
129 - --
130 - -- Table structure for table `vote_comment`
131 - --
132 - -- Creation: Jul 03, 2017 at 03:39 PM
133 - --
134 -
135 - DROP TABLE IF EXISTS `vote_comment`;
136 - CREATE TABLE IF NOT EXISTS `vote_comment` (
137 - `vote` smallint(6) NOT NULL,
138 - `datetime` datetime NOT NULL,
139 - `commentId` int(11) NOT NULL,
140 - `userId` int(11) NOT NULL
141 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
142 -
143 - -- --------------------------------------------------------
144 -
145 - --
146 - -- Table structure for table `vote_post`
147 - --
148 - -- Creation: Jul 03, 2017 at 03:39 PM
149 - --
150 -
151 - DROP TABLE IF EXISTS `vote_post`;
152 - CREATE TABLE IF NOT EXISTS `vote_post` (
153 - `vote` smallint(6) NOT NULL,
154 - `datetime` datetime NOT NULL,
155 - `postId` int(11) NOT NULL,
156 - `userId` int(11) NOT NULL
157 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
158 -
159 - --
160 - -- Indexes for dumped tables
161 - --
162 -
163 - --
164 - -- Indexes for table `comment`
165 - --
166 - ALTER TABLE `comment`
167 - ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `hashId` (`hashId`), ADD KEY `IDX_9474526C10EE4CEE` (`parentId`), ADD KEY `IDX_9474526C251330C5` (`parentUserId`), ADD KEY `IDX_9474526CE094D20D` (`postId`), ADD KEY `IDX_9474526C64B64DCC` (`userId`), ADD KEY `created` (`created`), ADD KEY `dateCreated` (`dateCreated`), ADD KEY `isRead` (`read`), ADD KEY `vote` (`vote`);
168 -
169 - --
170 - -- Indexes for table `post`
171 - --
172 - ALTER TABLE `post`
173 - ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `hashId` (`hashId`), ADD KEY `IDX_5A8A6C8D64B64DCC` (`userId`), ADD KEY `created` (`created`), ADD KEY `dateCreated` (`dateCreated`), ADD KEY `vote` (`vote`);
174 -
175 - --
176 - -- Indexes for table `remember_me`
177 - --
178 - ALTER TABLE `remember_me`
179 - ADD PRIMARY KEY (`token`), ADD KEY `userId` (`userId`);
180 -
181 - --
182 - -- Indexes for table `topic`
183 - --
184 - ALTER TABLE `topic`
185 - ADD PRIMARY KEY (`post_id`,`name`);
186 -
187 - --
188 - -- Indexes for table `user`
189 - --
190 - ALTER TABLE `user`
191 - ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `hashId` (`hashId`), ADD UNIQUE KEY `username` (`username`), ADD UNIQUE KEY `email` (`email`), ADD UNIQUE KEY `passwordResetCode` (`passwordResetToken`), ADD UNIQUE KEY `session` (`session`);
192 -
193 - --
194 - -- Indexes for table `vote_comment`
195 - --
196 - ALTER TABLE `vote_comment`
197 - ADD PRIMARY KEY (`commentId`,`userId`), ADD KEY `IDX_1FC60DF46690C3F5` (`commentId`), ADD KEY `IDX_1FC60DF464B64DCC` (`userId`);
198 -
199 - --
200 - -- Indexes for table `vote_post`
201 - --
202 - ALTER TABLE `vote_post`
203 - ADD PRIMARY KEY (`postId`,`userId`), ADD KEY `IDX_EDE89DBCE094D20D` (`postId`), ADD KEY `IDX_EDE89DBC64B64DCC` (`userId`);
204 -
205 - --
206 - -- AUTO_INCREMENT for dumped tables
207 - --
208 -
209 - --
210 - -- AUTO_INCREMENT for table `comment`
211 - --
212 - ALTER TABLE `comment`
213 - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=11235;
214 - --
215 - -- AUTO_INCREMENT for table `post`
216 - --
217 - ALTER TABLE `post`
218 - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=13821;
219 - --
220 - -- AUTO_INCREMENT for table `user`
221 - --
222 - ALTER TABLE `user`
223 - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=341;
224 - --
225 - -- Constraints for dumped tables
226 - --
227 -
228 - --
229 - -- Constraints for table `comment`
230 - --
231 - ALTER TABLE `comment`
232 - ADD CONSTRAINT `FK_9474526C10EE4CEE` FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`),
233 - ADD CONSTRAINT `FK_9474526C251330C5` FOREIGN KEY (`parentUserId`) REFERENCES `user` (`id`),
234 - ADD CONSTRAINT `FK_9474526C64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
235 - ADD CONSTRAINT `FK_9474526CE094D20D` FOREIGN KEY (`postId`) REFERENCES `post` (`id`);
236 -
237 - --
238 - -- Constraints for table `post`
239 - --
240 - ALTER TABLE `post`
241 - ADD CONSTRAINT `FK_5A8A6C8D64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`);
242 -
243 - --
244 - -- Constraints for table `remember_me`
245 - --
246 - ALTER TABLE `remember_me`
247 - ADD CONSTRAINT `FK_userId` FOREIGN KEY (`userId`) REFERENCES `user` (`id`);
248 -
249 - --
250 - -- Constraints for table `topic`
251 - --
252 - ALTER TABLE `topic`
253 - ADD CONSTRAINT `fk_topic_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
254 -
255 - --
256 - -- Constraints for table `vote_comment`
257 - --
258 - ALTER TABLE `vote_comment`
259 - ADD CONSTRAINT `FK_1FC60DF464B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
260 - ADD CONSTRAINT `FK_1FC60DF46690C3F5` FOREIGN KEY (`commentId`) REFERENCES `comment` (`id`);
261 -
262 - --
263 - -- Constraints for table `vote_post`
264 - --
265 - ALTER TABLE `vote_post`
266 - ADD CONSTRAINT `FK_EDE89DBC64B64DCC` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
267 - ADD CONSTRAINT `FK_EDE89DBCE094D20D` FOREIGN KEY (`postId`) REFERENCES `post` (`id`);
268 - COMMIT;
269 -
270 - /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
271 - /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
272 - /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

+2/-1 M   requirements.txt
index b0bd0eb..d565677
old size: 71B - new size: 85B
@@ -1,9 +1,10 @@
1 1 bleach
2 2 bottle
3 + python-dateutil
3 4 jinja2
4 5 markdown
5 - mysqlclient
6 6 pyld
7 + pysqlite3
7 8 pyyaml
8 9 requests
9 10 timeago

+2/-8 M   settings.yaml
index b46a8ab..7311c09
old size: 941B - new size: 822B
@@ -12,14 +12,8 @@ cookies:
12 12 # Used to verify that cookies haven't been tampered with.
13 13 secret: "secret random string"
14 14
15 - mysql:
16 - host: localhost
17 - port: 3306
18 - schema: freepost_freepost
19 - # charset: utf8mb4
20 - charset: utf8
21 - username: freepost
22 - password: freepost
15 + sqlite:
16 + database: ./database.sqlite
23 17
24 18 # Emails are sent using the local sendmail MTA.
25 19 sendmail: