Author | zPlus <zplus@peers.community> 2018-07-16 20:27:20 |
Committer | zPlus <zplus@peers.community> 2018-07-16 20:27:20 |
Commit | be63132 (patch) |
Tree | ac401d6 |
Parent(s) |
index 541ca10..4b9d04d | |||
old size: 34B - new size: 74B | |||
@@ -1,3 +1,6 @@ | |||
1 | - | __pycache__ | |
2 | - | cache/template/* | |
3 | - | venv | |
1 | + | __pycache__/ | |
2 | + | venv/ | |
3 | + | *.pyc | |
4 | + | ||
5 | + | /freepost/static/css/ | |
6 | + | /settings.production.yaml |
index 0000000..dd590bc | |||
old size: 0B - new size: 849B | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,29 @@ | |||
1 | + | Options +ExecCGI | |
2 | + | # AddHandler cgi-script .cgi | |
3 | + | ||
4 | + | # Other useful info for deploying on TuxFamily | |
5 | + | # https://faq.tuxfamily.org/WebArea/En#How_to_play_with_types_and_handlers | |
6 | + | ||
7 | + | Options +FollowSymLinks | |
8 | + | Options -MultiViews | |
9 | + | RewriteEngine On | |
10 | + | RewriteBase / | |
11 | + | ||
12 | + | # Redirect everything to HTTPS | |
13 | + | RewriteCond %{HTTPS} off | |
14 | + | RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] | |
15 | + | ||
16 | + | # Remove www | |
17 | + | RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] | |
18 | + | RewriteRule ^(.*)$ http://%1%{REQUEST_URI} [R=301,QSA,NC,L] | |
19 | + | ||
20 | + | # Remove trailing slashes | |
21 | + | # Do we need this rule? It's probably better not to use it, as it | |
22 | + | # could interfere with any regex match in some topic. | |
23 | + | # RewriteCond %{REQUEST_FILENAME} !-d | |
24 | + | # RewriteRule ^(.*)/+$ /$1 [NC,L,R=301,QSA] | |
25 | + | ||
26 | + | # Redirect all requests to CGI script | |
27 | + | RewriteEngine On | |
28 | + | RewriteCond %{REQUEST_FILENAME} !-f | |
29 | + | RewriteRule ^(.*)$ freepost.cgi/$1 [L] |
index 0000000..03bae46 | |||
old size: 0B - new size: 654B | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,22 @@ | |||
1 | + | # The Python interpreter option for the webserver. Use virtualenv. | |
2 | + | PassengerPython "./venv/bin/python3" | |
3 | + | ||
4 | + | Options +FollowSymLinks | |
5 | + | Options -MultiViews | |
6 | + | RewriteEngine On | |
7 | + | RewriteBase / | |
8 | + | ||
9 | + | # Redirect everything to HTTPS | |
10 | + | RewriteCond %{HTTPS} off | |
11 | + | RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] | |
12 | + | ||
13 | + | # Remove www | |
14 | + | RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] | |
15 | + | RewriteRule ^(.*)$ http://%1%{REQUEST_URI} [R=301,QSA,NC,L] | |
16 | + | ||
17 | + | # Remove trailing slashes | |
18 | + | # Do we need this rule? It's probably better not to use it, as it | |
19 | + | # could interfere with any regex match in some topic. | |
20 | + | # RewriteCond %{REQUEST_FILENAME} !-d | |
21 | + | # RewriteRule ^(.*)/+$ /$1 [NC,L,R=301,QSA] | |
22 | + |
index 82f84bd..ac6ecd1 | |||
old size: 1K - new size: 963B | |||
@@ -1,40 +1,34 @@ | |||
1 | - | # FreePost | |
1 | + | # freepost | |
2 | 2 | ||
3 | - | This is the code powering [freepost](http://freepo.st). FreePost is a web-based | |
4 | - | discussion board that allows users to post text and links which other users may | |
5 | - | read and comment on (start a discussion). It also supports upvoting and downvoting | |
6 | - | of posts and has some nifty features to display the so-called **Hot** posts (those | |
7 | - | that are very popular and have been upvoted a lot) as well as the newest posts, | |
8 | - | aptly named **New**. Each user has a profile page which includes some information | |
9 | - | on themselves. | |
3 | + | This is the code powering [freepost](http://freepost.peers.community), a free | |
4 | + | discussion board that allows users to post text and links that other | |
5 | + | users can read and comment. | |
10 | 6 | ||
11 | - | ## Development | |
7 | + | # Development | |
12 | 8 | ||
13 | - | ### Setup Python3 virtual environment | |
9 | + | ## Setup Python3 virtual environment | |
14 | 10 | ||
15 | 11 | mkdir venv | |
16 | 12 | virtualenv -p python3 venv | |
17 | 13 | -> alternative: python3 -m venv venv | |
18 | - | source ./venv/bin/activate | |
19 | - | pip3 install --no-binary :all: -r requirements.txt | |
14 | + | source venv/bin/activate | |
15 | + | pip3 install -r requirements.txt | |
20 | 16 | ||
21 | - | ### Run | |
17 | + | ## Run dev server | |
22 | 18 | ||
23 | - | source ./venv/bin/activate | |
19 | + | source venv/bin/activate | |
24 | 20 | python3 -m bottle --debug --reload --bind 127.0.0.1:8000 freepost | |
25 | 21 | ||
22 | + | ## Build stylesheets | |
26 | 23 | ||
27 | - | ### Stylesheets | |
24 | + | Build CSS files | |
28 | 25 | ||
29 | - | Sources are in `css/`. They are compiled using Stylus. Run this command from | |
30 | - | the project's root: | |
26 | + | stylus --watch --compress --disable-cache --out freepost/static/css/ freepost/static/stylus/freepost.styl | |
31 | 27 | ||
32 | - | stylus --watch --compress --disable-cache --out css/ css/ | |
28 | + | ## Deploy | |
33 | 29 | ||
34 | - | ## Contacts | |
35 | - | ||
36 | - | If you have any questions please get in contact with us at | |
37 | - | [freepost](http://freepo.st). | |
30 | + | - Rename `.htaccess.wsgi` or `.htaccess.cgi` to `.htaccess` | |
31 | + | - Change settings in `settings.yaml` | |
38 | 32 | ||
39 | 33 | ## License | |
40 | 34 |
index 0000000..6047e4f | |||
old size: 0B - new size: 76B | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,4 @@ | |||
1 | + | #!./venv/bin/python3 | |
2 | + | ||
3 | + | from freepost import bottle | |
4 | + | bottle.run (server='cgi') |
index e1e5e22..7319e2e | |||
old size: 495B - new size: 18K | |||
old mode: -rw-r--r--
new mode: -rwxr-xr-x |
@@ -1,22 +1,628 @@ | |||
1 | + | #!./venv/bin/python3 | |
2 | + | ||
3 | + | import bleach | |
1 | 4 | import bottle | |
2 | 5 | import configparser | |
3 | 6 | import functools | |
4 | 7 | import importlib | |
5 | 8 | import json | |
6 | - | from bottle import abort, get, post, redirect, request | |
9 | + | import markdown | |
10 | + | import timeago | |
11 | + | import urllib3 | |
12 | + | import yaml | |
13 | + | from datetime import datetime, timezone | |
14 | + | from bottle import abort, get, jinja2_template as template, post, redirect, request, response, static_file | |
7 | 15 | from urllib.parse import urlparse | |
8 | 16 | ||
9 | - | # This is used to export the bottle object for the WSGI server | |
17 | + | # This is used to export the bottle object for a WSGI server | |
10 | 18 | # See passenger_wsgi.py | |
11 | 19 | application = bottle.app () | |
12 | 20 | ||
13 | - | # Load settings for this app | |
14 | - | application.config.load_config ('settings.ini') | |
21 | + | # Load user settings for this app | |
22 | + | with open ('settings.yaml', encoding='UTF-8') as file: | |
23 | + | settings = yaml.load (file) | |
15 | 24 | ||
16 | - | # Default app templates | |
25 | + | # Directories to search for app templates | |
17 | 26 | bottle.TEMPLATE_PATH = [ './freepost/templates' ] | |
18 | 27 | ||
28 | + | # Custom settings and functions for templates | |
29 | + | template = functools.partial ( | |
30 | + | template, | |
31 | + | template_settings = { | |
32 | + | 'filters': { | |
33 | + | 'ago': lambda date: timeago.format (date), | |
34 | + | 'datetime': lambda date: date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'), | |
35 | + | 'title': lambda date: date.strftime ('%b %-d, %Y - %H:%M%z%Z'), | |
36 | + | # Convert markdown to html | |
37 | + | 'md2html': lambda text: bleach.clean (markdown.markdown ( | |
38 | + | text, | |
39 | + | # https://python-markdown.github.io/extensions/ | |
40 | + | extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ], | |
41 | + | output_format='html5')), | |
42 | + | # Get the domain part of a URL | |
43 | + | 'netloc': lambda url: urlparse (url).netloc | |
44 | + | }, | |
45 | + | 'globals': { | |
46 | + | 'new_messages': lambda user_id: database.count_unread_messages (user_id), | |
47 | + | 'now': lambda: datetime.now (timezone.utc), | |
48 | + | 'url': application.get_url, | |
49 | + | 'session_user': lambda: session.user () | |
50 | + | }, | |
51 | + | 'autoescape': True | |
52 | + | }) | |
53 | + | ||
54 | + | # "bleach" library is used to sanitize the HTML output of jinja2's "md2html" | |
55 | + | # filter. The library has only a very restrictive list of white-listed | |
56 | + | # tags, so we add some more here. | |
57 | + | bleach.sanitizer.ALLOWED_TAGS += [ 'p', 'pre', 'img' ] | |
58 | + | bleach.sanitizer.ALLOWED_ATTRIBUTES.update ({ | |
59 | + | 'img': [ 'src' ] | |
60 | + | }) | |
61 | + | ||
62 | + | from freepost import database, session | |
63 | + | ||
64 | + | # Decorator. | |
65 | + | # Make sure user is logged in | |
66 | + | def requires_login (controller): | |
67 | + | def wrapper (): | |
68 | + | session_token = request.get_cookie ( | |
69 | + | key = settings['session']['name'], | |
70 | + | secret = settings['cookies']['secret']) | |
71 | + | ||
72 | + | if database.is_valid_session (session_token): | |
73 | + | return controller () | |
74 | + | else: | |
75 | + | redirect (application.get_url ('login')) | |
76 | + | ||
77 | + | return wrapper | |
78 | + | ||
79 | + | # Decorator. | |
80 | + | # Make sure user is logged out | |
81 | + | def requires_logout (controller): | |
82 | + | def wrapper (): | |
83 | + | session_token = request.get_cookie ( | |
84 | + | key = settings['session']['name'], | |
85 | + | secret = settings['cookies']['secret']) | |
86 | + | ||
87 | + | if database.is_valid_session (session_token): | |
88 | + | redirect (application.get_url ('user')) | |
89 | + | else: | |
90 | + | return controller () | |
91 | + | ||
92 | + | return wrapper | |
93 | + | ||
94 | + | # Routes | |
95 | + | ||
96 | + | @get ('/', name='homepage') | |
97 | + | def homepage (): | |
98 | + | # Page number | |
99 | + | page = int (request.query.page or 0) | |
100 | + | ||
101 | + | if page < 0: | |
102 | + | redirect (application.get_url ('homepage')) | |
103 | + | ||
104 | + | user = session.user () | |
105 | + | posts = database.get_hot_posts (page, user['id'] if user else None) | |
106 | + | ||
107 | + | return template ( | |
108 | + | 'homepage.html', | |
109 | + | page_number=page, | |
110 | + | posts=posts, | |
111 | + | sorting='hot') | |
112 | + | ||
113 | + | @get ('/new', name='new') | |
114 | + | def new (): | |
115 | + | # Page number | |
116 | + | page = int (request.query.page or 0) | |
117 | + | ||
118 | + | if page < 0: | |
119 | + | redirect (application.get_url ('homepage')) | |
120 | + | ||
121 | + | user = session.user () | |
122 | + | posts = database.get_new_posts (page, user['id'] if user else None) | |
123 | + | ||
124 | + | return template ( | |
125 | + | 'homepage.html', | |
126 | + | page_number=page, | |
127 | + | posts=posts, | |
128 | + | sorting='new') | |
129 | + | ||
130 | + | @get ('/about', name='about') | |
131 | + | def about (): | |
132 | + | return template ('about.html') | |
133 | + | ||
134 | + | @get ('/login', name='login') | |
135 | + | @requires_logout | |
136 | + | def login (): | |
137 | + | return template ('login.html') | |
138 | + | ||
139 | + | @post ('/login') | |
140 | + | @requires_logout | |
141 | + | def login_check (): | |
142 | + | username = request.forms.get ('username') | |
143 | + | password = request.forms.get ('password') | |
144 | + | remember = 'remember' in request.forms | |
145 | + | ||
146 | + | if not username or not password: | |
147 | + | return template ( | |
148 | + | 'login.html', | |
149 | + | flash = 'Bad login!') | |
150 | + | ||
151 | + | # Check if user exists | |
152 | + | user = database.check_user_credentials (username, password) | |
153 | + | ||
154 | + | # Username/Password not working | |
155 | + | if not user: | |
156 | + | return template ( | |
157 | + | 'login.html', | |
158 | + | flash = 'Bad login!') | |
159 | + | ||
160 | + | session.start (user['id'], remember) | |
161 | + | ||
162 | + | redirect (application.get_url ('homepage')) | |
163 | + | ||
164 | + | @get ('/register', name='register') | |
165 | + | @requires_logout | |
166 | + | def register (): | |
167 | + | return template ('register.html') | |
168 | + | ||
169 | + | @post ('/register') | |
170 | + | @requires_logout | |
171 | + | def register_new_account (): | |
172 | + | username = request.forms.get ('username') | |
173 | + | password = request.forms.get ('password') | |
174 | + | ||
175 | + | # Normalize username | |
176 | + | username = username.strip () | |
177 | + | ||
178 | + | if len (username) == 0 or database.username_exists (username): | |
179 | + | return template ( | |
180 | + | 'register.html', | |
181 | + | flash='Name taken, please choose another.') | |
182 | + | ||
183 | + | # Password too short? | |
184 | + | if len (password) < 8: | |
185 | + | return template ( | |
186 | + | 'register.html', | |
187 | + | flash = 'Password too short') | |
188 | + | ||
189 | + | # Username OK, Password OK: create new user | |
190 | + | database.new_user (username, password) | |
191 | + | ||
192 | + | # Retrieve user (check if it was created) | |
193 | + | user = database.check_user_credentials (username, password) | |
194 | + | ||
195 | + | # Something bad happened... | |
196 | + | if user is None: | |
197 | + | return template ( | |
198 | + | 'register.html', | |
199 | + | flash = 'An error has occurred, please try again.') | |
200 | + | ||
201 | + | # Start session... | |
202 | + | session.start (user['id']) | |
203 | + | ||
204 | + | # ... and go to the homepage of the new user | |
205 | + | redirect (application.get_url ('user')) | |
206 | + | ||
207 | + | @get ('/logout', name='logout') | |
208 | + | @requires_login | |
209 | + | def logout (): | |
210 | + | session.close () | |
211 | + | ||
212 | + | redirect (application.get_url ('homepage')) | |
213 | + | ||
214 | + | @get ('/user', name='user') | |
215 | + | @requires_login | |
216 | + | def user_private_homepage (): | |
217 | + | return template ('user_private_homepage.html') | |
218 | + | ||
219 | + | @post ('/user') | |
220 | + | @requires_login | |
221 | + | def update_user (): | |
222 | + | user = session.user () | |
223 | + | ||
224 | + | about = request.forms.get ('about') | |
225 | + | email = request.forms.get ('email') | |
226 | + | ||
227 | + | if about is None or email is None: | |
228 | + | redirect (application.get_url ('user')) | |
229 | + | ||
230 | + | database.update_user (user['id'], about, email, False) | |
231 | + | ||
232 | + | redirect (application.get_url ('user')) | |
233 | + | ||
234 | + | @get ('/user_activity/posts') | |
235 | + | @requires_login | |
236 | + | def user_posts (): | |
237 | + | user = session.user () | |
238 | + | posts = database.get_user_posts (user['id']) | |
239 | + | ||
240 | + | return template ('user_posts.html', posts=posts) | |
241 | + | ||
242 | + | @get ('/user_activity/comments') | |
243 | + | @requires_login | |
244 | + | def user_comments (): | |
245 | + | user = session.user () | |
246 | + | comments = database.get_user_comments (user['id']) | |
247 | + | ||
248 | + | return template ('user_comments.html', comments=comments) | |
249 | + | ||
250 | + | @get ('/user_activity/replies') | |
251 | + | @requires_login | |
252 | + | def user_replies (): | |
253 | + | user = session.user () | |
254 | + | replies = database.get_user_replies (user['id']) | |
255 | + | ||
256 | + | database.set_replies_as_read (user['id']) | |
257 | + | ||
258 | + | return template ('user_replies.html', replies=replies) | |
259 | + | ||
260 | + | @get ('/user/<username>', name='user_public') | |
261 | + | def user_public_homepage (username): | |
262 | + | account = database.get_user_by_username (username) | |
263 | + | ||
264 | + | if account is None: | |
265 | + | redirect (application.get_url ('user')) | |
266 | + | ||
267 | + | return template ('user_public_homepage.html', account=account) | |
268 | + | ||
269 | + | @get ('/post/<hash_id>', name='post') | |
270 | + | def post_thread (hash_id): | |
271 | + | user = session.user () | |
272 | + | post = database.get_post (hash_id, user['id'] if user else None) | |
273 | + | comments = database.get_post_comments (post['id'], user['id'] if user else None) | |
274 | + | ||
275 | + | # Group comments by parent | |
276 | + | comments_tree = {} | |
277 | + | ||
278 | + | for comment in comments: | |
279 | + | if comment['parentId'] is None: | |
280 | + | if 0 not in comments_tree: | |
281 | + | comments_tree[0] = [] | |
282 | + | ||
283 | + | comments_tree[0].append (comment) | |
284 | + | else: | |
285 | + | if comment['parentId'] not in comments_tree: | |
286 | + | comments_tree[comment['parentId']] = [] | |
287 | + | ||
288 | + | comments_tree[comment['parentId']].append (comment) | |
289 | + | ||
290 | + | # Build ordered list of comments (recourse tree) | |
291 | + | def children (parent_id = 0, depth = 0): | |
292 | + | temp_comments = [] | |
293 | + | ||
294 | + | if parent_id in comments_tree: | |
295 | + | for comment in comments_tree[parent_id]: | |
296 | + | comment['depth'] = depth | |
297 | + | temp_comments.append (comment) | |
298 | + | ||
299 | + | temp_comments.extend (children (comment['id'], depth + 1)) | |
300 | + | ||
301 | + | return temp_comments | |
302 | + | ||
303 | + | comments = children () | |
304 | + | ||
305 | + | return template ( | |
306 | + | 'post.html', | |
307 | + | post=post, | |
308 | + | comments=comments, | |
309 | + | votes = { | |
310 | + | 'post': {}, | |
311 | + | 'comment': {} | |
312 | + | }) | |
313 | + | ||
314 | + | @requires_login | |
315 | + | @post ('/post/<hash_id>') | |
316 | + | def new_comment (hash_id): | |
317 | + | user = session.user () | |
318 | + | ||
319 | + | # The comment text | |
320 | + | comment = request.forms.get ('new_comment').strip () | |
321 | + | ||
322 | + | # Empty comment | |
323 | + | if len (comment) == 0: | |
324 | + | redirect (application.get_url ('post', hash_id=hash_id)) | |
325 | + | ||
326 | + | comment_hash_id = database.new_comment (comment, hash_id, user['id']) | |
327 | + | ||
328 | + | # Retrieve new comment | |
329 | + | comment = database.get_comment (comment_hash_id) | |
330 | + | ||
331 | + | # Automatically add an upvote for the original poster | |
332 | + | database.vote_comment (comment['id'], user['id'], +1) | |
333 | + | ||
334 | + | redirect (application.get_url ('post', hash_id=hash_id)) | |
335 | + | ||
336 | + | @requires_login | |
337 | + | @get ('/edit/post/<hash_id>', name='edit_post') | |
338 | + | def edit_post (hash_id): | |
339 | + | user = session.user () | |
340 | + | post = database.get_post (hash_id, user['id']) | |
341 | + | ||
342 | + | # Make sure the session user is the actual poster/commenter | |
343 | + | if post['userId'] != user['id']: | |
344 | + | redirect (application.get_url ('post', hash_id=hash_id)) | |
345 | + | ||
346 | + | return template ('edit_post.html', post=post) | |
347 | + | ||
348 | + | @requires_login | |
349 | + | @post ('/edit/post/<hash_id>') | |
350 | + | def edit_post_check (hash_id): | |
351 | + | user = session.user () | |
352 | + | post = database.get_post (hash_id, user['id']) | |
353 | + | ||
354 | + | # Make sure the session user is the actual poster/commenter | |
355 | + | if post['userId'] != user['id']: | |
356 | + | redirect (application.get_url ('homepage')) | |
357 | + | ||
358 | + | # MUST have a title. If empty, use original title | |
359 | + | title = request.forms.get ('title').strip () | |
360 | + | if len (title) == 0: title = post['title'] | |
361 | + | link = request.forms.get ('link').strip () if 'link' in request.forms else '' | |
362 | + | text = request.forms.get ('text').strip () if 'text' in request.forms else '' | |
363 | + | ||
364 | + | # If there is a URL, make sure it has a "scheme" | |
365 | + | if len (link) > 0 and urlparse (link).scheme == '': | |
366 | + | link = 'http://' + link | |
367 | + | ||
368 | + | # Update post | |
369 | + | database.update_post (title, link, text, hash_id, user['id']) | |
370 | + | ||
371 | + | redirect (application.get_url ('post', hash_id=hash_id)) | |
372 | + | ||
373 | + | @requires_login | |
374 | + | @get ('/edit/comment/<hash_id>', name='edit_comment') | |
375 | + | def edit_comment (hash_id): | |
376 | + | user = session.user () | |
377 | + | comment = database.get_comment (hash_id, user['id']) | |
378 | + | ||
379 | + | # Make sure the session user is the actual poster/commenter | |
380 | + | if comment['userId'] != user['id']: | |
381 | + | redirect (application.get_url ('post', hash_id=comment['postHashId'])) | |
382 | + | ||
383 | + | return template ('edit_comment.html', comment=comment) | |
384 | + | ||
385 | + | @requires_login | |
386 | + | @post ('/edit/comment/<hash_id>') | |
387 | + | def edit_comment_check (hash_id): | |
388 | + | user = session.user () | |
389 | + | comment = database.get_comment (hash_id, user['id']) | |
390 | + | ||
391 | + | # Make sure the session user is the actual poster/commenter | |
392 | + | if comment['userId'] != user['id']: | |
393 | + | redirect (application.get_url ('homepage')) | |
394 | + | ||
395 | + | text = request.forms.get ('text').strip () if 'text' in request.forms else '' | |
396 | + | ||
397 | + | # Only update if the text is not empty | |
398 | + | if len (text) > 0: | |
399 | + | database.update_comment (text, hash_id, user['id']) | |
400 | + | ||
401 | + | redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + hash_id) | |
402 | + | ||
403 | + | @get ('/submit') | |
404 | + | @requires_login | |
405 | + | def submit (): | |
406 | + | return template ('submit.html') | |
407 | + | ||
408 | + | @post ('/submit') | |
409 | + | @requires_login | |
410 | + | def submit_check (): | |
411 | + | # Somebody sent a <form> without a title??? | |
412 | + | if 'title' not in request.forms or \ | |
413 | + | 'link' not in request.forms or \ | |
414 | + | 'text' not in request.forms: | |
415 | + | abort () | |
416 | + | ||
417 | + | user = session.user () | |
418 | + | ||
419 | + | # Retrieve title | |
420 | + | title = request.forms.get ('title').strip () | |
421 | + | ||
422 | + | # Bad title? | |
423 | + | if len (title) == 0: | |
424 | + | return template ('submit.html', flash='Title is missing.') | |
425 | + | ||
426 | + | # Retrieve link | |
427 | + | link = request.forms.get ('link').strip () | |
428 | + | ||
429 | + | # If there is a URL, make sure it has a "scheme" | |
430 | + | if len (link) > 0 and urlparse (link).scheme == '': | |
431 | + | link = 'http://' + link | |
432 | + | ||
433 | + | # Retrieve text | |
434 | + | text = request.forms.get ('text') | |
435 | + | ||
436 | + | # Add the new post | |
437 | + | post_hash_id = database.new_post (title, link, text, user['id']) | |
438 | + | ||
439 | + | # Retrieve new post | |
440 | + | post = database.get_post (post_hash_id) | |
441 | + | ||
442 | + | # Automatically add an upvote for the original poster | |
443 | + | database.vote_post (post['id'], user['id'], +1) | |
444 | + | ||
445 | + | # Posted. Now go the new post's page | |
446 | + | redirect (application.get_url ('post', hash_id=post_hash_id)) | |
447 | + | ||
448 | + | @requires_login | |
449 | + | @get ('/reply/<hash_id>', name='reply') | |
450 | + | def reply (hash_id): | |
451 | + | user = session.user () | |
452 | + | ||
453 | + | # The comment to reply to | |
454 | + | comment = database.get_comment (hash_id) | |
455 | + | ||
456 | + | # Does the comment exist? | |
457 | + | if not comment: | |
458 | + | redirect (application.get_url ('homepage')) | |
459 | + | ||
460 | + | return template ('reply.html', comment=comment) | |
461 | + | ||
462 | + | @requires_login | |
463 | + | @post ('/reply/<hash_id>') | |
464 | + | def reply_check (hash_id): | |
465 | + | user = session.user () | |
466 | + | ||
467 | + | # The comment to reply to | |
468 | + | comment = database.get_comment (hash_id) | |
469 | + | ||
470 | + | # The text of the reply | |
471 | + | text = request.forms.get ('text').strip () | |
472 | + | ||
473 | + | # Empty comment. Redirect to parent post | |
474 | + | if len (text) == 0: | |
475 | + | redirect (application.get_url ('post', hash_id=comment['postHashId'])) | |
476 | + | ||
477 | + | # We have a text, add the reply and redirect to the new reply | |
478 | + | reply_hash_id = database.new_comment (text, comment['postHashId'], user['id'], comment['id']) | |
479 | + | ||
480 | + | # TODO upvote comment | |
481 | + | # TODO Increase comments count for post | |
482 | + | ||
483 | + | redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + reply_hash_id) | |
484 | + | ||
485 | + | @requires_login | |
486 | + | @post ('/vote', name='vote') | |
487 | + | def vote (): | |
488 | + | user = session.user () | |
489 | + | ||
490 | + | # Vote a post | |
491 | + | if request.forms.get ('target') == 'post': | |
492 | + | # Retrieve the post | |
493 | + | post = database.get_post (request.forms.get ('post'), user['id']) | |
494 | + | ||
495 | + | if not post: | |
496 | + | return | |
497 | + | ||
498 | + | # If user clicked the "upvote" button... | |
499 | + | if request.forms.get ('updown') == 'up': | |
500 | + | # If user has upvoted this post before... | |
501 | + | if post['user_vote'] == 1: | |
502 | + | # Remove +1 | |
503 | + | database.vote_post (post['id'], user['id'], -1) | |
504 | + | # If user has downvoted this post before... | |
505 | + | elif post['user_vote'] == -1: | |
506 | + | # Change vote from -1 to +1 | |
507 | + | database.vote_post (post['id'], user['id'], +2) | |
508 | + | # If user hasn't voted this post... | |
509 | + | else: | |
510 | + | # Add +1 | |
511 | + | database.vote_post (post['id'], user['id'], +1) | |
512 | + | ||
513 | + | # If user clicked the "downvote" button... | |
514 | + | if request.forms.get ('updown') == 'down': | |
515 | + | # If user has downvoted this post before... | |
516 | + | if post['user_vote'] == -1: | |
517 | + | # Remove -1 | |
518 | + | database.vote_post (post['id'], user['id'], +1) | |
519 | + | # If user has upvoted this post before... | |
520 | + | elif post['user_vote'] == 1: | |
521 | + | # Change vote from +1 to -1 | |
522 | + | database.vote_post (post['id'], user['id'], -2) | |
523 | + | # If user hasn't voted this post... | |
524 | + | else: | |
525 | + | # Add -1 | |
526 | + | database.vote_post (post['id'], user['id'], -1) | |
527 | + | ||
528 | + | # Vote a comment | |
529 | + | if request.forms.get ('target') == 'comment': | |
530 | + | # Retrieve the comment | |
531 | + | comment = database.get_comment (request.forms.get ('comment'), user['id']) | |
532 | + | ||
533 | + | if not comment: | |
534 | + | return | |
535 | + | ||
536 | + | # If user clicked the "upvote" button... | |
537 | + | if request.forms.get ('updown') == 'up': | |
538 | + | # If user has upvoted this comment before... | |
539 | + | if comment['user_vote'] == 1: | |
540 | + | # Remove +1 | |
541 | + | database.vote_comment (comment['id'], user['id'], -1) | |
542 | + | # If user has downvoted this comment before... | |
543 | + | elif comment['user_vote'] == -1: | |
544 | + | # Change vote from -1 to +1 | |
545 | + | database.vote_comment (comment['id'], user['id'], +2) | |
546 | + | # If user hasn't voted this comment... | |
547 | + | else: | |
548 | + | # Add +1 | |
549 | + | database.vote_comment (comment['id'], user['id'], +1) | |
550 | + | ||
551 | + | # If user clicked the "downvote" button... | |
552 | + | if request.forms.get ('updown') == 'down': | |
553 | + | # If user has downvoted this comment before... | |
554 | + | if comment['user_vote'] == -1: | |
555 | + | # Remove -1 | |
556 | + | database.vote_comment (comment['id'], user['id'], +1) | |
557 | + | # If user has upvoted this comment before... | |
558 | + | elif comment['user_vote'] == 1: | |
559 | + | # Change vote from +1 to -1 | |
560 | + | database.vote_comment (comment['id'], user['id'], -2) | |
561 | + | # If user hasn't voted this comment... | |
562 | + | else: | |
563 | + | # Add -1 | |
564 | + | database.vote_comment (comment['id'], user['id'], -1) | |
565 | + | ||
566 | + | @get ('/search') | |
567 | + | def search (): | |
568 | + | # Get the search query | |
569 | + | query = request.query.get ('q') | |
570 | + | ||
571 | + | # Results order | |
572 | + | order = request.query.get ('order') | |
573 | + | if order not in [ 'newest', 'points' ]: | |
574 | + | order = 'newest' | |
575 | + | ||
576 | + | results = database.search (query, order=order) | |
577 | + | ||
578 | + | if not results: | |
579 | + | results = [] | |
580 | + | ||
581 | + | return template ('search.html', results=results, query=query, order=order) | |
582 | + | ||
583 | + | @get ('/rss') | |
584 | + | def rss_default (): | |
585 | + | return redirect (application.get_url ('rss', sorting='hot')) | |
586 | + | ||
587 | + | # TODO check if <description> is correctly displayed in RSS aggregators | |
588 | + | @get ('/rss/<sorting>', name='rss') | |
589 | + | def rss (sorting): | |
590 | + | posts = [] | |
591 | + | ||
592 | + | # Retrieve the hostname from the HTTP request. | |
593 | + | # This is used to build absolute URLs in the RSS feed. | |
594 | + | base_url = request.urlparts.scheme + '://' + request.urlparts.netloc | |
595 | + | ||
596 | + | if sorting == 'new': | |
597 | + | posts = database.get_new_posts () | |
598 | + | ||
599 | + | if sorting == 'hot': | |
600 | + | posts = database.get_hot_posts () | |
601 | + | ||
602 | + | # Set correct Content-Type header for this RSS feed | |
603 | + | response.content_type = 'application/rss+xml; charset=UTF-8' | |
604 | + | ||
605 | + | return template ('rss.xml', base_url=base_url, posts=posts) | |
606 | + | ||
607 | + | @get ('/<filename:path>') | |
608 | + | def static (filename): | |
609 | + | return static_file (filename, root='freepost/static/') | |
610 | + | ||
611 | + | ||
612 | + | ||
613 | + | ||
614 | + | ||
615 | + | ||
616 | + | ||
617 | + | ||
618 | + | ||
619 | + | ||
620 | + | ||
621 | + | ||
622 | + | ||
623 | + | ||
624 | + | ||
625 | + | ||
626 | + | ||
627 | + | ||
19 | 628 | ||
20 | - | @get ('/', name='index') | |
21 | - | def index (): | |
22 | - | return "ll" |
index 0000000..fa2f00a | |||
old size: 0B - new size: 14K | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,598 @@ | |||
1 | + | import MySQLdb | |
2 | + | import re | |
3 | + | from freepost import random, settings | |
4 | + | ||
5 | + | db = MySQLdb.connect ( | |
6 | + | host = settings['mysql']['host'], | |
7 | + | port = settings['mysql']['port'], | |
8 | + | db = settings['mysql']['schema'], | |
9 | + | user = settings['mysql']['username'], | |
10 | + | passwd = settings['mysql']['password'], | |
11 | + | autocommit = True) | |
12 | + | ||
13 | + | # Store a new session_id for a user that has logged in | |
14 | + | # The session token is stored in the user cookies during login, here | |
15 | + | # we store the hash value of that token. | |
16 | + | def new_session (user_id, session_token): | |
17 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
18 | + | ||
19 | + | cursor.execute ( | |
20 | + | """ | |
21 | + | UPDATE user | |
22 | + | SET session = SHA2(%(session)s, 512) | |
23 | + | WHERE id = %(user)s | |
24 | + | """, | |
25 | + | { | |
26 | + | 'user': user_id, | |
27 | + | 'session': session_token | |
28 | + | } | |
29 | + | ) | |
30 | + | ||
31 | + | # Delete user session token on logout | |
32 | + | def delete_session (user_id): | |
33 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
34 | + | ||
35 | + | cursor.execute ( | |
36 | + | """ | |
37 | + | UPDATE user | |
38 | + | SET session = NULL | |
39 | + | WHERE id = %(user)s | |
40 | + | """, | |
41 | + | { | |
42 | + | 'user': user_id | |
43 | + | } | |
44 | + | ) | |
45 | + | ||
46 | + | # Check user login credentials | |
47 | + | # | |
48 | + | # @return None if bad credentials, otherwise return the user | |
49 | + | def check_user_credentials (username, password): | |
50 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
51 | + | ||
52 | + | cursor.execute ( | |
53 | + | """ | |
54 | + | SELECT * | |
55 | + | FROM user | |
56 | + | WHERE | |
57 | + | username = %(username)s AND | |
58 | + | password = SHA2(CONCAT(%(password)s, salt), 512) AND | |
59 | + | isActive = 1 | |
60 | + | """, | |
61 | + | { | |
62 | + | 'username': username, | |
63 | + | 'password': password | |
64 | + | } | |
65 | + | ) | |
66 | + | ||
67 | + | return cursor.fetchone () | |
68 | + | ||
69 | + | # Check if username exists | |
70 | + | def username_exists (username): | |
71 | + | return get_user_by_username (username) is not None | |
72 | + | ||
73 | + | # Create new user account | |
74 | + | def new_user (username, password): | |
75 | + | # Create a hash_id for the new post | |
76 | + | hash_id = random.alphanumeric_string (10) | |
77 | + | ||
78 | + | # Create a salt for user's password | |
79 | + | salt = random.ascii_string (16) | |
80 | + | ||
81 | + | # Add user to database | |
82 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
83 | + | ||
84 | + | cursor.execute ( | |
85 | + | """ | |
86 | + | INSERT INTO user (hashId, isActive, password, registered, salt, username) | |
87 | + | VALUES (%(hash_id)s, 1, SHA2(CONCAT(%(password)s, %(salt)s), 512), NOW(), %(salt)s, %(username)s) | |
88 | + | """, | |
89 | + | { | |
90 | + | 'hash_id': hash_id, | |
91 | + | 'password': password, | |
92 | + | 'salt': salt, | |
93 | + | 'username': username | |
94 | + | } | |
95 | + | ) | |
96 | + | ||
97 | + | # Check if session token exists | |
98 | + | def is_valid_session (token): | |
99 | + | return get_user_by_session_token (token) is not None | |
100 | + | ||
101 | + | # Return the number of unread replies | |
102 | + | def count_unread_messages (user_id): | |
103 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
104 | + | ||
105 | + | cursor.execute ( | |
106 | + | """ | |
107 | + | SELECT COUNT(1) as new_messages | |
108 | + | FROM comment | |
109 | + | WHERE parentUserId = %(user)s AND userId != %(user)s AND `read` = 0 | |
110 | + | """, | |
111 | + | { | |
112 | + | 'user': user_id | |
113 | + | } | |
114 | + | ) | |
115 | + | ||
116 | + | return cursor.fetchone ()['new_messages'] | |
117 | + | ||
118 | + | # Retrieve a user | |
119 | + | def get_user_by_username (username): | |
120 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
121 | + | ||
122 | + | cursor.execute ( | |
123 | + | """ | |
124 | + | SELECT * | |
125 | + | FROM user | |
126 | + | WHERE username = %(username)s | |
127 | + | """, | |
128 | + | { | |
129 | + | 'username': username | |
130 | + | } | |
131 | + | ) | |
132 | + | ||
133 | + | return cursor.fetchone () | |
134 | + | ||
135 | + | # Retrieve a user from a session cookie | |
136 | + | def get_user_by_session_token (session_token): | |
137 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
138 | + | ||
139 | + | cursor.execute ( | |
140 | + | """ | |
141 | + | SELECT * | |
142 | + | FROM user | |
143 | + | WHERE session = SHA2(%(session)s, 512) | |
144 | + | """, | |
145 | + | { | |
146 | + | 'session': session_token | |
147 | + | } | |
148 | + | ) | |
149 | + | ||
150 | + | return cursor.fetchone () | |
151 | + | ||
152 | + | # Get posts by date (for homepage) | |
153 | + | def get_new_posts (page = 0, session_user_id = None): | |
154 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
155 | + | ||
156 | + | cursor.execute ( | |
157 | + | """ | |
158 | + | SELECT P.*, U.username, V.vote AS user_vote | |
159 | + | FROM post AS P | |
160 | + | JOIN user AS U ON P.userId = U.id | |
161 | + | LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = %(user)s | |
162 | + | ORDER BY P.created DESC | |
163 | + | LIMIT %(limit)s | |
164 | + | OFFSET %(offset)s | |
165 | + | """, | |
166 | + | { | |
167 | + | 'user': session_user_id, | |
168 | + | 'limit': settings['defaults']['items_per_page'], | |
169 | + | 'offset': page * settings['defaults']['items_per_page'] | |
170 | + | } | |
171 | + | ) | |
172 | + | ||
173 | + | return cursor.fetchall () | |
174 | + | ||
175 | + | # Get posts by rating (for homepage) | |
176 | + | def get_hot_posts (page = 0, session_user_id = None): | |
177 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
178 | + | ||
179 | + | cursor.execute ( | |
180 | + | """ | |
181 | + | SELECT P.*, U.username, V.vote AS user_vote | |
182 | + | FROM post AS P | |
183 | + | JOIN user AS U ON P.userId = U.id | |
184 | + | LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = %(user)s | |
185 | + | ORDER BY P.dateCreated DESC, P.vote DESC, P.commentsCount DESC | |
186 | + | LIMIT %(limit)s | |
187 | + | OFFSET %(offset)s | |
188 | + | """, | |
189 | + | { | |
190 | + | 'user': session_user_id, | |
191 | + | 'limit': settings['defaults']['items_per_page'], | |
192 | + | 'offset': page * settings['defaults']['items_per_page'] | |
193 | + | } | |
194 | + | ) | |
195 | + | ||
196 | + | return cursor.fetchall () | |
197 | + | ||
198 | + | # Retrieve user's own posts | |
199 | + | def get_user_posts (user_id): | |
200 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
201 | + | ||
202 | + | cursor.execute ( | |
203 | + | """ | |
204 | + | SELECT * | |
205 | + | FROM post | |
206 | + | WHERE userId = %(user)s | |
207 | + | ORDER BY created DESC | |
208 | + | LIMIT 50 | |
209 | + | """, | |
210 | + | { | |
211 | + | 'user': user_id | |
212 | + | } | |
213 | + | ) | |
214 | + | ||
215 | + | return cursor.fetchall () | |
216 | + | ||
217 | + | # Retrieve user's own comments | |
218 | + | def get_user_comments (user_id): | |
219 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
220 | + | ||
221 | + | cursor.execute ( | |
222 | + | """ | |
223 | + | SELECT | |
224 | + | C.*, | |
225 | + | P.title AS postTitle, | |
226 | + | P.hashId AS postHashId | |
227 | + | FROM comment AS C | |
228 | + | JOIN post AS P ON P.id = C.postId | |
229 | + | WHERE C.userId = %(user)s | |
230 | + | ORDER BY C.created DESC | |
231 | + | LIMIT 50 | |
232 | + | """, | |
233 | + | { | |
234 | + | 'user': user_id | |
235 | + | } | |
236 | + | ) | |
237 | + | ||
238 | + | return cursor.fetchall () | |
239 | + | ||
240 | + | # Retrieve user's own replies to other people | |
241 | + | def get_user_replies (user_id): | |
242 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
243 | + | ||
244 | + | cursor.execute ( | |
245 | + | """ | |
246 | + | SELECT | |
247 | + | C.*, | |
248 | + | P.title AS postTitle, | |
249 | + | P.hashId AS postHashId, | |
250 | + | U.username AS username | |
251 | + | FROM comment AS C | |
252 | + | JOIN post AS P ON P.id = C.postId | |
253 | + | JOIN user AS U ON U.id = C.userId | |
254 | + | WHERE C.parentUserId = %(user)s AND C.userId != %(user)s | |
255 | + | ORDER BY C.created DESC | |
256 | + | LIMIT 50 | |
257 | + | """, | |
258 | + | { | |
259 | + | 'user': user_id | |
260 | + | } | |
261 | + | ) | |
262 | + | ||
263 | + | return cursor.fetchall () | |
264 | + | ||
265 | + | # Update user information | |
266 | + | def update_user (user_id, about, email, email_notifications): | |
267 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
268 | + | ||
269 | + | cursor.execute ( | |
270 | + | """ | |
271 | + | UPDATE user | |
272 | + | SET about = %(about)s, | |
273 | + | email = %(email)s, | |
274 | + | email_notifications = %(notifications)s | |
275 | + | WHERE id = %(user)s | |
276 | + | """, | |
277 | + | { | |
278 | + | 'about': about, | |
279 | + | 'email': email, | |
280 | + | 'notifications': email_notifications, | |
281 | + | 'user': user_id | |
282 | + | } | |
283 | + | ) | |
284 | + | ||
285 | + | # Set user replies as read | |
286 | + | def set_replies_as_read (user_id): | |
287 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
288 | + | ||
289 | + | cursor.execute ( | |
290 | + | """ | |
291 | + | UPDATE comment | |
292 | + | SET `read` = 1 | |
293 | + | WHERE parentUserId = %(user)s AND `read` = 0 | |
294 | + | """, | |
295 | + | { | |
296 | + | 'user': user_id | |
297 | + | } | |
298 | + | ) | |
299 | + | ||
300 | + | # Submit a new post/link | |
301 | + | def new_post (title, link, text, user_id): | |
302 | + | # Create a hash_id for the new post | |
303 | + | hash_id = random.alphanumeric_string (10) | |
304 | + | ||
305 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
306 | + | ||
307 | + | cursor.execute ( | |
308 | + | """ | |
309 | + | INSERT INTO post (hashId, created, dateCreated, title, | |
310 | + | link, text, vote, commentsCount, userId) | |
311 | + | VALUES (%(hash_id)s, NOW(), CURDATE(), %(title)s, %(link)s, | |
312 | + | %(text)s, 0, 0, %(user)s) | |
313 | + | """, | |
314 | + | { | |
315 | + | 'hash_id': hash_id, | |
316 | + | 'title': title, | |
317 | + | 'link': link, | |
318 | + | 'text': text, | |
319 | + | 'user': user_id | |
320 | + | } | |
321 | + | ) | |
322 | + | ||
323 | + | return hash_id | |
324 | + | ||
325 | + | # Retrieve a post | |
326 | + | def get_post (hash, session_user_id = None): | |
327 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
328 | + | ||
329 | + | cursor.execute ( | |
330 | + | """ | |
331 | + | SELECT P.*, U.username, V.vote AS user_vote | |
332 | + | FROM post AS P | |
333 | + | JOIN user AS U ON P.userId = U.id | |
334 | + | LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = %(user)s | |
335 | + | WHERE P.hashId = %(post)s | |
336 | + | """, | |
337 | + | { | |
338 | + | 'user': session_user_id, | |
339 | + | 'post': hash | |
340 | + | } | |
341 | + | ) | |
342 | + | ||
343 | + | return cursor.fetchone () | |
344 | + | ||
345 | + | # Update a post | |
346 | + | def update_post (title, link, text, post_hash_id, user_id): | |
347 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
348 | + | ||
349 | + | cursor.execute ( | |
350 | + | """ | |
351 | + | UPDATE post | |
352 | + | SET title = %(title)s, | |
353 | + | link = %(link)s, | |
354 | + | text = %(text)s | |
355 | + | WHERE hashId = %(hash_id)s AND userId = %(user)s | |
356 | + | """, | |
357 | + | { | |
358 | + | 'title': title, | |
359 | + | 'link': link, | |
360 | + | 'text': text, | |
361 | + | 'hash_id': post_hash_id, | |
362 | + | 'user': user_id | |
363 | + | } | |
364 | + | ) | |
365 | + | ||
366 | + | # Retrieve all comments for a specific post | |
367 | + | def get_post_comments (post_id, session_user_id = None): | |
368 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
369 | + | ||
370 | + | cursor.execute ( | |
371 | + | """ | |
372 | + | SELECT C.*, U.username, V.vote AS user_vote | |
373 | + | FROM comment AS C | |
374 | + | JOIN user AS U ON C.userId = U.id | |
375 | + | LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = %(user)s | |
376 | + | WHERE C.postId = %(post)s | |
377 | + | ORDER BY C.vote DESC, C.created ASC | |
378 | + | """, | |
379 | + | { | |
380 | + | 'user': session_user_id, | |
381 | + | 'post': post_id | |
382 | + | } | |
383 | + | ) | |
384 | + | ||
385 | + | return cursor.fetchall () | |
386 | + | ||
387 | + | # Submit a new comment to a post | |
388 | + | def new_comment (comment_text, post_hash_id, user_id, parent_comment_id = None): | |
389 | + | # Create a hash_id for the new comment | |
390 | + | hash_id = random.alphanumeric_string (10) | |
391 | + | ||
392 | + | # Retrieve post | |
393 | + | post = get_post (post_hash_id) | |
394 | + | ||
395 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
396 | + | ||
397 | + | cursor.execute ( | |
398 | + | """ | |
399 | + | INSERT INTO comment (hashId, created, dateCreated, `read`, `text`, `vote`, | |
400 | + | parentId, parentUserId, postId, userId) | |
401 | + | VALUES (%(hash_id)s, NOW(), CURDATE(), 0, %(text)s, 0, %(parent_id)s, | |
402 | + | %(parent_user_id)s, %(post_id)s, %(user)s) | |
403 | + | """, | |
404 | + | { | |
405 | + | 'hash_id': hash_id, | |
406 | + | 'text': comment_text, | |
407 | + | 'parent_id': parent_comment_id, | |
408 | + | 'parent_user_id': post['userId'], | |
409 | + | 'post_id': post['id'], | |
410 | + | 'user': user_id | |
411 | + | } | |
412 | + | ) | |
413 | + | ||
414 | + | # Increase comments count for post | |
415 | + | cursor.execute ( | |
416 | + | """ | |
417 | + | UPDATE post | |
418 | + | SET commentsCount = commentsCount + 1 | |
419 | + | WHERE id = %(post)s | |
420 | + | """, | |
421 | + | { | |
422 | + | 'post': post['id'] | |
423 | + | } | |
424 | + | ) | |
425 | + | ||
426 | + | return hash_id | |
427 | + | ||
428 | + | # Retrieve a single comment | |
429 | + | def get_comment (hash_id, session_user_id = None): | |
430 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
431 | + | ||
432 | + | cursor.execute ( | |
433 | + | """ | |
434 | + | SELECT | |
435 | + | C.*, | |
436 | + | P.hashId AS postHashId, | |
437 | + | P.title AS postTitle, | |
438 | + | U.username, | |
439 | + | V.vote AS user_vote | |
440 | + | FROM comment AS C | |
441 | + | JOIN user AS U ON C.userId = U.id | |
442 | + | JOIN post AS P ON P.id = C.postId | |
443 | + | LEFT JOIN vote_comment as V ON V.commentId = C.id AND V.userId = %(user)s | |
444 | + | WHERE C.hashId = %(comment)s | |
445 | + | """, | |
446 | + | { | |
447 | + | 'user': session_user_id, | |
448 | + | 'comment': hash_id | |
449 | + | } | |
450 | + | ) | |
451 | + | ||
452 | + | return cursor.fetchone () | |
453 | + | ||
454 | + | # Update a comment | |
455 | + | def update_comment (text, comment_hash_id, user_id): | |
456 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
457 | + | ||
458 | + | cursor.execute ( | |
459 | + | """ | |
460 | + | UPDATE comment | |
461 | + | SET text = %(text)s | |
462 | + | WHERE hashId = %(comment)s AND userId = %(user)s | |
463 | + | """, | |
464 | + | { | |
465 | + | 'text': text, | |
466 | + | 'comment': comment_hash_id, | |
467 | + | 'user': user_id | |
468 | + | } | |
469 | + | ) | |
470 | + | ||
471 | + | # Add or update vote to a post | |
472 | + | def vote_post (post_id, user_id, vote): | |
473 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
474 | + | ||
475 | + | # Insert or update the user vote | |
476 | + | cursor.execute ( | |
477 | + | """ | |
478 | + | INSERT INTO vote_post (vote, datetime, postId, userId) | |
479 | + | VALUES (%(vote)s, NOW(), %(post)s, %(user)s) | |
480 | + | ON DUPLICATE KEY UPDATE | |
481 | + | vote = vote + %(vote)s, | |
482 | + | datetime = NOW() | |
483 | + | """, | |
484 | + | { | |
485 | + | 'vote': vote, | |
486 | + | 'post': post_id, | |
487 | + | 'user': user_id | |
488 | + | } | |
489 | + | ) | |
490 | + | ||
491 | + | # Update vote counter for post | |
492 | + | cursor.execute ( | |
493 | + | """ | |
494 | + | UPDATE post | |
495 | + | SET vote = vote + %(vote)s | |
496 | + | WHERE id = %(post)s | |
497 | + | """, | |
498 | + | { | |
499 | + | 'vote': vote, | |
500 | + | 'post': post_id | |
501 | + | } | |
502 | + | ) | |
503 | + | ||
504 | + | # Add or update vote to a comment | |
505 | + | def vote_comment (comment_id, user_id, vote): | |
506 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
507 | + | ||
508 | + | # Insert or update the user vote | |
509 | + | cursor.execute ( | |
510 | + | """ | |
511 | + | INSERT INTO vote_comment (vote, datetime, commentId, userId) | |
512 | + | VALUES (%(vote)s, NOW(), %(comment)s, %(user)s) | |
513 | + | ON DUPLICATE KEY UPDATE | |
514 | + | vote = vote + %(vote)s, | |
515 | + | datetime = NOW() | |
516 | + | """, | |
517 | + | { | |
518 | + | 'vote': vote, | |
519 | + | 'comment': comment_id, | |
520 | + | 'user': user_id | |
521 | + | } | |
522 | + | ) | |
523 | + | ||
524 | + | # Update vote counter for comment | |
525 | + | cursor.execute ( | |
526 | + | """ | |
527 | + | UPDATE comment | |
528 | + | SET vote = vote + %(vote)s | |
529 | + | WHERE id = %(comment)s | |
530 | + | """, | |
531 | + | { | |
532 | + | 'vote': vote, | |
533 | + | 'comment': comment_id | |
534 | + | } | |
535 | + | ) | |
536 | + | ||
537 | + | # Search posts | |
538 | + | def search (query, page = 0, order = 'newest'): | |
539 | + | if not query: | |
540 | + | return None | |
541 | + | ||
542 | + | # Remove multiple white spaces and replace with '|' (for query REGEXP) | |
543 | + | query = re.sub (' +', '|', query.strip ()) | |
544 | + | ||
545 | + | if len (query) == 0: | |
546 | + | return None | |
547 | + | ||
548 | + | cursor = db.cursor (MySQLdb.cursors.DictCursor) | |
549 | + | ||
550 | + | if order == 'newest': | |
551 | + | order = 'P.created DESC' | |
552 | + | if order == 'points': | |
553 | + | order = 'P.vote DESC' | |
554 | + | ||
555 | + | cursor.execute ( | |
556 | + | """ | |
557 | + | SELECT P.*, U.username | |
558 | + | FROM post AS P | |
559 | + | JOIN user AS U ON P.userId = U.id | |
560 | + | WHERE P.title REGEXP %(query)s | |
561 | + | ORDER BY {order} | |
562 | + | LIMIT %(limit)s | |
563 | + | OFFSET %(offset)s | |
564 | + | """.format (order=order), | |
565 | + | { | |
566 | + | 'query': query, | |
567 | + | 'limit': settings['defaults']['search_results_per_page'], | |
568 | + | 'offset': page * settings['defaults']['search_results_per_page'] | |
569 | + | } | |
570 | + | ) | |
571 | + | ||
572 | + | return cursor.fetchall () | |
573 | + | ||
574 | + | ||
575 | + | ||
576 | + | ||
577 | + | ||
578 | + | ||
579 | + | ||
580 | + | ||
581 | + | ||
582 | + | ||
583 | + | ||
584 | + | ||
585 | + | ||
586 | + | ||
587 | + | ||
588 | + | ||
589 | + | ||
590 | + | ||
591 | + | ||
592 | + | ||
593 | + | ||
594 | + | ||
595 | + | ||
596 | + | ||
597 | + | ||
598 | + |
index 0000000..4f3a3fa | |||
old size: 0B - new size: 1K | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,27 @@ | |||
1 | + | # The secrets module is used for generating cryptographically strong random | |
2 | + | # numbers suitable for managing data such as passwords, account authentication, | |
3 | + | # security tokens, and related secrets. | |
4 | + | # In particularly, secrets should be used in preference to the default | |
5 | + | # pseudo-random number generator in the random module, which is designed for | |
6 | + | # modelling and simulation, not security or cryptography. | |
7 | + | # | |
8 | + | # Requires Python 3.6+ | |
9 | + | # import secrets | |
10 | + | import random | |
11 | + | import string | |
12 | + | ||
13 | + | def ascii_string ( | |
14 | + | length = 16, | |
15 | + | alphabet = string.ascii_letters + string.digits + string.punctuation): | |
16 | + | ||
17 | + | # return ''.join (secrets.choice (alphabet) for i in range (length)) | |
18 | + | return ''.join (random.choice (alphabet) for i in range (length)) | |
19 | + | ||
20 | + | def alphanumeric_string (length = 16): | |
21 | + | return ascii_string (length, string.ascii_letters + string.digits) | |
22 | + | ||
23 | + | def digit_string (length = 16): | |
24 | + | return ascii_string (length, alphabet = string.digits) | |
25 | + | ||
26 | + | def hex_string (length = 16): | |
27 | + | return ascii_string (length, alphabet = string.hexdigits) |
index 0000000..767934e | |||
old size: 0B - new size: 1K | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,46 @@ | |||
1 | + | from bottle import request, response | |
2 | + | from freepost import database, random, settings | |
3 | + | ||
4 | + | # Start a new session | |
5 | + | def start (user_id, remember = False): | |
6 | + | # Create a new token for this session. | |
7 | + | # The random token is stored as a user cookie, and its hash value is | |
8 | + | # stored in the database to match the current user for the future requests. | |
9 | + | session_token = random.ascii_string (64) | |
10 | + | ||
11 | + | # Create session cookie | |
12 | + | response.set_cookie ( | |
13 | + | name = settings['session']['name'], | |
14 | + | value = session_token, | |
15 | + | secret = settings['cookies']['secret'], | |
16 | + | path = '/', | |
17 | + | # When to end the session | |
18 | + | max_age = settings['session']['remember_me'] if remember else None, | |
19 | + | # HTTPS only | |
20 | + | secure = False, | |
21 | + | # Do not allow JavaScript to read this cookie | |
22 | + | httponly = True) | |
23 | + | ||
24 | + | # Store session to database | |
25 | + | database.new_session (user_id, session_token) | |
26 | + | ||
27 | + | # Close the current open session | |
28 | + | def close (): | |
29 | + | session_user = user () | |
30 | + | ||
31 | + | # Delete user cookie containing session token | |
32 | + | response.delete_cookie (settings['session']['name']) | |
33 | + | ||
34 | + | # Delete session token from database | |
35 | + | database.delete_session (session_user['id']) | |
36 | + | ||
37 | + | # Retrieve user from session token | |
38 | + | def user (): | |
39 | + | session_token = request.get_cookie ( | |
40 | + | key = settings['session']['name'], | |
41 | + | secret = settings['cookies']['secret']) | |
42 | + | ||
43 | + | if session_token is None: | |
44 | + | return None | |
45 | + | ||
46 | + | return database.get_user_by_session_token (session_token) |
index 0000000..3cc8c56 | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..e46e311 | |||
old size: 0B - new size: 7K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..f09e9e3 | |||
old size: 0B - new size: 259B | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..a67f4bd | |||
old size: 0B - new size: 334B | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..46967c0 | |||
old size: 0B - new size: 14K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..6ad1448 | |||
old size: 0B - new size: 3K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..5936ed5 | |||
old size: 0B - new size: 516B | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..a7fbaa9 | |||
old size: 0B - new size: 6K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..e7e68a4 | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
Binary file |
index 0000000..8e08258 | |||
old size: 0B - new size: 5K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,147 @@ | |||
1 | + | /* | |
2 | + | @licstart The following is the entire license notice for the JavaScript code in this page. | |
3 | + | ||
4 | + | This is the code powering <http://freepo.st>. | |
5 | + | Copyright © 2014-2016 zPlus | |
6 | + | Copyright © 2016 Adonay "adfeno" Felipe Nogueira <adfeno@openmailbox.org> <https://libreplanet.org/wiki/User:Adfeno> | |
7 | + | ||
8 | + | This program is free software: you can redistribute it and/or modify | |
9 | + | it under the terms of the GNU Affero General Public License as published by | |
10 | + | the Free Software Foundation, either version 3 of the License, or | |
11 | + | (at your option) any later version. | |
12 | + | ||
13 | + | This program is distributed in the hope that it will be useful, | |
14 | + | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | + | GNU Affero General Public License for more details. | |
17 | + | ||
18 | + | You should have received a copy of the GNU Affero General Public License | |
19 | + | along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 | + | ||
21 | + | As additional permission under GNU GPL version 3 section 7, you may | |
22 | + | distribute non-source (e.g., minimized or compacted) forms of that code | |
23 | + | without the copy of the GNU GPL normally required by section 4, provided | |
24 | + | you include this license notice and a URL through which recipients can | |
25 | + | access the Corresponding Source. | |
26 | + | ||
27 | + | @licend The above is the entire license notice for the JavaScript code in this page. | |
28 | + | */ | |
29 | + | ||
30 | + | /** | |
31 | + | * Store which keys have been pressed. | |
32 | + | * When a key has been pressed, pressed_key[e.keyCode] will be set | |
33 | + | * to TRUE. When a key is released, pressed_key[e.keyCode] will be | |
34 | + | * set to FALSE. | |
35 | + | */ | |
36 | + | var pressed_key = []; | |
37 | + | ||
38 | + | /** | |
39 | + | * Change arrows class when voting. | |
40 | + | */ | |
41 | + | function vote (action, dom_element) | |
42 | + | { | |
43 | + | var arrow_up = dom_element.querySelector ('button[title="upvote"]'); | |
44 | + | var vote_counter = dom_element.querySelector ('.count') | |
45 | + | var arrow_down = dom_element.querySelector ('button[title="downvote"]'); | |
46 | + | ||
47 | + | // Voted/Upvoted | |
48 | + | var current_status = 0; | |
49 | + | ||
50 | + | if (arrow_up.classList.contains('upvoted')) | |
51 | + | current_status = 1; | |
52 | + | ||
53 | + | if (arrow_down.classList.contains('downvoted')) | |
54 | + | current_status = -1; | |
55 | + | ||
56 | + | // Current vote | |
57 | + | var current_vote = Number (vote_counter.textContent); | |
58 | + | ||
59 | + | // Remove class from arrows | |
60 | + | arrow_up.classList.remove ('upvoted'); | |
61 | + | arrow_down.classList.remove ('downvoted'); | |
62 | + | ||
63 | + | // Toggle upvote class for arrow | |
64 | + | if ("up" == action) | |
65 | + | switch (current_status) | |
66 | + | { | |
67 | + | case -1: | |
68 | + | vote_counter.textContent = current_vote + 2; | |
69 | + | arrow_up.classList.add ('upvoted'); | |
70 | + | break; | |
71 | + | case 0: | |
72 | + | vote_counter.textContent = current_vote + 1; | |
73 | + | arrow_up.classList.add ('upvoted'); | |
74 | + | break; | |
75 | + | case 1: | |
76 | + | vote_counter.textContent = current_vote - 1; | |
77 | + | break; | |
78 | + | } | |
79 | + | ||
80 | + | // Toggle downvote class for arrow | |
81 | + | if ("down" == action) | |
82 | + | switch (current_status) | |
83 | + | { | |
84 | + | case -1: | |
85 | + | vote_counter.textContent = current_vote + 1; | |
86 | + | break; | |
87 | + | case 0: | |
88 | + | vote_counter.textContent = current_vote - 1; | |
89 | + | arrow_down.classList.add ('downvoted'); | |
90 | + | break; | |
91 | + | case 1: | |
92 | + | vote_counter.textContent = current_vote - 2; | |
93 | + | arrow_down.classList.add ('downvoted'); | |
94 | + | break; | |
95 | + | } | |
96 | + | } | |
97 | + | ||
98 | + | // Wait DOM to be ready... | |
99 | + | document.addEventListener ('DOMContentLoaded', function() { | |
100 | + | ||
101 | + | /** | |
102 | + | * A "vote section" is a <span/> containing | |
103 | + | * - up arrow | |
104 | + | * - votes sum | |
105 | + | * - down arrow | |
106 | + | * | |
107 | + | * However, if the user is not logged in, there's only a text | |
108 | + | * with the sum of votes, eg. "2 votes" (no <tag> children). | |
109 | + | */ | |
110 | + | var vote_sections = document.querySelectorAll ('.vote '); | |
111 | + | ||
112 | + | // Bind vote() event to up/down vote arrows | |
113 | + | for (var i = 0; i < vote_sections.length; i++) | |
114 | + | // See comment above on the "vote_sections" declaration. | |
115 | + | if (vote_sections[i].children.length > 0) | |
116 | + | { | |
117 | + | vote_sections[i] | |
118 | + | .querySelector ('button[title="upvote"]') | |
119 | + | .addEventListener ('click', function () { | |
120 | + | vote ('up', this.closest ('.vote')) | |
121 | + | }); | |
122 | + | ||
123 | + | vote_sections[i] | |
124 | + | .querySelector ('button[title="downvote"]') | |
125 | + | .addEventListener ('click', function () { | |
126 | + | vote ('down', this.closest ('.vote')) | |
127 | + | }); | |
128 | + | } | |
129 | + | ||
130 | + | // Bind onkeydown()/onkeyup() event to keys | |
131 | + | document.onkeydown = document.onkeyup = function(e) { | |
132 | + | // Set the current key code as TRUE/FALSE | |
133 | + | pressed_key[e.keyCode] = e.type == 'keydown'; | |
134 | + | ||
135 | + | // If Ctrl+Enter have been pressed | |
136 | + | // Key codes: Ctrl=17, Enter=13 | |
137 | + | if (pressed_key[17] && pressed_key[13]) | |
138 | + | { | |
139 | + | // Select all forms in the current page with class "shortcut-submit" | |
140 | + | var forms = document.querySelectorAll ("form.shortcut-submit"); | |
141 | + | ||
142 | + | for (var i = 0; i < forms.length; i++) | |
143 | + | forms[i].submit (); | |
144 | + | } | |
145 | + | } | |
146 | + | ||
147 | + | }); |
index 0000000..df942c7 | |||
old size: 0B - new size: 10K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,313 @@ | |||
1 | + | @require 'reset.styl' | |
2 | + | ||
3 | + | /* A class used for displaying URLs domain (under post tile) */ | |
4 | + | .netloc | |
5 | + | color #828282 | |
6 | + | font-style italic | |
7 | + | ||
8 | + | .monkey | |
9 | + | height 1.5em | |
10 | + | margin 0 1em | |
11 | + | vertical-align middle | |
12 | + | ||
13 | + | /* Logo text */ | |
14 | + | a.logo, | |
15 | + | a.logo:hover, | |
16 | + | a.logo:visited | |
17 | + | color #000 | |
18 | + | font-weight bold | |
19 | + | text-decoration none | |
20 | + | ||
21 | + | body | |
22 | + | > .container | |
23 | + | margin auto | |
24 | + | max-width 80% | |
25 | + | ||
26 | + | /* Page header */ | |
27 | + | > .header | |
28 | + | padding 1rem 0 | |
29 | + | text-align center | |
30 | + | ||
31 | + | /* Menu under the logo */ | |
32 | + | > .menu | |
33 | + | border-bottom 1px solid transparent | |
34 | + | display flex | |
35 | + | flex-direction row | |
36 | + | flex-wrap wrap | |
37 | + | justify-content flex-start | |
38 | + | align-content flex-start | |
39 | + | align-items flex-start | |
40 | + | ||
41 | + | margin 1em auto | |
42 | + | ||
43 | + | > .flex-item | |
44 | + | flex 0 1 auto | |
45 | + | align-self auto | |
46 | + | order 0 | |
47 | + | ||
48 | + | border 0 | |
49 | + | border-bottom 1px solid #ccc | |
50 | + | color #000 | |
51 | + | margin 0 0 | |
52 | + | padding 0 .5rem .5rem .5rem | |
53 | + | ||
54 | + | &:first-child | |
55 | + | border-bottom 2px solid transparent | |
56 | + | margin-left 0 | |
57 | + | ||
58 | + | /* Highlight menu item of the current active page (Hot/New/Submit/...) */ | |
59 | + | > .active_page | |
60 | + | border-bottom 3px solid #000 | |
61 | + | ||
62 | + | /* Highlight username if there are unread messages */ | |
63 | + | .new_messages | |
64 | + | background-color rgb(255, 175, 50) | |
65 | + | border-radius 4px | |
66 | + | color #fff | |
67 | + | font-weight bold | |
68 | + | margin 0 | |
69 | + | padding .5em .5em | |
70 | + | text-decoration none | |
71 | + | ||
72 | + | ||
73 | + | > .content | |
74 | + | padding 1em 0 | |
75 | + | line-height 1.5em | |
76 | + | ||
77 | + | .vote | |
78 | + | margin 0 | |
79 | + | ||
80 | + | > form | |
81 | + | display inline-block | |
82 | + | ||
83 | + | > button | |
84 | + | background transparent | |
85 | + | border 0 | |
86 | + | cursor pointer | |
87 | + | display inline-block | |
88 | + | font-family "Courier New", Courier, monospace | |
89 | + | font-size 1rem | |
90 | + | margin 0 | |
91 | + | overflow hidden | |
92 | + | padding 0 | |
93 | + | text-decoration none | |
94 | + | vertical-align middle | |
95 | + | ||
96 | + | /* Arrow style if already upvoted (green) */ | |
97 | + | &.upvoted | |
98 | + | background-color #00e313 | |
99 | + | border-radius 999em | |
100 | + | color #fff | |
101 | + | font-weight bolder | |
102 | + | height 1rem | |
103 | + | width 1rem | |
104 | + | ||
105 | + | /* Arrow style if already upvoted (red) */ | |
106 | + | &.downvoted | |
107 | + | background-color #f00 | |
108 | + | border-radius 999em | |
109 | + | color #fff | |
110 | + | font-weight bolder | |
111 | + | height 1rem | |
112 | + | width 1rem | |
113 | + | ||
114 | + | /* Votes counter */ | |
115 | + | > .count | |
116 | + | margin 0 .5rem | |
117 | + | ||
118 | + | /* Home page */ | |
119 | + | .posts | |
120 | + | ||
121 | + | /* A singe post */ | |
122 | + | .post | |
123 | + | margin 0 0 2em 0 | |
124 | + | vertical-align top | |
125 | + | ||
126 | + | > .title | |
127 | + | font-size 1.5em | |
128 | + | ||
129 | + | > a | |
130 | + | color #000 | |
131 | + | ||
132 | + | /* Some post info showed below the title */ | |
133 | + | > .info | |
134 | + | color #666 | |
135 | + | margin .5em 0 | |
136 | + | opacity .8 | |
137 | + | ||
138 | + | > .username | |
139 | + | margin-left 1rem | |
140 | + | ||
141 | + | /* New submission page */ | |
142 | + | > form.submit | |
143 | + | margin auto | |
144 | + | max-width 30em | |
145 | + | ||
146 | + | /* Page for a post */ | |
147 | + | > .post | |
148 | + | ||
149 | + | /* Style used to format Markdown tables */ | |
150 | + | table | |
151 | + | background #fff | |
152 | + | border-collapse collapse | |
153 | + | text-align left | |
154 | + | ||
155 | + | th | |
156 | + | border-bottom 2px solid #6678b1 | |
157 | + | color #039 | |
158 | + | font-weight normal | |
159 | + | padding 0 1em | |
160 | + | ||
161 | + | td | |
162 | + | border-bottom 1px solid #ccc | |
163 | + | color #669 | |
164 | + | padding .5em 1em | |
165 | + | ||
166 | + | tbody tr:hover td | |
167 | + | color #009 | |
168 | + | ||
169 | + | /* The post title */ | |
170 | + | > .title | |
171 | + | font-size 1.5em | |
172 | + | ||
173 | + | /* Info below post title */ | |
174 | + | > .info | |
175 | + | margin 1em 0 | |
176 | + | ||
177 | + | > .username | |
178 | + | margin-left 1rem | |
179 | + | ||
180 | + | /* Post text */ | |
181 | + | > .text | |
182 | + | margin 2rem 0 | |
183 | + | word-wrap break-word | |
184 | + | ||
185 | + | /* The "new comment" form for this post page */ | |
186 | + | .new_comment | |
187 | + | > textarea | |
188 | + | height 5rem | |
189 | + | ||
190 | + | > input[type=submit] | |
191 | + | margin 1em 0 | |
192 | + | ||
193 | + | /* Comments tree for the Post page */ | |
194 | + | > .comments | |
195 | + | margin 5em 0 0 0 | |
196 | + | ||
197 | + | /* A single comment in the tree */ | |
198 | + | > .comment | |
199 | + | margin 0 0 1rem 0 | |
200 | + | overflow hidden | |
201 | + | ||
202 | + | /* Some info about this comment */ | |
203 | + | > .info | |
204 | + | display inline-block | |
205 | + | font-size .9rem | |
206 | + | ||
207 | + | > .username | |
208 | + | display inline-block | |
209 | + | margin 0 1rem | |
210 | + | ||
211 | + | > a, a:hover, a:visited | |
212 | + | display inline-block | |
213 | + | text-decoration none | |
214 | + | ||
215 | + | > .op | |
216 | + | background-color rgb(255, 175, 50) | |
217 | + | border-radius 4px | |
218 | + | font-weight bold | |
219 | + | padding 0 1rem | |
220 | + | ||
221 | + | > a, a:hover, a:visited | |
222 | + | color #fff | |
223 | + | ||
224 | + | /* The comment text */ | |
225 | + | > .text | |
226 | + | word-wrap break-word | |
227 | + | ||
228 | + | /* Give the comment that's linked to in the URL location hash a lightyellow background color */ | |
229 | + | .comment:target | |
230 | + | background-color lightyellow | |
231 | + | ||
232 | + | > .search | |
233 | + | margin-bottom 3rem | |
234 | + | ||
235 | + | /* User home page */ | |
236 | + | table.user | |
237 | + | /* If one length specified: both horizontal and vertical spacing | |
238 | + | * If two length specified: first sets the horizontal spacing, and | |
239 | + | * the second sets the vertical spacing | |
240 | + | */ | |
241 | + | border-spacing 2em 1em | |
242 | + | border-collapse separate | |
243 | + | margin auto | |
244 | + | width 80% | |
245 | + | ||
246 | + | tr | |
247 | + | > td:first-child | |
248 | + | font-weight bold | |
249 | + | text-align right | |
250 | + | vertical-align top | |
251 | + | width 30% | |
252 | + | ||
253 | + | > td:last-child | |
254 | + | text-align left | |
255 | + | ||
256 | + | /* User activity */ | |
257 | + | > .user_activity | |
258 | + | ||
259 | + | > * | |
260 | + | margin 0 0 2em 0 | |
261 | + | ||
262 | + | > .info | |
263 | + | color #888 | |
264 | + | ||
265 | + | /* Login page */ | |
266 | + | > .login | |
267 | + | margin auto | |
268 | + | max-width 20em | |
269 | + | ||
270 | + | input[type=submit] | |
271 | + | margin 1em 0 | |
272 | + | ||
273 | + | .title | |
274 | + | line-height 2em | |
275 | + | ||
276 | + | /* Page to edit a post or a comment */ | |
277 | + | > .edit | |
278 | + | { | |
279 | + | } | |
280 | + | ||
281 | + | /* Page to reply to a comment */ | |
282 | + | > .reply | |
283 | + | { | |
284 | + | } | |
285 | + | ||
286 | + | /* About page */ | |
287 | + | > .about | |
288 | + | ||
289 | + | > h3 | |
290 | + | margin 1em 0 .5em 0 | |
291 | + | ||
292 | + | > p | |
293 | + | line-height 1.5em | |
294 | + | ||
295 | + | > footer | |
296 | + | border-top 1px solid #ccc | |
297 | + | margin 3em 0 0 0 | |
298 | + | padding 2em 0 | |
299 | + | ||
300 | + | img | |
301 | + | height 1.2em | |
302 | + | margin 0 .5em 0 0 | |
303 | + | vertical-align middle | |
304 | + | ||
305 | + | > ul | |
306 | + | list-style none | |
307 | + | margin 0 | |
308 | + | overflow hidden | |
309 | + | padding 0 | |
310 | + | ||
311 | + | > li | |
312 | + | float left | |
313 | + | margin 0 2em 0 0 |
index 0000000..273c774 | |||
old size: 0B - new size: 5K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,195 @@ | |||
1 | + | * | |
2 | + | margin 0 | |
3 | + | padding 0 | |
4 | + | font-family "Helvetica Neue", Helvetica, Arial, sans-serif | |
5 | + | ||
6 | + | -moz-box-sizing border-box | |
7 | + | -webkit-box-sizing border-box | |
8 | + | box-sizing border-box | |
9 | + | ||
10 | + | a, a:hover, a:visited | |
11 | + | background transparent | |
12 | + | color #337ab7 | |
13 | + | text-decoration none | |
14 | + | ||
15 | + | blockquote | |
16 | + | background-color #f8f8f8 | |
17 | + | border-left 5px solid #e9e9e9 | |
18 | + | font-size .85em | |
19 | + | margin 1em 0 | |
20 | + | padding .5em 1em | |
21 | + | ||
22 | + | blockquote cite | |
23 | + | color #999 | |
24 | + | display block | |
25 | + | font-size .8em | |
26 | + | margin-top 1em | |
27 | + | ||
28 | + | blockquote cite:before | |
29 | + | content "\2014 \2009" | |
30 | + | ||
31 | + | h3 | |
32 | + | font-size 1.5em | |
33 | + | font-weight normal | |
34 | + | margin 1em 0 .5em 0 | |
35 | + | ||
36 | + | p | |
37 | + | margin 0 0 10px 0 | |
38 | + | ||
39 | + | .bg-green | |
40 | + | background-color #d9ffca | |
41 | + | border-radius 4px | |
42 | + | padding .5em 1em | |
43 | + | ||
44 | + | .bg-red | |
45 | + | background-color #f2dede | |
46 | + | border-radius 4px | |
47 | + | padding .5em 1em | |
48 | + | ||
49 | + | .bg-blue | |
50 | + | background-color #337ab7 | |
51 | + | border-radius 4px | |
52 | + | padding .5em 1em | |
53 | + | ||
54 | + | .bg-light-blue | |
55 | + | background-color #d9edf7 | |
56 | + | border-radius 4px | |
57 | + | padding .5em 1em | |
58 | + | ||
59 | + | /* Some styles for buttons */ | |
60 | + | .button | |
61 | + | border 0px | |
62 | + | border-radius 4px | |
63 | + | cursor pointer | |
64 | + | display inline-block | |
65 | + | padding .2em 1em | |
66 | + | text-align center | |
67 | + | ||
68 | + | .button_ok /* Green */ | |
69 | + | .button_ok:hover, | |
70 | + | .button_ok:visited | |
71 | + | background-color #4caf50 | |
72 | + | color #fff | |
73 | + | ||
74 | + | .button_info /* Blue */ | |
75 | + | .button_info:hover, | |
76 | + | .button_info:visited | |
77 | + | background-color #008cba | |
78 | + | color #fff | |
79 | + | ||
80 | + | .button_alert /* Red */ | |
81 | + | .button_alert:hover, | |
82 | + | .button_alert:visited | |
83 | + | background-color #f44336 | |
84 | + | color #fff | |
85 | + | ||
86 | + | .button_default /* Gray */ | |
87 | + | .button_default:hover, | |
88 | + | .button_default:visited | |
89 | + | background-color #e7e7e7 | |
90 | + | color #000 | |
91 | + | ||
92 | + | .button_default1, /* Black */ | |
93 | + | .button_default1:hover, | |
94 | + | .button_default1:visited | |
95 | + | background-color #555 | |
96 | + | color #fff | |
97 | + | ||
98 | + | img | |
99 | + | /* Prevent images from taking up too much space in comments */ | |
100 | + | max-width 100% | |
101 | + | ||
102 | + | label | |
103 | + | cursor pointer | |
104 | + | font-weight normal | |
105 | + | ||
106 | + | /* Add light blue shadow to form controls */ | |
107 | + | .form-control:focus | |
108 | + | border-color #66afe9 | |
109 | + | outline 0 | |
110 | + | -webkit-box-shadow inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) | |
111 | + | box-shadow inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) | |
112 | + | ||
113 | + | .form-control | |
114 | + | display block | |
115 | + | width 100% | |
116 | + | padding .5em 1em | |
117 | + | line-height 1.42857143 | |
118 | + | color #555 | |
119 | + | border 1px solid #ccc | |
120 | + | border-radius 4px | |
121 | + | -webkit-box-shadow inset 0 1px 1px rgba(0,0,0,.075) | |
122 | + | box-shadow inset 0 1px 1px rgba(0,0,0,.075) | |
123 | + | -webkit-transition border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s | |
124 | + | -o-transition border-color ease-in-out .15s,box-shadow ease-in-out .15s | |
125 | + | transition border-color ease-in-out .15s,box-shadow ease-in-out .15s | |
126 | + | ||
127 | + | /* When users vote, this <iframe/> is used as target, such that | |
128 | + | * the page is not reloaded | |
129 | + | */ | |
130 | + | .vote_sink | |
131 | + | height 1px; | |
132 | + | left -10px | |
133 | + | position fixed | |
134 | + | top -10px | |
135 | + | width 1px | |
136 | + | ||
137 | + | html, body | |
138 | + | background-color #fff | |
139 | + | font-size 1em | |
140 | + | height 100% | |
141 | + | line-height 1em | |
142 | + | margin 0 | |
143 | + | padding 0 | |
144 | + | width 100% | |
145 | + | ||
146 | + | pre | |
147 | + | background-color #f9f9f9 | |
148 | + | font-family "Courier 10 Pitch", Courier, monospace | |
149 | + | font-size 95% | |
150 | + | line-height 140% | |
151 | + | white-space pre | |
152 | + | white-space pre-wrap | |
153 | + | white-space -moz-pre-wrap | |
154 | + | white-space -o-pre-wrap | |
155 | + | ||
156 | + | code | |
157 | + | font-family Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace | |
158 | + | font-size 95% | |
159 | + | line-height 140% | |
160 | + | white-space pre | |
161 | + | white-space pre-wrap | |
162 | + | white-space -moz-pre-wrap | |
163 | + | white-space -o-pre-wrap | |
164 | + | ||
165 | + | /* Monospace <pre/> to write some nice ASCII art in frontpage */ | |
166 | + | pre.new_year | |
167 | + | background-color transparent | |
168 | + | color #BF0000 | |
169 | + | font-family monospace | |
170 | + | font-size .8rem | |
171 | + | font-webkit bold | |
172 | + | margin 0 0 2em 0 | |
173 | + | text-align center | |
174 | + | white-space pre | |
175 | + | white-space pre-wrap | |
176 | + | white-space -moz-pre-wrap | |
177 | + | white-space -o-pre-wrap | |
178 | + | ||
179 | + | /* Inline code */ | |
180 | + | p > code | |
181 | + | background-color #f5f5f5 | |
182 | + | border-radius 3px | |
183 | + | display inline-block | |
184 | + | font-family Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace | |
185 | + | font-size 85% | |
186 | + | line-height 140% | |
187 | + | margin 0 .2em | |
188 | + | padding .2em | |
189 | + | white-space pre | |
190 | + | white-space pre-wrap | |
191 | + | white-space -moz-pre-wrap | |
192 | + | white-space -o-pre-wrap | |
193 | + | ||
194 | + | ul, ol | |
195 | + | margin 1.2em 2em |
index 0000000..eeea2e0 | |||
old size: 0B - new size: 4K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,86 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'about' %} | |
5 | + | {% set title = 'About' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | <div class="about"> | |
9 | + | ||
10 | + | <p> | |
11 | + | We are a community of people interested in free software and free culture. Here at <em>freepost</em> we share interesting links, articles, and stories related to these topics. | |
12 | + | </p> | |
13 | + | <p> | |
14 | + | Please read this page before using the website. | |
15 | + | </p> | |
16 | + | ||
17 | + | <h3 id="who"> | |
18 | + | Who maintains <em>freepost</em>? | |
19 | + | </h3> | |
20 | + | <p> | |
21 | + | <em>freepost</em> is maintained by <a href="http://peers.community">Peers</a>. We are part of a highly motivated community of people with a strong interest in free culture. | |
22 | + | </p> | |
23 | + | ||
24 | + | <h3 id="what-to-post"> | |
25 | + | What to post | |
26 | + | </h3> | |
27 | + | <p> | |
28 | + | The general rule is: anything related to free software and free culture, that is interesting to you, and that you think other like-minded people will find interesting too. | |
29 | + | </p> | |
30 | + | <p> | |
31 | + | You are free to submit any links ranging from software, to art, science, and politics, as long as they have some interesting relevance with free culture. This means that most posts about proprietary software, politics, religion, sport, or meme are off-topic. | |
32 | + | </p> | |
33 | + | ||
34 | + | <h3 id="how-to-post"> | |
35 | + | How to post | |
36 | + | </h3> | |
37 | + | <p> | |
38 | + | Use a concise and (if possible) objective title. Do <strong>not</strong> use clickbait titles. | |
39 | + | </p> | |
40 | + | <p> | |
41 | + | Link to the original source whenever possible: avoid ads, paywalls, and content farms. | |
42 | + | </p> | |
43 | + | <p> | |
44 | + | Please do not submit too many links at once. | |
45 | + | </p> | |
46 | + | ||
47 | + | <h3 id="how-to-comment"> | |
48 | + | How to comment | |
49 | + | </h3> | |
50 | + | <p> | |
51 | + | In short: don't be a dick. Keep the discussion civil, do not insult other people, and do not start flamewars. Do not spam, and try to avoid "+1 comments" as well as unconstructive criticism. They add nothing to the conversation, and we try our best to keep discussions interesting. | |
52 | + | </p> | |
53 | + | <p> | |
54 | + | Unless malice is overwhelmingly evident, assume good intentions whenever reading and responding others’ comments. We do <strong>not</strong> tolerate insults, threats, or harassment. | |
55 | + | </p> | |
56 | + | ||
57 | + | <h3 id="terms-of-use"> | |
58 | + | Terms of Use | |
59 | + | </h3> | |
60 | + | <p> | |
61 | + | By using this website, you irrevocably agree to release your posts and comments under the <a href="http://creativecommons.org/licenses/by/4.0">CC BY 4.0</a> License. A link is sufficient for CC BY 4.0 attribution. | |
62 | + | </p> | |
63 | + | <p> | |
64 | + | The website is offered in the hope that it will be useful, but we do not assume any sort of legal responsibility or liability and we do not offer any sort of warranty. We cannot be held responsible for any misuse or money loss derived from the use of the website. | |
65 | + | </p> | |
66 | + | <p> | |
67 | + | By using the website you agree to accept the terms of services written on this page. | |
68 | + | </p> | |
69 | + | ||
70 | + | <h3 id="privacy-policy"> | |
71 | + | Privacy Policy | |
72 | + | </h3> | |
73 | + | <p> | |
74 | + | <em>freepost</em> can optionally store a user email address, which is only used to reset the user password. Emails are not shared with anybody and can be deleted at any time by the user. | |
75 | + | <br /> | |
76 | + | <em>freepost</em> is currently hosted by <a href="https://tuxfamily.org">TuxFamily</a>. TuxFamily keeps the web server raw access logs for up to a year, as required by French laws. Read more on their <a href="https://faq.tuxfamily.org/Privacy/En">FAQ</a>. | |
77 | + | </p> | |
78 | + | <p> | |
79 | + | If you wish to increase your anonymity while browsing <em>freepost</em>, we suggest to use <a href="https://www.torproject.org">Tor</a> and a random email address. | |
80 | + | </p> | |
81 | + | <p> | |
82 | + | <em>freepost</em> does <strong>not</strong> make use of any third-party service (for example CDNs). | |
83 | + | </p> | |
84 | + | ||
85 | + | </div> | |
86 | + | {% endblock %} |
index 0000000..96d835c | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,35 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {% block content %} | |
4 | + | ||
5 | + | <div class="edit"> | |
6 | + | <h3> | |
7 | + | Edit: {{ comment.postTitle }} | |
8 | + | </h3> | |
9 | + | ||
10 | + | <div class="info"> | |
11 | + | by <a href="{{ url ('user_public', username=comment.username) }}">{{ comment.username }}</a> | |
12 | + | <time title="{{ comment.created|title }}" datetime="{{ comment.created|datetime }}"><em> {{ comment.created|ago }} </em></time> | |
13 | + | </div> | |
14 | + | ||
15 | + | <div style="margin: 2em 0;"> | |
16 | + | {{ comment.text|md2html|safe }} | |
17 | + | </div> | |
18 | + | ||
19 | + | {# "shortcut-submit" is a class used exclusively from javascript | |
20 | + | # to submit the form when a key (Ctrl+Enter) is pressed. | |
21 | + | #} | |
22 | + | <form action="" method="post" class="shortcut-submit"> | |
23 | + | <input type="hidden" name="comment" value="{{ comment.hashId }}" /> | |
24 | + | ||
25 | + | <div style="margin: 2em 0;"> | |
26 | + | <textarea name="text" rows=10 class="form-control">{{ comment.text }}</textarea> | |
27 | + | </div> | |
28 | + | ||
29 | + | <div> | |
30 | + | <input type="submit" class="button button_info" value="Save changes" /> | |
31 | + | </div> | |
32 | + | </form> | |
33 | + | </div> | |
34 | + | ||
35 | + | {% endblock %} |
index 0000000..fd84c18 | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,37 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {% block content %} | |
4 | + | ||
5 | + | <div class="edit"> | |
6 | + | {# "shortcut-submit" is a class used exclusively from javascript | |
7 | + | # to submit the form when a key (Ctrl+Enter) is pressed. | |
8 | + | #} | |
9 | + | <form action="" method="post" class="submit shortcut-submit"> | |
10 | + | <input type="hidden" name="post" value="{{ post.hashId }}" /> | |
11 | + | ||
12 | + | <h3>Title <em>(required)</em></h3> | |
13 | + | <div> | |
14 | + | <input type="text" name="title" class="form-control" value="{{ post.title }}" required='required' /> | |
15 | + | </div> | |
16 | + | <div class="info"> | |
17 | + | by <a href="{{ url ('user_public', username=post.username) }}">{{ post.username }}</a> | |
18 | + | <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}"><em> {{ post.created|ago }} </em></time> | |
19 | + | </div> | |
20 | + | ||
21 | + | <h3>Link</h3> | |
22 | + | <div> | |
23 | + | <input type="text" name="link" class="form-control" value="{{ post.link }}" /> | |
24 | + | </div> | |
25 | + | ||
26 | + | <h3>Text</h3> | |
27 | + | <div> | |
28 | + | <textarea name="text" rows=10 class="form-control">{{ post.text }}</textarea> | |
29 | + | </div> | |
30 | + | ||
31 | + | <div style="margin: 1em 0 0 0;"> | |
32 | + | <input type="submit" class="button button_info" value="Save changes" /> | |
33 | + | </div> | |
34 | + | </form> | |
35 | + | </div> | |
36 | + | ||
37 | + | {% endblock %} |
index 0000000..543685a | |||
old size: 0B - new size: 3K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,98 @@ | |||
1 | + | {% from 'vote.html' import vote %} | |
2 | + | ||
3 | + | {% extends 'layout.html' %} | |
4 | + | ||
5 | + | {# Set variables for base layour #} | |
6 | + | {% set active_page = sorting %} | |
7 | + | {% set title = '' %} | |
8 | + | ||
9 | + | {% block content %} | |
10 | + | <div class="posts"> | |
11 | + | ||
12 | + | {# banner | |
13 | + | <div class="bg-green" style="margin: 0 0 2em 0; padding: .5em;"> | |
14 | + | <img alt="" title="" src="images/pulse.gif" style="height: 1em;" /> | |
15 | + | <a href="https://fosdem.org/2018/schedule/streaming/">FOSDEM 2018</a> | |
16 | + | </div> | |
17 | + | #} | |
18 | + | ||
19 | + | {# Welcome New Year. | |
20 | + | # To maintain alignment, keep al lines below of the same length | |
21 | + | # and with no padding. | |
22 | + | <pre class="new_year"> | |
23 | + | .''. | |
24 | + | . *''* :_\/_: | |
25 | + | _\(/_ .:.*_\/_* : /\ : | |
26 | + | ./)\ ':'* /\ * : '..'. | |
27 | + | ' *''* * '.\'/.' _\(/_' | |
28 | + | *_\/_* -= o =- /)\ | |
29 | + | * /\ * .'/.\'. ' | |
30 | + | *..* : | |
31 | + | ___ ___ __ ___ | |
32 | + | |__ \ / _ \/_ |/ _ \ | |
33 | + | ) | | | || | (_) | | |
34 | + | / /| | | || |> _ < | |
35 | + | / /_| |_| || | (_) | | |
36 | + | |____|\___/ |_|\___/ | |
37 | + | </pre> | |
38 | + | #} | |
39 | + | ||
40 | + | {% for post in posts %} | |
41 | + | ||
42 | + | <div class="post"> | |
43 | + | <div class="title"> | |
44 | + | {% if post.link|length > 0 %} | |
45 | + | <a href="{{ post.link }}">{{ post.title }}</a> | |
46 | + | {% else %} | |
47 | + | <a href="post/{{ post.hashId }}">{{ post.title }}</a> | |
48 | + | {% endif %} | |
49 | + | </div> | |
50 | + | ||
51 | + | {% if post.link %} | |
52 | + | <div class="netloc"> | |
53 | + | {{ post.link|netloc }} | |
54 | + | </div> | |
55 | + | {% endif %} | |
56 | + | ||
57 | + | <div class="info"> | |
58 | + | {{ vote ('post', post, user) }} | |
59 | + | ||
60 | + | <em class="username"> | |
61 | + | <a href="post/{{ post.hashId }}"> | |
62 | + | <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}"> | |
63 | + | {{ post.created|ago }} | |
64 | + | </time> | |
65 | + | </a> | |
66 | + | </em> | |
67 | + | by | |
68 | + | <a href="{{ url ('user_public', username=post.username) }}"> | |
69 | + | {{ post.username }} | |
70 | + | </a> | |
71 | + | — | |
72 | + | <a href="post/{{ post.hashId }}#comments"> | |
73 | + | {% if post.commentsCount > 0 %} | |
74 | + | {{ post.commentsCount }} comments | |
75 | + | {% else %} | |
76 | + | discuss | |
77 | + | {% endif %} | |
78 | + | </a> | |
79 | + | </div> | |
80 | + | </div> | |
81 | + | ||
82 | + | {% endfor %} | |
83 | + | ||
84 | + | <div class="more"> | |
85 | + | {% if page_number > 0 %} | |
86 | + | <a href="{{ url ('homepage') if page_number == 1 else '?page=' ~ (page_number - 1) }}" class="button button_default1"> | |
87 | + | Previous | |
88 | + | </a> | |
89 | + | {% endif %} | |
90 | + | ||
91 | + | <a href="?page={{ page_number + 1 }}" class="button button_default1"> | |
92 | + | Next | |
93 | + | </a> | |
94 | + | </div> | |
95 | + | </div> | |
96 | + | ||
97 | + | ||
98 | + | {% endblock %} |
index 0000000..a583fab | |||
old size: 0B - new size: 4K | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,91 @@ | |||
1 | + | {% set user = session_user () %} | |
2 | + | ||
3 | + | <!DOCTYPE html> | |
4 | + | <html lang="en"> | |
5 | + | <head> | |
6 | + | <meta charset="utf-8"> | |
7 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
8 | + | ||
9 | + | <link href="/css/freepost.css" rel="stylesheet"> | |
10 | + | ||
11 | + | <title>{{ title ~ ' - ' if title else '' }}freepost</title> | |
12 | + | </head> | |
13 | + | ||
14 | + | <body> | |
15 | + | <div class="container"> | |
16 | + | ||
17 | + | <div class="header"> | |
18 | + | <div class="menu"> | |
19 | + | <a href="/" class="flex-item logo"> | |
20 | + | freepost | |
21 | + | <img alt="🐵 " title="freepost" src="/images/freepost.png" class="monkey" /> | |
22 | + | </a> | |
23 | + | <a href="/" class="flex-item {{ "active_page" if active_page == "hot" else "" }}">Hot</a> | |
24 | + | <a href="/new" class="flex-item {{ "active_page" if active_page == "new" else "" }}">New</a> | |
25 | + | <a href="/search" class="flex-item {{ "active_page" if active_page == "search" else "" }}">Search</a> | |
26 | + | ||
27 | + | {% if user %} | |
28 | + | {% set unread_messages = new_messages (user.id) %} | |
29 | + | ||
30 | + | <a href="/submit" class="flex-item {{ "active_page" if active_page == "submit" else "" }}">Submit</a> | |
31 | + | ||
32 | + | {% if unread_messages %} | |
33 | + | <a href="/user_activity/replies" class="new_messages flex-item"> | |
34 | + | {{ user.username }} ({{ unread_messages }}) | |
35 | + | </a> | |
36 | + | {% else %} | |
37 | + | <a href="/user" class="flex-item {{ "active_page" if active_page == "user" else "" }}"> | |
38 | + | {{ user.username }} | |
39 | + | </a> | |
40 | + | {% endif %} | |
41 | + | {% endif %} | |
42 | + | ||
43 | + | <a href="/about" class="flex-item {{ "active_page" if active_page == "about" else "" }}">About</a> | |
44 | + | ||
45 | + | {% if user %} | |
46 | + | <a href="/logout" class="flex-item">Log out</a> | |
47 | + | {% else %} | |
48 | + | <a href="/login" class="flex-item {{ "active_page" if active_page == "login" else "" }}">Log in</a> | |
49 | + | {% endif %} | |
50 | + | </div> | |
51 | + | </div> | |
52 | + | ||
53 | + | <div class="content"> | |
54 | + | {% block content %}{% endblock %} | |
55 | + | </div> | |
56 | + | ||
57 | + | <footer> | |
58 | + | <p> | |
59 | + | Text is available under a <a href="http://creativecommons.org/licenses/by/4.0">Creative Commons Attribution 4.0 International License</a>. | |
60 | + | </p> | |
61 | + | ||
62 | + | <ul> | |
63 | + | <li> | |
64 | + | <img alt="Peers" title="" src="/images/peers.png" /> | |
65 | + | <a href="http://peers.community">Peers</a> | |
66 | + | </li> | |
67 | + | <li> | |
68 | + | <img alt="RSS" title="" src="/images/rss.png" /> | |
69 | + | <a href="/rss/hot">Hot</a> • | |
70 | + | <a href="/rss/new">New</a> | |
71 | + | </li> | |
72 | + | <li> | |
73 | + | <img alt="Source" title="" src="/images/source.png" /> | |
74 | + | <a href="https://notabug.org/zPlus/freepost">Source code</a> | |
75 | + | </li> | |
76 | + | <li> | |
77 | + | <img alt="TuxFamily" title="" src="/images/tuxfamily.png" /> | |
78 | + | Hosted by <a href="https://tuxfamily.org">TuxFamily</a> | |
79 | + | </li> | |
80 | + | </ul> | |
81 | + | </footer> | |
82 | + | </div> | |
83 | + | ||
84 | + | {# When users vote, this <iframe/> is used as target, such that | |
85 | + | # the page is not reloaded | |
86 | + | #} | |
87 | + | <iframe name="vote_sink" class="vote_sink"></iframe> | |
88 | + | ||
89 | + | <script src="/javascript/freepost.js"></script> | |
90 | + | </body> | |
91 | + | </html> |
index 0000000..5bf5db2 | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,53 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'login' %} | |
5 | + | {% set title = 'Login' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | <div class="login"> | |
9 | + | {% if flash %} | |
10 | + | <div class="alert bg-red"> | |
11 | + | {{ flash }} | |
12 | + | </div> | |
13 | + | {% endif %} | |
14 | + | ||
15 | + | <h3>Log in</h3> | |
16 | + | ||
17 | + | <form action="" method="post"> | |
18 | + | <div class="title"> | |
19 | + | Screen name | |
20 | + | </div> | |
21 | + | <div> | |
22 | + | <input type="text" name="username" class="form-control" /> | |
23 | + | </div> | |
24 | + | ||
25 | + | <div class="title"> | |
26 | + | Password | |
27 | + | </div> | |
28 | + | <div> | |
29 | + | <input type="password" name="password" class="form-control" /> | |
30 | + | </div> | |
31 | + | ||
32 | + | <div> | |
33 | + | <label><input type="checkbox" name="remember" /> Remember me</label> | |
34 | + | </div> | |
35 | + | ||
36 | + | <div> | |
37 | + | <input type="submit" name="login" class="button button_info" value="Login" /> | |
38 | + | </div> | |
39 | + | </form> | |
40 | + | ||
41 | + | <div> | |
42 | + | <a href="login_reset">Reset password</a> | |
43 | + | </div> | |
44 | + | <div> | |
45 | + | <a href="{{ url ('register') }}">Create new account</a> | |
46 | + | </div> | |
47 | + | </div> | |
48 | + | ||
49 | + | {% endblock %} | |
50 | + | ||
51 | + | ||
52 | + | ||
53 | + |
index 0000000..fb3ca1b | |||
old size: 0B - new size: 4K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,104 @@ | |||
1 | + | {% from 'vote.html' import vote %} | |
2 | + | ||
3 | + | {% extends 'layout.html' %} | |
4 | + | ||
5 | + | {# Set variables for base layour #} | |
6 | + | {% set active_page = '' %} | |
7 | + | {% set title = post.title %} | |
8 | + | ||
9 | + | {% block content %} | |
10 | + | ||
11 | + | <div class="post"> | |
12 | + | ||
13 | + | <div class="title"> | |
14 | + | {% if post.link and post.link|length > 0 %} | |
15 | + | <a href="{{ post.link }}"> | |
16 | + | {{ post.title }} | |
17 | + | </a> | |
18 | + | {% else %} | |
19 | + | {{ post.title }} | |
20 | + | {% endif %} | |
21 | + | </div> | |
22 | + | ||
23 | + | {% if post.link %} | |
24 | + | <div class="netloc"> | |
25 | + | {{ post.link|netloc }} | |
26 | + | </div> | |
27 | + | {% endif %} | |
28 | + | ||
29 | + | <div class="info"> | |
30 | + | {{ vote ('post', post, user) }} | |
31 | + | ||
32 | + | <a href="{{ url ('user_public', username=post.username) }}" class="username"> | |
33 | + | {{ post.username }} | |
34 | + | </a> | |
35 | + | <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}"> | |
36 | + | <em>{{ post.created|ago }}</em> | |
37 | + | </time> | |
38 | + | ||
39 | + | — {{ post.vote }} votes, <a href="#comments">{{ post.commentsCount }} comments</a> | |
40 | + | ||
41 | + | {% if user and post.userId == user.id %} | |
42 | + | — <a href="{{ url ('edit_post', hash_id=post.hashId) }}">Edit</a> | |
43 | + | {% endif %} | |
44 | + | </div> | |
45 | + | ||
46 | + | <div class="text"> | |
47 | + | {{ post.text|md2html|safe }} | |
48 | + | </div> | |
49 | + | ||
50 | + | {# "shortcut-submit" is a class used exclusively from javascript | |
51 | + | # to submit the form when a key (Ctrl+Enter) is pressed. | |
52 | + | #} | |
53 | + | <form action="" method="post" class="new_comment shortcut-submit"> | |
54 | + | <textarea | |
55 | + | name="new_comment" | |
56 | + | required="required" | |
57 | + | class="form-control" | |
58 | + | placeholder="{{ 'Write a comment' if user else 'Login to post a comment' }}" | |
59 | + | {{ 'disabled' if not user }}></textarea> | |
60 | + | <input | |
61 | + | type="submit" | |
62 | + | value="Add comment" | |
63 | + | class="button button_info" | |
64 | + | {{ 'disabled' if not user }}/> | |
65 | + | </form> | |
66 | + | ||
67 | + | {# id="" used as anchor #} | |
68 | + | <div class="comments" id="comments"> | |
69 | + | {% for comment in comments %} | |
70 | + | {# The id="" is used as anchor #} | |
71 | + | <div class="comment" style="margin-left:{{ comment.depth * 2 }}em" id="comment-{{ comment.hashId }}"> | |
72 | + | <div class="info"> | |
73 | + | {{ vote ('comment', comment, user) }} | |
74 | + | ||
75 | + | {# Username #} | |
76 | + | <span class="username {{ 'op' if post.userId == comment.userId else '' }}"> | |
77 | + | <a href="{{ url ('user_public', username=comment.username) }}">{{ comment.username }}</a> | |
78 | + | </span> | |
79 | + | ||
80 | + | {# DateTime #} | |
81 | + | <a href="{{ url ('post', hash_id=post.hashId) ~ '#comment-' ~ comment.hashId }}"><time title="{{ comment.created|title }}" datetime="{{ comment.created|datetime }}"><em> {{ comment.created|ago }} </em></time></a> | |
82 | + | ||
83 | + | {% if user %} | |
84 | + | — | |
85 | + | ||
86 | + | {# Reply #} | |
87 | + | <a href="{{ url ('reply', hash_id=comment.hashId) }}">Reply</a> | |
88 | + | ||
89 | + | {# Edit #} | |
90 | + | {% if comment.userId == user.id %} | |
91 | + | <a href="{{ url ('edit_comment', hash_id=comment.hashId) }}">Edit</a> | |
92 | + | {% endif %} | |
93 | + | {% endif %} | |
94 | + | </div> | |
95 | + | ||
96 | + | <div class="text"> | |
97 | + | {{ comment.text|md2html|safe }} | |
98 | + | </div> | |
99 | + | </div> | |
100 | + | {% endfor %} | |
101 | + | </div> | |
102 | + | </div> | |
103 | + | ||
104 | + | {% endblock %} |
index 0000000..20008e0 | |||
old size: 0B - new size: 1002B | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,43 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'login' %} | |
5 | + | {% set title = 'Register' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | <div class="login"> | |
9 | + | {% if flash %} | |
10 | + | <div class="alert bg-red"> | |
11 | + | {{ flash }} | |
12 | + | </div> | |
13 | + | {% endif %} | |
14 | + | ||
15 | + | <h3>Create new account</h3> | |
16 | + | ||
17 | + | <form action="" method="post"> | |
18 | + | <div class="title"> | |
19 | + | Screen name | |
20 | + | </div> | |
21 | + | <div> | |
22 | + | <input type="text" name="username" class="form-control" /> | |
23 | + | </div> | |
24 | + | ||
25 | + | <div class="title"> | |
26 | + | Password | |
27 | + | </div> | |
28 | + | <div> | |
29 | + | <input type="password" name="password" class="form-control" /> | |
30 | + | <em>At least 8 characters</em> | |
31 | + | </div> | |
32 | + | ||
33 | + | <div> | |
34 | + | <input type="submit" name="new_account" class="button button_info" value="Create account" /> | |
35 | + | </div> | |
36 | + | </form> | |
37 | + | </div> | |
38 | + | ||
39 | + | {% endblock %} | |
40 | + | ||
41 | + | ||
42 | + | ||
43 | + |
index 0000000..ca07c9c | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,33 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {% block content %} | |
4 | + | ||
5 | + | <div class="reply"> | |
6 | + | <h3>Reply to <em><a href="{{ url ('user_public', username=comment.username) }}">{{ comment.username }}</a></em></h3> | |
7 | + | ||
8 | + | <div class="info"> | |
9 | + | posted <em><a href="{{ url ('post', hash_id=comment.postHashId) }}#comment-{{ comment.hashId }}"><time title="{{ comment.created|title }}" datetime="{{ comment.created|datetime }}"> {{ comment.created|ago }} </time></a></em> | |
10 | + | on <a href="{{ url ('post', hash_id=comment.postHashId) }}">{{ comment.postTitle }}</a> | |
11 | + | </div> | |
12 | + | ||
13 | + | <div style="margin: 2em 0;"> | |
14 | + | {{ comment.text|md2html|safe }} | |
15 | + | </div> | |
16 | + | ||
17 | + | {# "shortcut-submit" is a class used exclusively from javascript | |
18 | + | # to submit the form when a key (Ctrl+Enter) is pressed. | |
19 | + | #} | |
20 | + | <form action="" method="post" class="shortcut-submit"> | |
21 | + | <input type="hidden" name="parent_comment" value="{{ comment.hashId }}" /> | |
22 | + | ||
23 | + | <div style="margin: 2em 0;"> | |
24 | + | <textarea name="text" required="required" rows=10 class="form-control"></textarea> | |
25 | + | </div> | |
26 | + | ||
27 | + | <div> | |
28 | + | <input type="submit" class="button button_info" value="Reply" /> | |
29 | + | </div> | |
30 | + | </form> | |
31 | + | </div> | |
32 | + | ||
33 | + | {% endblock %} |
index 0000000..04eb682 | |||
old size: 0B - new size: 1K | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,27 @@ | |||
1 | + | <?xml version="1.0" encoding="UTF-8" ?> | |
2 | + | <rss version="2.0"> | |
3 | + | <channel> | |
4 | + | <title>freepost</title> | |
5 | + | <description></description> | |
6 | + | <link>{{ base_url }}</link> | |
7 | + | <lastBuildDate>{{ now () }}</lastBuildDate> | |
8 | + | ||
9 | + | {% for post in posts %} | |
10 | + | {# freepost URL of this post #} | |
11 | + | {% set freepost_url = base_url ~ url ('post', hash_id=post.hashId) %} | |
12 | + | ||
13 | + | <item> | |
14 | + | <guid isPermaLink="false">{{ post.hashId }}</guid> | |
15 | + | <title>{{ post.title }}</title> | |
16 | + | <description> | |
17 | + | <p>by {{ post.username }} — {{ post.vote }} votes, <a href="{{ freepost_url }}">{{ post.commentsCount ~ ' comments' if post.commentsCount > 0 else 'discuss' }}</a></p> | |
18 | + | <p>{{ post.text }}</p> | |
19 | + | </description> | |
20 | + | <link>{{ post.link if post.link and post.link|length > 0 else freepost_url }}</link> | |
21 | + | <freepostLink>{{ freepost_url }}</freepostLink> | |
22 | + | <pubDate>{{ post.created }}</pubDate> | |
23 | + | <author>{{ post.username }}</author> | |
24 | + | </item> | |
25 | + | {% endfor %} | |
26 | + | </channel> | |
27 | + | </rss> |
index 0000000..4c972fd | |||
old size: 0B - new size: 2K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,76 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'search' %} | |
5 | + | {% set title = 'Search' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <div class="search"> | |
10 | + | <form action="/search"> | |
11 | + | <p> | |
12 | + | <input type="text" name="q" value="{{ query if query else '' }}" placeholder="Search..." required="required" /> | |
13 | + | <input type="submit" value="Search" /> | |
14 | + | </p> | |
15 | + | <p> | |
16 | + | Order by | |
17 | + | <label> | |
18 | + | <input type="radio" name="order" value="newest" {{ 'checked' if order == 'newest' }}> | |
19 | + | Most recent | |
20 | + | </label> | |
21 | + | <label> | |
22 | + | <input type="radio" name="order" value="points" {{ 'checked' if order == 'points' }}> | |
23 | + | Points | |
24 | + | </label> | |
25 | + | </p> | |
26 | + | </form> | |
27 | + | </div> | |
28 | + | ||
29 | + | <div class="posts"> | |
30 | + | ||
31 | + | {% for post in results %} | |
32 | + | ||
33 | + | <div class="post"> | |
34 | + | <div class="title"> | |
35 | + | {% if post.link and post.link|length > 0 %} | |
36 | + | <a href="{{ post.link }}"> | |
37 | + | {{ post.title }} | |
38 | + | </a> | |
39 | + | {% else %} | |
40 | + | <a href="{{ url ('post', hash_id=post.hashId) }}"> | |
41 | + | {{ post.title }} | |
42 | + | </a> | |
43 | + | {% endif %} | |
44 | + | </div> | |
45 | + | ||
46 | + | <div class="info"> | |
47 | + | <em> | |
48 | + | <a href="{{ url ('post', hash_id=post.hashId) }}"> | |
49 | + | <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}"> | |
50 | + | {{ post.created|ago }} | |
51 | + | </time> | |
52 | + | </a> | |
53 | + | </em> | |
54 | + | by | |
55 | + | <a href="{{ url ('user_public', username=post.username) }}"> | |
56 | + | {{ post.username }} | |
57 | + | </a> | |
58 | + | — | |
59 | + | <a href="{{ url ('post', hash_id=post.hashId) }}#comments"> | |
60 | + | {{ post.commentsCount if post.commentsCount else '' }} comments | |
61 | + | </a> | |
62 | + | </div> | |
63 | + | </div> | |
64 | + | ||
65 | + | {% endfor %} | |
66 | + | ||
67 | + | {# Add once I'll have fulltext search | |
68 | + | <div class="more"> | |
69 | + | <a href="?page={{ page + 1 }}" class="button button_default1"> | |
70 | + | More | |
71 | + | </a> | |
72 | + | </div> | |
73 | + | #} | |
74 | + | </div> | |
75 | + | ||
76 | + | {% endblock %} |
index 0000000..f75dfbd | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,39 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'submit' %} | |
5 | + | {% set title = 'Submit' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | {% if flash %} | |
10 | + | <div class="alert bg-red"> | |
11 | + | {{ flash }} | |
12 | + | </div> | |
13 | + | {% endif %} | |
14 | + | ||
15 | + | {# "shortcut-submit" is a class used exclusively from javascript | |
16 | + | # to submit the form when a key (Ctrl+Enter) is pressed. | |
17 | + | #} | |
18 | + | <form action="" method="post" class="submit shortcut-submit"> | |
19 | + | <h3>Title <em>(required)</em></h3> | |
20 | + | <div> | |
21 | + | <input type="text" name="title" class="form-control" required='required' /> | |
22 | + | </div> | |
23 | + | ||
24 | + | <h3>Link</h3> | |
25 | + | <div> | |
26 | + | <input type="text" name="link" class="form-control" /> | |
27 | + | </div> | |
28 | + | ||
29 | + | <h3>Text</h3> | |
30 | + | <div> | |
31 | + | <textarea name="text" rows=10 class="form-control"></textarea> | |
32 | + | </div> | |
33 | + | ||
34 | + | <div style="margin: 1em 0 0 0;"> | |
35 | + | <input type="submit" class="button button_info" value="Submit post" /> | |
36 | + | </div> | |
37 | + | </form> | |
38 | + | ||
39 | + | {% endblock %} |
index 0000000..9ffabe4 | |||
old size: 0B - new size: 832B | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,28 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'user' %} | |
5 | + | {% set title = '' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <div class="user_activity"> | |
10 | + | ||
11 | + | {% for comment in comments %} | |
12 | + | ||
13 | + | <div> | |
14 | + | {{ comment.text|md2html|safe }} | |
15 | + | ||
16 | + | {# Post info #} | |
17 | + | <div class="info"> | |
18 | + | <a href="../post/{{ comment.postHashId }}#comment-{{ comment.hashId }}">read</a> | |
19 | + | — | |
20 | + | <time title="{{ comment.created|title }}" datetime="{{ comment.created|datetime }}"><em> {{ comment.created|ago }} </em></time> on <a href="../post/{{ comment.postHashId }}">{{ comment.postTitle }}</a> | |
21 | + | </div> | |
22 | + | </div> | |
23 | + | ||
24 | + | {% endfor %} | |
25 | + | ||
26 | + | </div> | |
27 | + | ||
28 | + | {% endblock %} |
index 0000000..b08347e | |||
old size: 0B - new size: 1K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,38 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'user' %} | |
5 | + | {% set title = '' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <div class="user_activity"> | |
10 | + | ||
11 | + | {% for post in posts %} | |
12 | + | ||
13 | + | <div> | |
14 | + | {# Post title #} | |
15 | + | {% if post.link|length > 0 %} | |
16 | + | <a href="{{ post.link }}"> | |
17 | + | {{ post.title }} | |
18 | + | </a> | |
19 | + | {% else %} | |
20 | + | <a href="../post/{{ post.hashId }}"> | |
21 | + | {{ post.title }} | |
22 | + | </a> | |
23 | + | {% endif %} | |
24 | + | ||
25 | + | {# Post info #} | |
26 | + | <div class="info"> | |
27 | + | {{ post.vote }} votes, | |
28 | + | <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}"><em> {{ post.created|ago }} </em></time> | |
29 | + | — | |
30 | + | <a href="../post/{{ post.hashId }}">{{ post.commentsCount }} comments</a> | |
31 | + | </div> | |
32 | + | </div> | |
33 | + | ||
34 | + | {% endfor %} | |
35 | + | ||
36 | + | </div> | |
37 | + | ||
38 | + | {% endblock %} |
index 0000000..73a6b7e | |||
old size: 0B - new size: 2K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,70 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'user' %} | |
5 | + | {% set title = '' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <form action="" method="post"> | |
10 | + | <table class="user"> | |
11 | + | <tr> | |
12 | + | <td> | |
13 | + | User | |
14 | + | </td> | |
15 | + | <td> | |
16 | + | {{ user.username }} | |
17 | + | ||
18 | + | <div><a href="/user_activity/posts">Your posts</a></div> | |
19 | + | <div><a href="/user_activity/comments">Your comments</a></div> | |
20 | + | <div><a href="/user_activity/replies">Replies to your comments</a></div> | |
21 | + | </td> | |
22 | + | </tr> | |
23 | + | ||
24 | + | <tr> | |
25 | + | <td> | |
26 | + | Since | |
27 | + | </td> | |
28 | + | <td> | |
29 | + | {{ user.registered|datetime }} ({{ user.registered|ago }}) | |
30 | + | </td> | |
31 | + | </tr> | |
32 | + | ||
33 | + | <tr> | |
34 | + | <td> | |
35 | + | About | |
36 | + | </td> | |
37 | + | <td> | |
38 | + | <textarea name="about" class="form-control">{{ user.about }}</textarea> | |
39 | + | </td> | |
40 | + | </tr> | |
41 | + | ||
42 | + | <tr> | |
43 | + | <td> | |
44 | + | ||
45 | + | </td> | |
46 | + | <td> | |
47 | + | <input type="text" name="email" class="form-control" value="{{ user.email if user.email else '' }}" /> | |
48 | + | <em>Required if you wish to change your password</em> | |
49 | + | {# | |
50 | + | <p> | |
51 | + | <label> | |
52 | + | <input type="checkbox" name="email_notifications" {{ user.email_notifications ? 'checked="checked"' }} /> | |
53 | + | Send notifications via email | |
54 | + | </label> | |
55 | + | </p> | |
56 | + | #} | |
57 | + | </td> | |
58 | + | </tr> | |
59 | + | ||
60 | + | <tr> | |
61 | + | <td> | |
62 | + | </td> | |
63 | + | <td> | |
64 | + | <input type="submit" name="update" value="Update" class="button button_info" /> | |
65 | + | </td> | |
66 | + | </tr> | |
67 | + | </table> | |
68 | + | </form> | |
69 | + | ||
70 | + | {% endblock %} |
index 0000000..137b536 | |||
old size: 0B - new size: 720B | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,38 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'user' %} | |
5 | + | {% set title = '' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <table class="user"> | |
10 | + | <tr> | |
11 | + | <td> | |
12 | + | User | |
13 | + | </td> | |
14 | + | <td> | |
15 | + | {{ account.username }} | |
16 | + | </td> | |
17 | + | </tr> | |
18 | + | ||
19 | + | <tr> | |
20 | + | <td> | |
21 | + | Since | |
22 | + | </td> | |
23 | + | <td> | |
24 | + | {{ account.registered|datetime }} ({{ account.registered|ago }}) | |
25 | + | </td> | |
26 | + | </tr> | |
27 | + | ||
28 | + | <tr> | |
29 | + | <td> | |
30 | + | About | |
31 | + | </td> | |
32 | + | <td> | |
33 | + | {{ account.about|md2html|safe }} | |
34 | + | </td> | |
35 | + | </tr> | |
36 | + | </table> | |
37 | + | ||
38 | + | {% endblock %} |
index 0000000..494d649 | |||
old size: 0B - new size: 1002B | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,29 @@ | |||
1 | + | {% extends 'layout.html' %} | |
2 | + | ||
3 | + | {# Set variables for base layour #} | |
4 | + | {% set active_page = 'user' %} | |
5 | + | {% set title = '' %} | |
6 | + | ||
7 | + | {% block content %} | |
8 | + | ||
9 | + | <div class="user_activity"> | |
10 | + | ||
11 | + | {% for comment in replies %} | |
12 | + | ||
13 | + | <div> | |
14 | + | {{ comment.text|md2html|safe }} | |
15 | + | ||
16 | + | {# Post info #} | |
17 | + | <div class="info"> | |
18 | + | <a href="../post/{{ comment.postHashId }}#comment-{{ comment.hashId }}">read</a> | |
19 | + | <a href="../reply?comment={{ comment.hashId }}">reply</a> | |
20 | + | — | |
21 | + | by <a href="{{ url ("user_public", username=comment.username) }}">{{ comment.username }}</a> <time title="{{ comment.created|title }}" datetime="{{ comment.created|datetime }}"><em> {{ comment.created|ago }} </em></time> on <a href="../post/{{ comment.postHashId }}">{{ comment.postTitle }}</a> | |
22 | + | </div> | |
23 | + | </div> | |
24 | + | ||
25 | + | {% endfor %} | |
26 | + | ||
27 | + | </div> | |
28 | + | ||
29 | + | {% endblock %} |
index 0000000..274ad3a | |||
old size: 0B - new size: 2K | |||
new file mode: -rwxr-xr-x |
@@ -0,0 +1,46 @@ | |||
1 | + | {# Template for up/down vote arrows. | |
2 | + | # This template expects these inputs | |
3 | + | # | |
4 | + | # - target: ('post', 'comment') | |
5 | + | # - item: either a "post" object or a "comment" | |
6 | + | # - user: reference to logged in user | |
7 | + | #} | |
8 | + | ||
9 | + | {% macro vote (target, item, user) %} | |
10 | + | ||
11 | + | <span class="vote"> | |
12 | + | ||
13 | + | {% if user %} | |
14 | + | ||
15 | + | <form action="{{ url ('vote') }}" target="vote_sink" method="post"> | |
16 | + | <input type="hidden" name="target" value="{{ target }}" /> | |
17 | + | <input type="hidden" name="{{ target }}" value="{{ item.hashId }}" /> | |
18 | + | <input type="hidden" name="updown" value="up" /> | |
19 | + | ||
20 | + | <button title="upvote" class="{{ 'upvoted' if item.user_vote == 1 else '' }}"> | |
21 | + | ˄ | |
22 | + | </button> | |
23 | + | </form> | |
24 | + | ||
25 | + | {# Show number of votes #} | |
26 | + | <span class="count">{{ item.vote }}</span> | |
27 | + | ||
28 | + | <form action="{{ url ('vote') }}" target="vote_sink" method="post"> | |
29 | + | <input type="hidden" name="target" value="{{ target }}" /> | |
30 | + | <input type="hidden" name="{{ target }}" value="{{ item.hashId }}" /> | |
31 | + | <input type="hidden" name="updown" value="down" /> | |
32 | + | ||
33 | + | <button title="downvote" class="{{ 'downvoted' if item.user_vote == -1 else '' }}"> | |
34 | + | ˅ | |
35 | + | </button> | |
36 | + | </form> | |
37 | + | ||
38 | + | {% else %} | |
39 | + | ||
40 | + | {{ item.vote ~ ' vote' ~ ('s' if item.vote != 1 else '') }} | |
41 | + | ||
42 | + | {% endif %} | |
43 | + | ||
44 | + | </span> | |
45 | + | ||
46 | + | {% endmacro %} |
index 91e0401..b0bd0eb | |||
old size: 89B - new size: 71B | |||
@@ -1,4 +1,9 @@ | |||
1 | - | bottle == 0.12.* | |
2 | - | pyld == 1.* | |
3 | - | PyMySQL == 0.9.* | |
4 | - | requests == 2.* | |
1 | + | bleach | |
2 | + | bottle | |
3 | + | jinja2 | |
4 | + | markdown | |
5 | + | mysqlclient | |
6 | + | pyld | |
7 | + | pyyaml | |
8 | + | requests | |
9 | + | timeago |
index 1451b85..0000000 | |||
old size: 69B - new size: 0B | |||
deleted file mode: -rw-r--r-- |
@@ -1,4 +0,0 @@ | |||
1 | - | # This is a bunch of settings useful for the app | |
2 | - | ||
3 | - | [name] | |
4 | - | key = value |
index 0000000..1ccff06 | |||
old size: 0B - new size: 749B | |||
new file mode: -rw-r--r-- |
@@ -0,0 +1,29 @@ | |||
1 | + | # This is a bunch of settings useful for the app | |
2 | + | ||
3 | + | defaults: | |
4 | + | items_per_page: 50 | |
5 | + | search_results_per_page: 50 | |
6 | + | ||
7 | + | session: | |
8 | + | # Name to use for the session cookie | |
9 | + | name: freepost | |
10 | + | ||
11 | + | # Timeout in seconds for the "remember me" option. | |
12 | + | # By default, if the user doesn't click "remember me" during login the | |
13 | + | # session will end when the browser is closed. | |
14 | + | # 2592000 = 30 days | |
15 | + | remember_me: 2592000 | |
16 | + | ||
17 | + | cookies: | |
18 | + | # A secret key for signing cookies. Must be kept private. | |
19 | + | # Used to verify that cookies haven't been tampered with. | |
20 | + | secret: "secret random string" | |
21 | + | ||
22 | + | mysql: | |
23 | + | host: localhost | |
24 | + | port: 3306 | |
25 | + | schema: freepost_freepost | |
26 | + | # charset: utf8mb4 | |
27 | + | charset: utf8 | |
28 | + | username: freepost | |
29 | + | password: freepost |