home » zplus/freepost.git
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)

Initial port.


commits diff: c54c323..be63132
42 files changed, 3026 insertions, 41 deletionsdownload


Diffstat
-rwxr-xr-x .gitignore 9
-rw-r--r-- .htaccess.cgi 29
-rw-r--r-- .htaccess.wsgi 22
-rwxr-xr-x README.md 38
-rwxr-xr-x freepost.cgi 4
-rwxr-xr-x freepost/__init__.py 622
-rw-r--r-- freepost/database.py 598
-rw-r--r-- freepost/random.py 27
-rw-r--r-- freepost/session.py 46
-rwxr-xr-x freepost/static/images/downvote.png 0
-rwxr-xr-x freepost/static/images/freepost.png 0
-rwxr-xr-x freepost/static/images/libre.exchange.png 0
-rwxr-xr-x freepost/static/images/peers.png 0
-rwxr-xr-x freepost/static/images/pulse.gif 0
-rwxr-xr-x freepost/static/images/rss.png 0
-rwxr-xr-x freepost/static/images/source.png 0
-rwxr-xr-x freepost/static/images/tuxfamily.png 0
-rwxr-xr-x freepost/static/images/upvote.png 0
-rwxr-xr-x freepost/static/javascript/freepost.js 147
-rwxr-xr-x freepost/static/stylus/freepost.styl 313
-rwxr-xr-x freepost/static/stylus/reset.styl 195
-rwxr-xr-x freepost/templates/about.html 86
-rwxr-xr-x freepost/templates/edit_comment.html 35
-rwxr-xr-x freepost/templates/edit_post.html 37
-rwxr-xr-x freepost/templates/homepage.html 98
-rw-r--r-- freepost/templates/layout.html 91
-rwxr-xr-x freepost/templates/login.html 53
-rwxr-xr-x freepost/templates/post.html 104
-rwxr-xr-x freepost/templates/register.html 43
-rwxr-xr-x freepost/templates/reply.html 33
-rw-r--r-- freepost/templates/rss.xml 27
-rwxr-xr-x freepost/templates/search.html 76
-rwxr-xr-x freepost/templates/submit.html 39
-rwxr-xr-x freepost/templates/user_comments.html 28
-rwxr-xr-x freepost/templates/user_posts.html 38
-rwxr-xr-x freepost/templates/user_private_homepage.html 70
-rwxr-xr-x freepost/templates/user_public_homepage.html 38
-rwxr-xr-x freepost/templates/user_replies.html 29
-rwxr-xr-x freepost/templates/vote.html 46
-rw-r--r-- requirements.txt 13
?--------- settings.ini 4
-rw-r--r-- settings.yaml 29

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+6/-3 M   .gitignore
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

+29/-0 A   .htaccess.cgi
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]

+22/-0 A   .htaccess.wsgi
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 +

+16/-22 M   README.md
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

+4/-0 A   freepost.cgi
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')

+614/-8 M   freepost/__init__.py
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"

+598/-0 A   freepost/database.py
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 +

+27/-0 A   freepost/random.py
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)

+46/-0 A   freepost/session.py
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)

+0/-0 A   freepost/static/images/downvote.png
index 0000000..3cc8c56
old size: 0B - new size: 1K
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/freepost.png
index 0000000..e46e311
old size: 0B - new size: 7K
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/libre.exchange.png
index 0000000..f09e9e3
old size: 0B - new size: 259B
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/peers.png
index 0000000..a67f4bd
old size: 0B - new size: 334B
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/pulse.gif
index 0000000..46967c0
old size: 0B - new size: 14K
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/rss.png
index 0000000..6ad1448
old size: 0B - new size: 3K
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/source.png
index 0000000..5936ed5
old size: 0B - new size: 516B
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/tuxfamily.png
index 0000000..a7fbaa9
old size: 0B - new size: 6K
new file mode: -rwxr-xr-x
Binary file

+0/-0 A   freepost/static/images/upvote.png
index 0000000..e7e68a4
old size: 0B - new size: 1K
new file mode: -rwxr-xr-x
Binary file

+147/-0 A   freepost/static/javascript/freepost.js
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 + });

+313/-0 A   freepost/static/stylus/freepost.styl
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

+195/-0 A   freepost/static/stylus/reset.styl
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

+86/-0 A   freepost/templates/about.html
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 %}

+35/-0 A   freepost/templates/edit_comment.html
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 %}

+37/-0 A   freepost/templates/edit_post.html
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 %}

+98/-0 A   freepost/templates/homepage.html
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 %}

+91/-0 A   freepost/templates/layout.html
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="🐵&nbsp;" 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>

+53/-0 A   freepost/templates/login.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 +

+104/-0 A   freepost/templates/post.html
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 %}

+43/-0 A   freepost/templates/register.html
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 +

+33/-0 A   freepost/templates/reply.html
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 %}

+27/-0 A   freepost/templates/rss.xml
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>

+76/-0 A   freepost/templates/search.html
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 %}

+39/-0 A   freepost/templates/submit.html
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 %}

+28/-0 A   freepost/templates/user_comments.html
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 %}

+38/-0 A   freepost/templates/user_posts.html
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 %}

+70/-0 A   freepost/templates/user_private_homepage.html
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 + Email
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 %}

+38/-0 A   freepost/templates/user_public_homepage.html
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 %}

+29/-0 A   freepost/templates/user_replies.html
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 %}

+46/-0 A   freepost/templates/vote.html
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 %}

+9/-4 M   requirements.txt
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

+0/-4 D   settings.ini
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

+29/-0 A   settings.yaml
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