home » zplus/freepost.git
Author zPlus <zplus@peers.community> 2023-06-19 11:52:30
Committer zPlus <zplus@peers.community> 2023-06-19 11:52:30
Commit ebe84c3 (patch)
Tree 8a883ec
Parent(s)

Add basic support for communities.


commits diff: fa6cb81..ebe84c3
19 files changed, 714 insertions, 914 deletionsdownload


Diffstat
-rw-r--r-- README 13
-rwxr-xr-x freepost.cgi 12
-rw-r--r-- freepost/__init__.py 318
-rw-r--r-- freepost/database.py 265
?--------- freepost/static/images/libre.exchange.png 0
-rw-r--r-- freepost/static/javascript/freepost.js 47
?--------- freepost/static/stylus/freepost.styl 375
?--------- freepost/static/stylus/reset.styl 261
-rw-r--r-- freepost/templates/banner.html 3
-rw-r--r-- freepost/templates/communities.html 26
-rw-r--r-- freepost/templates/community.html 62
-rw-r--r-- freepost/templates/community_administration.html 40
-rw-r--r-- freepost/templates/homepage.html 6
-rw-r--r-- freepost/templates/layout.html 80
-rw-r--r-- freepost/templates/post.html 45
-rw-r--r-- freepost/templates/posts.html 57
-rw-r--r-- freepost/templates/search.html 6
-rw-r--r-- freepost/templates/submit.html 10
-rw-r--r-- settings.yaml 2

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+3/-10 R   README.md -> README
index 827f059..601ab52
old size: 2K - new size: 1K
@@ -18,24 +18,17 @@ users can read and comment.
18 18 source venv/bin/activate
19 19 python3 -m bottle --debug --reload --bind 127.0.0.1:8000 freepost
20 20
21 - ## Build stylesheets
22 -
23 - Build CSS files
24 -
25 - stylus --watch --compress --disable-cache --out freepost/static/css/ freepost/static/stylus/freepost.styl
26 -
27 21 # Deployment
28 22
29 - - Build CSS stylesheets (see `Development` above)
30 23 - Copy all files to your `public_html` folder
31 - - Make sure `settings.yaml` has restricted permissions, for instance `0600`
32 - - If the SQLite database is located in the same HTML folder, make sure this too has
33 - restricted access
34 24 - Rename `.htaccess.wsgi` or `.htaccess.cgi` to `.htaccess` (if you use CGI or WSGI)
35 25 - Change settings in `settings.yaml` if needed
36 26 - Create Python virtual environment
37 27 For tuxfamily only: run `newgrp freepost` before creating the virtenv, for quota reasons
38 28 - Create a new empty SQLite database: `cat database.schema.sql | sqlite3 database.sqlite`
29 + - Make sure `settings.yaml` has restricted permissions, for instance `0600`.
30 + If the SQLite database is located in the same HTML folder, make sure this too has
31 + restricted access
39 32
40 33 Everything should be setup and working. Make sure your CGI or WSGI server is
41 34 configured correctly.

+11/-1 M   freepost.cgi
index 6047e4f..c6d4c2e
old size: 76B - new size: 669B
@@ -1,4 +1,14 @@
1 1 #!./venv/bin/python3
2 2
3 + import os
3 4 from freepost import bottle
4 - bottle.run (server='cgi')
5 +
6 + # freepost uses Bottle's function get_url() extensively (see https://bottlepy.org/docs/dev/_modules/bottle.html#Bottle.get_url
7 + # for a description). This function uses the env variable SCRIPT_NAME internally,
8 + # which is set by Apache to "/freepost.cgi" when redirecting URLs from .htaccess.
9 + # The result is that all the URLs created by get_url() will start with "/freepost.cgi",
10 + # for example "/freepost.cgi/post/<post_id>" instead of "/post/<post_id>".
11 + # So, here it's overwritten to an empty string in order to remove the script name from the URLs.
12 + os.environ['SCRIPT_NAME'] = ''
13 +
14 + bottle.run(server='cgi')

+240/-78 M   freepost/__init__.py
index a0bd7a5..29c4da0
old size: 27K - new size: 31K
@@ -33,18 +33,20 @@ template = functools.partial (
33 33 template_settings = {
34 34 'filters': {
35 35 'ago': lambda date: timeago.format(date),
36 - 'datetime': lambda date: date,# date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'),
36 + 'datetime': lambda date: date, #date.strftime('%b %-d, %Y - %H:%M%p%z%Z'),
37 37 # TODO this should be renamed. It's only a way to pretty print dates
38 38 'title': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
39 39 # Convert markdown to plain text
40 40 'md2txt': lambda text: bleach.clean (markdown.markdown(text),
41 41 tags=[], attributes={}, strip=True),
42 42 # Convert markdown to html
43 - 'md2html': lambda text: bleach.clean (bleach.linkify (markdown.markdown (
44 - text,
45 - # https://python-markdown.github.io/extensions/
46 - extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ],
47 - output_format='html5'))),
43 + 'md2html': lambda text: bleach.clean(
44 + markdown.markdown(
45 + text,
46 + # https://python-markdown.github.io/extensions/
47 + extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ],
48 + output_format='html5'),
49 + tags = list(bleach.sanitizer.ALLOWED_TAGS) + [ 'br', 'img', 'p', 'pre', 'h1', 'h2', 'h3', 'hr' ]),
48 50 # Get the domain part of a URL
49 51 'netloc': lambda url: urlparse (url).netloc
50 52 },
@@ -63,11 +65,6 @@ template = functools.partial (
63 65 'autoescape': True
64 66 })
65 67
66 - # "bleach" library is used to sanitize the HTML output of jinja2's "md2html"
67 - # filter. The library has only a very restrictive list of white-listed
68 - # tags, so we add some more here.
69 - # The list() casting is required because it's of type "frozenlist"
70 - bleach.sanitizer.ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [ 'br', 'img', 'p', 'pre', 'h1', 'h2', 'h3', 'hr' ]
71 68 bleach.sanitizer.ALLOWED_ATTRIBUTES.update ({
72 69 'img': [ 'src' ]
73 70 })
@@ -76,31 +73,31 @@ from freepost import database, mail, session
76 73
77 74 # Decorator.
78 75 # Make sure user is logged in
79 - def requires_login (controller):
80 - def wrapper ():
81 - session_token = request.get_cookie (
76 + def requires_login(controller):
77 + def wrapper(*args, **kwargs):
78 + session_token = request.get_cookie(
82 79 key = settings['session']['name'],
83 80 secret = settings['cookies']['secret'])
84 81
85 - if database.is_valid_session (session_token):
86 - return controller ()
82 + if database.is_valid_session(session_token):
83 + return controller(*args, **kwargs)
87 84 else:
88 - redirect (application.get_url ('login'))
85 + redirect(application.get_url('login'))
89 86
90 87 return wrapper
91 88
92 89 # Decorator.
93 90 # Make sure user is logged out
94 - def requires_logout (controller):
95 - def wrapper ():
96 - session_token = request.get_cookie (
91 + def requires_logout(controller):
92 + def wrapper(*args, **kwargs):
93 + session_token = request.get_cookie(
97 94 key = settings['session']['name'],
98 95 secret = settings['cookies']['secret'])
99 96
100 - if database.is_valid_session (session_token):
101 - redirect (application.get_url ('user_settings'))
97 + if database.is_valid_session(session_token):
98 + redirect(application.get_url('user_settings'))
102 99 else:
103 - return controller ()
100 + return controller(*args, **kwargs)
104 101
105 102 return wrapper
106 103
@@ -470,15 +467,165 @@ def user_public_homepage (username):
470 467
471 468 return template ('user_public_homepage.html', account=account)
472 469
473 - @get ('/post/<hash_id>', name='post')
474 - def post_thread (hash_id):
470 + @get('/c', name='communities')
471 + def communities():
472 + """
473 + List communities.
474 + """
475 +
476 + communities_list = database.get_communities_list()
477 +
478 + return template('communities.html', communities=communities_list)
479 +
480 + @post('/c')
481 + @requires_login
482 + def community_create():
483 + name = request.forms.getunicode('community_name').strip().replace(' ', '').lower()
484 +
485 + if len(name) < 3 or len(name) > 100:
486 + redirect(application.get_url('communities'))
487 +
488 + user = session.user()
489 +
490 + community = database.get_community(name)
491 + if community:
492 + redirect(application.get_url('community', cmty=name))
493 +
494 + database.create_community(name)
495 +
496 + community = database.get_community(name)
497 +
498 + # The community wasn't created for some reasons?
499 + if not community:
500 + redirect(application.get_url('communities'))
501 + else:
502 + database.add_community_member(community['id'], user['id'], True)
503 + redirect(application.get_url('community', cmty=name))
504 +
505 + @get('/c/<cmty>', name='community')
506 + def community(cmty):
507 + """
508 + Show a community.
509 + """
510 +
511 + # Sort order
512 + sort = request.query.sort or 'hot'
513 +
514 + user = session.user()
515 + cmty = database.get_community(cmty)
516 +
517 + if not cmty:
518 + abort(404, "This community doesn't exist.")
519 +
520 + mods = database.get_community_mods(cmty['id'])
521 +
522 + try:
523 + is_moderator = database.is_community_moderator(cmty['id'], user['id'])
524 + is_member = database.is_community_member(cmty['id'], user['id'])
525 + except:
526 + is_moderator = False
527 + is_member = False
528 +
529 + # Page number
530 + page = int(request.query.page or 0)
531 +
532 + if page < 0:
533 + redirect(application.get_url('homepage'))
534 +
535 + if sort in [ 'hot', 'new' ]:
536 + posts = database.get_posts(
537 + page, user['id'] if user else None, sort,
538 + topic=None, community_id=cmty['id'])
539 + else:
540 + posts = []
541 +
542 + return template(
543 + 'community.html',
544 + community=cmty['name'],
545 + community_data=cmty,
546 + is_member=is_member,
547 + is_moderator=is_moderator,
548 + moderators=mods,
549 + posts=posts,
550 + sort=sort)
551 +
552 + @get('/c/<community>/administration', name='community_administration')
553 + @requires_login
554 + def community_administration(community):
555 + """
556 + Administration page of community.
557 + """
558 +
559 + user = session.user()
560 + community = database.get_community(community)
561 +
562 + try:
563 + is_moderator = database.is_community_moderator(community['id'], user['id'])
564 + assert is_moderator
565 + except:
566 + redirect(application.get_url('homepage'))
567 + abort()
568 +
569 + mods = database.get_community_mods(community['id'])
570 +
571 + return template(
572 + 'community_administration.html',
573 + community=community,
574 + moderators=mods)
575 +
576 + @post('/c/<community>/administration')
577 + @requires_login
578 + def community_administration_update(community):
579 + """
580 + Update community settings.
581 + """
582 +
583 + user = session.user()
584 + community = database.get_community(community)
585 +
586 + try:
587 + is_moderator = database.is_community_moderator(community['id'], user['id'])
588 + assert is_moderator
589 + except:
590 + redirect(application.get_url('homepage'))
591 + abort()
592 +
593 + # Truncate at 1K
594 + description = request.forms.getunicode('description').strip()[:1024]
595 + allow_new_posts = request.forms.getunicode('allow_new_posts') == 'yes'
596 +
597 + database.update_community_settings(
598 + community['id'], description, allow_new_posts)
599 +
600 + redirect(application.get_url('community', cmty=community['name']))
601 +
602 + @post('/c/<community>')
603 + @requires_login
604 + def community_join(community):
605 + user = session.user()
606 + cmty = database.get_community(community)
607 +
608 + if not cmty:
609 + abort(500, "Community doesn't exist.")
610 + return
611 +
612 + if 'join' in request.forms:
613 + database.add_community_member(cmty['id'], user['id'])
614 +
615 + if 'leave' in request.forms:
616 + database.remove_community_member(cmty['id'], user['id'])
617 +
618 + redirect(application.get_url('community', cmty=community))
619 +
620 + @get('/post/<hash_id>', name='post')
621 + def post_thread(hash_id):
475 622 """
476 623 Display a single post with all its comments.
477 624 """
478 625
479 - user = session.user ()
480 - post = database.get_post (hash_id, user['id'] if user else None)
481 - comments = database.get_post_comments (post['id'], user['id'] if user else None)
626 + user = session.user()
627 + post = database.get_post(hash_id, user['id'] if user else None)
628 + comments = database.get_post_comments(post['id'], user['id'] if user else None)
482 629 topics = database.get_post_topics (post['id'])
483 630
484 631 # Group comments by parent
@@ -509,7 +656,7 @@ def post_thread (hash_id):
509 656
510 657 return temp_comments
511 658
512 - comments = children ()
659 + comments = children()
513 660
514 661 # Show posts/comments Markdown instead of rendered text
515 662 show_source = 'source' in request.query
@@ -522,6 +669,7 @@ def post_thread (hash_id):
522 669
523 670 return template (
524 671 'post.html',
672 + community=post['community_name'],
525 673 post=post,
526 674 comments=comments,
527 675 topics=topics,
@@ -531,15 +679,15 @@ def post_thread (hash_id):
531 679 'comment': {}
532 680 })
533 681
682 + @post('/post/<hash_id>')
534 683 @requires_login
535 - @post ('/post/<hash_id>')
536 - def new_comment (hash_id):
684 + def new_comment(hash_id):
537 685 # The comment text
538 - comment = request.forms.getunicode ('new_comment').strip ()
686 + comment = request.forms.getunicode('new_comment').strip()
539 687
540 688 # Empty comment?
541 - if len (comment) == 0:
542 - redirect (application.get_url ('post', hash_id=hash_id))
689 + if len(comment) == 0:
690 + redirect(application.get_url('post', hash_id=hash_id))
543 691
544 692 # Retrieve the post
545 693 post = database.get_post (hash_id)
@@ -560,45 +708,45 @@ def new_comment (hash_id):
560 708
561 709 redirect (application.get_url ('post', hash_id=hash_id))
562 710
563 - @requires_login
564 711 @get ('/edit/post/<hash_id>', name='edit_post')
565 - def edit_post (hash_id):
566 - user = session.user ()
567 - post = database.get_post (hash_id, user['id'])
712 + @requires_login
713 + def edit_post(hash_id):
714 + user = session.user()
715 + post = database.get_post(hash_id, user['id'])
568 716
569 717 # Make sure the session user is the actual poster/commenter
570 718 if post['userId'] != user['id']:
571 - redirect (application.get_url ('post', hash_id=hash_id))
719 + redirect(application.get_url('post', hash_id=hash_id))
572 720
573 - return template ('edit_post.html', post=post)
721 + return template('edit_post.html', community=post['community_name'], post=post)
574 722
575 - @requires_login
576 723 @post ('/edit/post/<hash_id>')
577 - def edit_post_check (hash_id):
578 - user = session.user ()
579 - post = database.get_post (hash_id, user['id'])
724 + @requires_login
725 + def edit_post_check(hash_id):
726 + user = session.user()
727 + post = database.get_post(hash_id, user['id'])
580 728
581 729 # Make sure the session user is the actual poster/commenter
582 730 if post['userId'] != user['id']:
583 - redirect (application.get_url ('homepage'))
731 + redirect(application.get_url('homepage'))
584 732
585 733 # MUST have a title. If empty, use original title
586 - title = request.forms.getunicode ('title').strip ()
734 + title = request.forms.getunicode('title').strip()
587 735 if len (title) == 0: title = post['title']
588 - link = request.forms.getunicode ('link').strip () if 'link' in request.forms else ''
589 - text = request.forms.getunicode ('text').strip () if 'text' in request.forms else ''
736 + link = request.forms.getunicode('link').strip() if 'link' in request.forms else ''
737 + text = request.forms.getunicode('text').strip() if 'text' in request.forms else ''
590 738
591 739 # If there is a URL, make sure it has a "scheme"
592 - if len (link) > 0 and urlparse (link).scheme == '':
740 + if len(link) > 0 and urlparse(link).scheme == '':
593 741 link = 'http://' + link
594 742
595 743 # Update post
596 - database.update_post (title, link, text, hash_id, user['id'])
744 + database.update_post(title, link, text, hash_id, user['id'])
597 745
598 - redirect (application.get_url ('post', hash_id=hash_id))
746 + redirect(application.get_url ('post', hash_id=hash_id))
599 747
600 - @requires_login
601 748 @get ('/edit/comment/<hash_id>', name='edit_comment')
749 + @requires_login
602 750 def edit_comment (hash_id):
603 751 user = session.user ()
604 752 comment = database.get_comment (hash_id, user['id'])
@@ -609,8 +757,8 @@ def edit_comment (hash_id):
609 757
610 758 return template ('edit_comment.html', comment=comment)
611 759
612 - @requires_login
613 760 @post ('/edit/comment/<hash_id>')
761 + @requires_login
614 762 def edit_comment_check (hash_id):
615 763 user = session.user ()
616 764 comment = database.get_comment (hash_id, user['id'])
@@ -627,37 +775,37 @@ def edit_comment_check (hash_id):
627 775
628 776 redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + hash_id)
629 777
630 - @get ('/submit')
778 + @get('/c/<community>/submit', name='submit')
631 779 @requires_login
632 - def submit ():
780 + def submit(community):
633 781 """
634 782 Submit a new post.
635 783 """
636 784
637 - return template ('submit.html')
785 + return template('submit.html', community=community)
638 786
639 - @post ('/submit', name='submit')
787 + @post('/c/<community>/submit')
640 788 @requires_login
641 - def submit_check ():
789 + def submit_check(community):
642 790 """
643 791 Check submission of new post.
644 792 """
645 793
646 794 # Somebody sent a <form> without a title???
647 - if not request.forms.getunicode ('title'):
648 - abort ()
795 + if 'title' not in request.forms:
796 + abort()
649 797
650 - user = session.user ()
798 + user = session.user()
651 799
652 800 # Retrieve title
653 - title = request.forms.getunicode ('title').strip ()
801 + title = request.forms.getunicode('title').strip()
654 802
655 803 # Bad title?
656 - if len (title) == 0:
657 - return template ('submit.html', flash='Title is missing.')
804 + if len(title) == 0:
805 + return template('submit.html', community=community, flash='Title is missing.')
658 806
659 807 # Retrieve link
660 - link = request.forms.getunicode ('link').strip ()
808 + link = request.forms.getunicode('link').strip()
661 809
662 810 if len (link) > 0:
663 811 # If there is a URL, make sure it has a "scheme"
@@ -669,36 +817,50 @@ def submit_check ():
669 817 if previous_posts:
670 818 posts_list = ''.join([
671 819 '<li><a href="{link}">{title}</a></li>'.format(
672 - link = application.get_url ('post', hash_id=post['hashId']),
820 + link = application.get_url('post', hash_id=post['hashId']),
673 821 title = post['title'])
674 822 for post in previous_posts ])
675 823
676 - return template ('submit.html',
677 - flash='This link was already submitted:<ul>{posts}</ul>'.format(posts=posts_list))
824 + return template('submit.html',
825 + community=community,
826 + flash='This link was already submitted:<ul>{posts}</ul>'.format(posts=posts_list))
678 827
679 828 # Retrieve topics
680 - topics = request.forms.getunicode ('topics')
829 + # topics = request.forms.getunicode ('topics')
681 830
682 831 # Retrieve text
683 - text = request.forms.getunicode ('text')
832 + text = request.forms.getunicode('text')
833 +
834 + # Retrieve the community
835 + community = database.get_community(community)
836 +
837 + if not community:
838 + abort(500, "Community doesn't exist.")
839 + return
840 +
841 + # Does this community allow new posts?
842 + if not community['allow_new_posts']:
843 + return template('submit.html',
844 + community=community['name'],
845 + flash='This community does not allow new posts.')
684 846
685 847 # Add the new post
686 - post_hash_id = database.new_post (title, link, text, user['id'])
848 + post_hash_id = database.new_post(title, link, text, user['id'], community['id'])
687 849
688 850 # Retrieve the new post just created
689 - post = database.get_post (post_hash_id)
851 + post = database.get_post(post_hash_id)
690 852
691 853 # Add topics for this post
692 - database.replace_post_topics (post['id'], topics)
854 + # database.replace_post_topics (post['id'], topics)
693 855
694 856 # Automatically add an upvote for the original poster
695 - database.vote_post (post['id'], user['id'], +1)
857 + database.vote_post(post['id'], user['id'], +1)
696 858
697 859 # Posted. Now go the new post's page
698 - redirect (application.get_url ('post', hash_id=post_hash_id))
860 + redirect(application.get_url('post', hash_id=post_hash_id))
699 861
700 - @requires_login
701 862 @get ('/reply/<hash_id>', name='reply')
863 + @requires_login
702 864 def reply (hash_id):
703 865 user = session.user ()
704 866
@@ -711,8 +873,8 @@ def reply (hash_id):
711 873
712 874 return template ('reply.html', comment=comment)
713 875
714 - @requires_login
715 876 @post ('/reply/<hash_id>')
877 + @requires_login
716 878 def reply_check (hash_id):
717 879 user = session.user ()
718 880
@@ -739,8 +901,8 @@ def reply_check (hash_id):
739 901
740 902 redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + reply_hash_id)
741 903
742 - @requires_login
743 904 @post ('/vote', name='vote')
905 + @requires_login
744 906 def vote ():
745 907 """
746 908 Handle upvotes and downvotes.

+204/-61 M   freepost/database.py
index 83110a7..256dbdb
old size: 22K - new size: 26K
@@ -53,7 +53,7 @@ def delete_session (user_id):
53 53 )
54 54
55 55 # Check user login credentials
56 - #
56 + #
57 57 # @return None if bad credentials, otherwise return the user
58 58 def check_user_credentials (username, password):
59 59 with db:
@@ -70,19 +70,19 @@ def check_user_credentials (username, password):
70 70 'password': password
71 71 }
72 72 )
73 -
73 +
74 74 return cursor.fetchone ()
75 75
76 76 # Check if username exists
77 77 def username_exists (username, case_sensitive = True):
78 78 if not username:
79 79 return None
80 -
80 +
81 81 if case_sensitive:
82 82 where = 'WHERE username = :username'
83 83 else:
84 84 where = 'WHERE LOWER(username) = LOWER(:username)'
85 -
85 +
86 86 with db:
87 87 cursor = db.execute(
88 88 """
@@ -94,7 +94,7 @@ def username_exists (username, case_sensitive = True):
94 94 'username': username
95 95 }
96 96 )
97 -
97 +
98 98 return cursor.fetchone() is not None
99 99
100 100 # Check if post with same link exists. This is used to check for duplicates.
@@ -107,7 +107,7 @@ def link_exists (link):
107 107 cursor = db.execute(
108 108 """
109 109 SELECT *
110 - FROM post
110 + FROM post
111 111 WHERE LOWER(link) = LOWER(:link)
112 112 ORDER BY created DESC
113 113 """,
@@ -115,17 +115,17 @@ def link_exists (link):
115 115 'link': link
116 116 }
117 117 )
118 -
118 +
119 119 return cursor.fetchall()
120 120
121 121 # Create new user account
122 122 def new_user (username, password):
123 123 # Create a hash_id for the new post
124 124 hash_id = random.alphanumeric_string (10)
125 -
125 +
126 126 # Create a salt for user's password
127 127 salt = random.ascii_string (16)
128 -
128 +
129 129 # Add user to database
130 130 with db:
131 131 db.execute (
@@ -158,14 +158,14 @@ def count_unread_messages (user_id):
158 158 'user': user_id
159 159 }
160 160 )
161 -
161 +
162 162 return cursor.fetchone ()['new_messages']
163 163
164 164 # Retrieve a user
165 165 def get_user_by_username (username):
166 166 if not username:
167 167 return None
168 -
168 +
169 169 with db:
170 170 cursor = db.execute(
171 171 """
@@ -177,7 +177,7 @@ def get_user_by_username (username):
177 177 'username': username
178 178 }
179 179 )
180 -
180 +
181 181 return cursor.fetchone()
182 182
183 183 # Retrieve a user from a session cookie
@@ -193,46 +193,54 @@ def get_user_by_session_token(session_token):
193 193 'session': session_token
194 194 }
195 195 )
196 -
196 +
197 197 return cursor.fetchone()
198 198
199 199 # Get posts by date (for homepage)
200 - def get_posts (page = 0, session_user_id = None, sort = 'hot', topic = None):
200 + def get_posts (page=0, session_user_id=None, sort='hot', topic=None, community_id=None):
201 201 if sort == 'new':
202 202 sort = 'ORDER BY P.created DESC'
203 203 else:
204 204 sort = 'ORDER BY P.dateCreated DESC, P.vote DESC, P.commentsCount DESC'
205 -
205 +
206 206 if topic:
207 207 topic_name = 'WHERE T.name = :topic'
208 208 else:
209 209 topic_name = ''
210 -
210 +
211 + if community_id:
212 + community_filter = 'AND C.id = :community_id'
213 + else:
214 + community_filter = ''
215 +
211 216 with db:
212 217 cursor = db.execute (
213 - """
218 + f"""
214 219 SELECT P.*,
215 220 U.username,
221 + C.name AS community_name,
216 222 V.vote AS user_vote,
217 223 GROUP_CONCAT(T.name, " ") AS topics
218 224 FROM post AS P
219 225 JOIN user AS U ON P.userId = U.id
226 + JOIN community AS C ON P.community_id = C.id {community_filter}
220 227 LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = :user
221 228 LEFT JOIN topic as T ON T.post_id = P.id
222 - {topic}
229 + {topic_name}
223 230 GROUP BY P.id
224 - {order}
231 + {sort}
225 232 LIMIT :limit
226 233 OFFSET :offset
227 - """.format (topic=topic_name, order=sort),
234 + """,
228 235 {
229 236 'user': session_user_id,
230 237 'limit': settings['defaults']['items_per_page'],
231 238 'offset': page * settings['defaults']['items_per_page'],
232 - 'topic': topic
239 + 'topic': topic,
240 + 'community_id': community_id
233 241 }
234 242 )
235 -
243 +
236 244 return cursor.fetchall ()
237 245
238 246 # Retrieve user's own posts
@@ -250,7 +258,7 @@ def get_user_posts (user_id):
250 258 'user': user_id
251 259 }
252 260 )
253 -
261 +
254 262 return cursor.fetchall()
255 263
256 264 # Retrieve user's own comments
@@ -271,7 +279,7 @@ def get_user_comments (user_id):
271 279 'user': user_id
272 280 }
273 281 )
274 -
282 +
275 283 return cursor.fetchall()
276 284
277 285 # Retrieve user's own replies to other people
@@ -294,7 +302,7 @@ def get_user_replies (user_id):
294 302 'user': user_id
295 303 }
296 304 )
297 -
305 +
298 306 return cursor.fetchall()
299 307
300 308 # Update user information
@@ -316,7 +324,7 @@ def update_user (user_id, about, email, email_notifications, preferred_feed):
316 324 'preferred_feed': preferred_feed
317 325 }
318 326 )
319 -
327 +
320 328 # Update email address. Convert all addresses to LOWER() case. This
321 329 # prevents two users from using the same address with different case.
322 330 # IGNORE update if the email address is already specified. This is
@@ -348,46 +356,47 @@ def set_replies_as_read (user_id):
348 356 )
349 357
350 358 # Submit a new post/link
351 - def new_post (title, link, text, user_id):
359 + def new_post (title, link, text, user_id, community_id):
352 360 # Create a hash_id for the new post
353 - hash_id = random.alphanumeric_string (10)
354 -
361 + hash_id = random.alphanumeric_string(10)
362 +
355 363 with db:
356 364 db.execute(
357 365 """
358 366 INSERT INTO post (hashId, created, dateCreated, title,
359 - link, text, vote, commentsCount, userId)
367 + link, text, vote, commentsCount, userId, community_id)
360 368 VALUES (:hash_id, DATETIME(), DATE(), :title, :link,
361 - :text, 0, 0, :user)
369 + :text, 0, 0, :user, :community)
362 370 """,
363 371 {
364 372 'hash_id': hash_id,
365 373 'title': title,
366 374 'link': link,
367 375 'text': text,
368 - 'user': user_id
376 + 'user': user_id,
377 + 'community': community_id
369 378 }
370 379 )
371 -
380 +
372 381 return hash_id
373 382
374 383 # Set topics post. Deletes existing ones.
375 384 def replace_post_topics (post_id, topics = ''):
376 385 if not topics:
377 386 return
378 -
387 +
379 388 # Normalize topics
380 389 # 1. Split topics by space
381 390 # 2. Remove empty strings
382 391 # 3. Lower case topic name
383 392 topics = [ topic.lower () for topic in topics.split (' ') if topic ]
384 -
393 +
385 394 if len (topics) == 0:
386 395 return
387 -
396 +
388 397 # Remove extra topics if the list is too long
389 398 topics = topics[:settings['defaults']['topics_per_post']]
390 -
399 +
391 400 with db:
392 401 # First we delete the existing topics
393 402 db.execute (
@@ -400,7 +409,7 @@ def replace_post_topics (post_id, topics = ''):
400 409 'post': post_id
401 410 }
402 411 )
403 -
412 +
404 413 # Now insert the new topics.
405 414 # IGNORE duplicates that trigger UNIQUE constraint.
406 415 db.executemany (
@@ -412,14 +421,16 @@ def replace_post_topics (post_id, topics = ''):
412 421 )
413 422
414 423 # Retrieve a post
415 - def get_post (hash, session_user_id = None):
424 + def get_post(hash, session_user_id = None):
416 425 with db:
417 426 cursor = db.execute (
418 427 """
419 428 SELECT P.*,
420 429 U.username,
421 - V.vote AS user_vote
430 + V.vote AS user_vote,
431 + C.name AS community_name
422 432 FROM post AS P
433 + JOIN community AS C ON P.community_id = C.id
423 434 JOIN user AS U ON P.userId = U.id
424 435 LEFT JOIN vote_post as V ON V.postId = P.id AND V.userId = :user
425 436 WHERE P.hashId = :post
@@ -429,7 +440,7 @@ def get_post (hash, session_user_id = None):
429 440 'post': hash
430 441 }
431 442 )
432 -
443 +
433 444 return cursor.fetchone ()
434 445
435 446 # Update a post
@@ -473,7 +484,7 @@ def get_post_comments (post_id, session_user_id = None):
473 484 'post': post_id
474 485 }
475 486 )
476 -
487 +
477 488 return cursor.fetchall ()
478 489
479 490 # Retrieve all topics for a specific post
@@ -490,17 +501,17 @@ def get_post_topics (post_id):
490 501 'post': post_id
491 502 }
492 503 )
493 -
504 +
494 505 return cursor.fetchall ()
495 506
496 507 # Submit a new comment to a post
497 508 def new_comment (comment_text, post_hash_id, user_id, parent_user_id = None, parent_comment_id = None):
498 509 # Create a hash_id for the new comment
499 510 hash_id = random.alphanumeric_string (10)
500 -
511 +
501 512 # Retrieve post
502 513 post = get_post (post_hash_id)
503 -
514 +
504 515 with db:
505 516 db.execute (
506 517 """
@@ -518,7 +529,7 @@ def new_comment (comment_text, post_hash_id, user_id, parent_user_id = None, par
518 529 'user': user_id
519 530 }
520 531 )
521 -
532 +
522 533 # Increase comments count for post
523 534 db.execute (
524 535 """
@@ -530,7 +541,7 @@ def new_comment (comment_text, post_hash_id, user_id, parent_user_id = None, par
530 541 'post': post['id']
531 542 }
532 543 )
533 -
544 +
534 545 return hash_id
535 546
536 547 # Retrieve a single comment
@@ -554,7 +565,7 @@ def get_comment (hash_id, session_user_id = None):
554 565 'comment': hash_id
555 566 }
556 567 )
557 -
568 +
558 569 return cursor.fetchone()
559 570
560 571 # Retrieve last N newest comments
@@ -575,7 +586,7 @@ def get_latest_comments ():
575 586 {
576 587 }
577 588 )
578 -
589 +
579 590 return cursor.fetchall ()
580 591
581 592 # Update a comment
@@ -608,7 +619,7 @@ def vote_post (post_id, user_id, vote):
608 619 'user': user_id
609 620 }
610 621 )
611 -
622 +
612 623 # Update user vote (+1 or -1)
613 624 db.execute(
614 625 """
@@ -622,7 +633,7 @@ def vote_post (post_id, user_id, vote):
622 633 'user': user_id
623 634 }
624 635 )
625 -
636 +
626 637 # Update post's total
627 638 db.execute (
628 639 """
@@ -650,7 +661,7 @@ def vote_comment (comment_id, user_id, vote):
650 661 'user': user_id
651 662 }
652 663 )
653 -
664 +
654 665 # Update user vote (+1 or -1)
655 666 db.execute (
656 667 """
@@ -664,7 +675,7 @@ def vote_comment (comment_id, user_id, vote):
664 675 'user': user_id
665 676 }
666 677 )
667 -
678 +
668 679 # Update comment's total
669 680 db.execute (
670 681 """
@@ -682,18 +693,18 @@ def vote_comment (comment_id, user_id, vote):
682 693 def search (query, sort='newest', page=0):
683 694 if not query:
684 695 return []
685 -
696 +
686 697 # Remove multiple white spaces and replace with '|' (for query REGEXP)
687 698 query = re.sub (' +', '|', query.strip ())
688 -
699 +
689 700 if len (query) == 0:
690 701 return []
691 -
702 +
692 703 if sort == 'newest':
693 704 sort = 'P.created DESC'
694 705 if sort == 'points':
695 706 sort = 'P.vote DESC'
696 -
707 +
697 708 with db:
698 709 cursor = db.execute (
699 710 """
@@ -713,14 +724,14 @@ def search (query, sort='newest', page=0):
713 724 'offset': page * settings['defaults']['search_results_per_page']
714 725 }
715 726 )
716 -
727 +
717 728 return cursor.fetchall ()
718 729
719 730 # Set reset token for user email
720 731 def set_password_reset_token (user_id = None, token = None):
721 732 if not user_id or not token:
722 733 return
723 -
734 +
724 735 with db:
725 736 db.execute (
726 737 """
@@ -766,14 +777,14 @@ def is_password_reset_token_valid (user_id = None):
766 777 'user': user_id
767 778 }
768 779 )
769 -
780 +
770 781 return cursor.fetchone()['valid'] == 1
771 782
772 783 # Reset user password
773 784 def reset_password (username = None, email = None, new_password = None, secret_token = None):
774 785 if not new_password:
775 786 return
776 -
787 +
777 788 with db:
778 789 db.execute (
779 790 """
@@ -794,11 +805,143 @@ def reset_password (username = None, email = None, new_password = None, secret_t
794 805 }
795 806 )
796 807
808 + def get_communities_list():
809 + with db:
810 + cursor = db.execute(
811 + f"""
812 + SELECT C.name, C.description, COUNT(M.user_id) AS members_count
813 + FROM community AS C
814 + JOIN community_member AS M ON M.community_id = C.id
815 + GROUP BY C.id
816 + """
817 + )
797 818
819 + return cursor.fetchall()
798 820
821 + def get_community(name):
822 + with db:
823 + cursor = db.execute(
824 + """
825 + SELECT C.* , COUNT(M.user_id) AS members_count
826 + FROM community AS C
827 + LEFT JOIN community_member AS M ON M.community_id = C.id
828 + WHERE name = :name
829 + """,
830 + {
831 + 'name': name
832 + }
833 + )
834 +
835 + community = cursor.fetchone()
836 + return community if community['name'] else None
837 +
838 + def get_community_mods(community_id):
839 + with db:
840 + cursor = db.execute(
841 + """
842 + SELECT U.username
843 + FROM community AS C
844 + JOIN community_member AS M ON M.community_id = C.id AND M.moderator = 1
845 + JOIN user AS U ON U.id = M.user_id
846 + WHERE C.id = :community_id
847 + """,
848 + {
849 + 'community_id': community_id
850 + }
851 + )
852 +
853 + return cursor.fetchall()
854 +
855 + def create_community(name):
856 + with db:
857 + db.execute(
858 + """
859 + INSERT OR IGNORE INTO community (name, created, description)
860 + VALUES (:name, DATETIME(), "")
861 + """,
862 + {
863 + 'name': name
864 + }
865 + )
866 +
867 + def add_community_member(community_id, user_id, is_moderator=False):
868 + with db:
869 + db.execute(
870 + """
871 + INSERT OR REPLACE INTO community_member (community_id, user_id, moderator)
872 + VALUES (:community_id, :user_id, :moderator)
873 + """,
874 + {
875 + 'community_id': community_id,
876 + 'user_id': user_id,
877 + 'moderator': 1 if is_moderator else 0
878 + }
879 + )
799 880
881 + def remove_community_member(community_id, user_id, is_moderator=False):
882 + with db:
883 + db.execute(
884 + """
885 + DELETE FROM community_member
886 + WHERE community_id = :community_id AND user_id = :user_id
887 + """,
888 + {
889 + 'community_id': community_id,
890 + 'user_id': user_id
891 + }
892 + )
893 +
894 + def is_community_moderator(community_id, user_id):
895 + with db:
896 + cursor = db.execute(
897 + """
898 + SELECT EXISTS (
899 + SELECT 1
900 + FROM community_member AS M
901 + WHERE M.community_id = :community_id AND M.user_id = :user_id AND M.moderator = 1
902 + ) AS count
903 + """,
904 + {
905 + 'community_id': community_id,
906 + 'user_id': user_id
907 + }
908 + )
800 909
910 + return cursor.fetchone()['count'] > 0
801 911
912 + def is_community_member(community_id, user_id):
913 + with db:
914 + cursor = db.execute(
915 + """
916 + SELECT EXISTS (
917 + SELECT 1
918 + FROM community_member AS M
919 + WHERE M.community_id = :community_id AND M.user_id = :user_id
920 + ) AS count
921 + """,
922 + {
923 + 'community_id': community_id,
924 + 'user_id': user_id
925 + }
926 + )
927 +
928 + return cursor.fetchone()['count'] > 0
929 +
930 + def update_community_settings(community_id, description, allow_new_posts):
931 + with db:
932 + db.execute (
933 + """
934 + UPDATE community
935 + SET description = :description,
936 + allow_new_posts = :allow_new_posts
937 + WHERE id = :community_id
938 + """,
939 + {
940 + 'community_id': community_id,
941 + 'description': description,
942 + 'allow_new_posts': 1 if allow_new_posts else 0
943 + }
944 + )
802 945
803 946
804 947

+0/-0 D   freepost/static/images/libre.exchange.png
index f09e9e3..0000000
old size: 259B - new size: 0B
deleted file mode: -rw-r--r--
Binary file

+24/-23 M   freepost/static/javascript/freepost.js
index fcf63c6..d92bbea
old size: 5K - new size: 5K
@@ -1,29 +1,29 @@
1 1 /*
2 2 @licstart The following is the entire license notice for the JavaScript code in this page.
3 -
3 +
4 4 This is the code powering <http://freepo.st>.
5 5 Copyright © 2014-2016 zPlus
6 6 Copyright © 2016 Adonay "adfeno" Felipe Nogueira <adfeno@openmailbox.org> <https://libreplanet.org/wiki/User:Adfeno>
7 -
7 +
8 8 This program is free software: you can redistribute it and/or modify
9 9 it under the terms of the GNU Affero General Public License as published by
10 10 the Free Software Foundation, either version 3 of the License, or
11 11 (at your option) any later version.
12 -
12 +
13 13 This program is distributed in the hope that it will be useful,
14 14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 16 GNU Affero General Public License for more details.
17 -
17 +
18 18 You should have received a copy of the GNU Affero General Public License
19 19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 -
20 +
21 21 As additional permission under GNU GPL version 3 section 7, you may
22 22 distribute non-source (e.g., minimized or compacted) forms of that code
23 23 without the copy of the GNU GPL normally required by section 4, provided
24 24 you include this license notice and a URL through which recipients can
25 25 access the Corresponding Source.
26 -
26 +
27 27 @licend The above is the entire license notice for the JavaScript code in this page.
28 28 */
29 29
@@ -35,23 +35,23 @@ function vote (action, vote_dom)
35 35 var arrow_up = vote_dom.querySelector ('button[title="upvote"]');
36 36 var vote_counter = vote_dom.querySelector ('.count')
37 37 var arrow_down = vote_dom.querySelector ('button[title="downvote"]');
38 -
38 +
39 39 // Voted/Upvoted
40 40 var current_status = 0;
41 -
41 +
42 42 if (vote_dom.classList.contains('upvoted'))
43 43 current_status = 1;
44 -
44 +
45 45 if (vote_dom.classList.contains('downvoted'))
46 46 current_status = -1;
47 -
47 +
48 48 // Current vote
49 49 var current_vote = Number (vote_counter.textContent);
50 -
50 +
51 51 // Remove any existing upvoted/downvoted class
52 52 vote_dom.classList.remove ('upvoted');
53 53 vote_dom.classList.remove ('downvoted');
54 -
54 +
55 55 // Toggle upvote class for arrow
56 56 if ("up" == action)
57 57 switch (current_status)
@@ -68,7 +68,7 @@ function vote (action, vote_dom)
68 68 vote_counter.textContent = current_vote - 1;
69 69 break;
70 70 }
71 -
71 +
72 72 // Toggle downvote class for arrow
73 73 if ("down" == action)
74 74 switch (current_status)
@@ -89,18 +89,18 @@ function vote (action, vote_dom)
89 89
90 90 // Wait DOM to be ready...
91 91 document.addEventListener ('DOMContentLoaded', function() {
92 -
92 +
93 93 /**
94 94 * A "vote section" is a <span/> containing
95 95 * - up arrow
96 96 * - votes sum
97 97 * - down arrow
98 - *
98 + *
99 99 * However, if the user is not logged in, there's only a text
100 100 * with the sum of votes, eg. "2 votes" (no <tag> children).
101 101 */
102 102 var vote_sections = document.querySelectorAll ('.vote ');
103 -
103 +
104 104 // Bind vote() event to up/down vote arrows
105 105 for (var i = 0; i < vote_sections.length; i++)
106 106 // See comment above on the "vote_sections" declaration.
@@ -111,21 +111,22 @@ document.addEventListener ('DOMContentLoaded', function() {
111 111 .addEventListener ('click', function () {
112 112 vote ('up', this.closest ('.vote'))
113 113 });
114 -
114 +
115 115 vote_sections[i]
116 116 .querySelector ('button[title="downvote"]')
117 117 .addEventListener ('click', function () {
118 118 vote ('down', this.closest ('.vote'))
119 119 });
120 120 }
121 -
121 +
122 122 // Function to hide/show menu when burger-icon has been clicked
123 - var burger_menus = document.getElementsByClassName ('burger-icon')
123 + var burger_menus = document.getElementsByClassName('burger-icon');
124 +
124 125 for (var i = 0; i < burger_menus.length; i++)
125 - burger_menus[i].addEventListener ('click', function (event) {
126 + burger_menus[i].addEventListener('click', function(event) {
126 127 // Toggle menu visibility
127 - document.getElementById ('menu').classList.toggle ('visible');
128 -
129 - this.classList.toggle ('open');
128 + document.getElementById('menu').classList.toggle('visible');
129 +
130 + this.classList.toggle('open');
130 131 });
131 132 });

+0/-375 D   freepost/static/stylus/freepost.styl
index b894a08..0000000
old size: 12K - new size: 0B
deleted file mode: -rw-r--r--
@@ -1,375 +0,0 @@
1 - @require 'reset.styl'
2 -
3 - /* A class used for displaying URLs domain (under post tile) */
4 - .netloc
5 - color #828282
6 - font-size 1rem
7 - font-style italic
8 -
9 - .monkey
10 - height 1.5em
11 - margin 0 1em
12 - vertical-align middle
13 -
14 - a.topic,
15 - a.topic:hover,
16 - a.topic:visited
17 - color rgba(200,0,100,.8)
18 - font-size 1rem
19 - padding 0 .2rem
20 - text-decoration none
21 -
22 - /* Text icon near the title, to display the post content */
23 - .text_preview
24 - height .8rem
25 - margin 0 .5rem
26 - vertical-align middle
27 -
28 - /* Logo text */
29 - a.logo,
30 - a.logo:hover,
31 - a.logo:visited
32 - color #000
33 - font-weight bold
34 - text-decoration none
35 -
36 - body
37 - > .container
38 - margin auto
39 - max-width 80%
40 -
41 - /* Page header */
42 - > .header
43 - @media only screen and (max-width: 800px)
44 - display grid
45 - grid-template-columns auto
46 -
47 - > .title-large
48 - display none
49 -
50 - > .title-small
51 - display grid
52 - grid-template-columns auto max-content
53 -
54 - @media only screen and (min-width: 800px)
55 - display grid
56 - grid-template-columns max-content auto
57 -
58 - > .title-small
59 - display none
60 -
61 - padding 1rem 0
62 -
63 - /* Menu under the logo */
64 - @media only screen and (max-width: 800px)
65 - .burger-icon
66 - display inline-block
67 -
68 - .menu
69 - border-bottom 2px dashed #aaa
70 - display none
71 - padding 1rem 0
72 -
73 - /* This class is toggled when the burger icon is clicked */
74 - &.visible
75 - display block
76 -
77 - /* Every menu item */
78 - > a
79 - border 0
80 - color #000
81 - display block
82 - margin 0
83 - padding .8rem 0
84 - text-align left
85 - text-decoration none
86 -
87 - /* Highlight menu item of the current active page (Hot/New/Submit/...) */
88 - > .active_page
89 - font-weight bold
90 - text-decoration underline dotted
91 - text-transform uppercase
92 -
93 - /* Highlight username if there are unread messages */
94 - .new_messages
95 - background-color rgb(255, 175, 50)
96 - color #fff
97 - font-weight bold
98 - padding-left 1rem
99 -
100 - @media only screen and (min-width: 800px)
101 - .burger-icon
102 - display none
103 -
104 - .menu
105 - border-bottom 1px solid transparent
106 - display flex
107 - flex-direction row
108 - flex-wrap wrap
109 - justify-content flex-start
110 - align-content flex-start
111 - align-items flex-end
112 -
113 - > .flex-item
114 - flex 0 1 auto
115 - align-self auto
116 - order 0
117 -
118 - border 0
119 - border-bottom 1px solid #ccc
120 - color #000
121 - margin 0
122 - padding 0 .5rem .5rem .5rem
123 -
124 - /* Highlight menu item of the current active page (Hot/New/Submit/...) */
125 - > .active_page
126 - border-bottom 3px solid #000
127 -
128 - /* Highlight username if there are unread messages */
129 - .new_messages
130 - background-color rgb(255, 175, 50)
131 - border 0
132 - border-radius 4px
133 - color #fff
134 - font-weight bold
135 - margin 0
136 - padding .5em .5em
137 - text-decoration none
138 -
139 - > .content
140 - padding 1em 0
141 - line-height 1.5em
142 -
143 - .vote
144 - margin 0 1rem 0 0
145 - padding 0 .5rem
146 -
147 - form
148 - display inline-block
149 - margin 0
150 - padding 0
151 -
152 - > button
153 - background transparent
154 - border 0
155 - cursor pointer
156 - display inline-block
157 - margin 0
158 - overflow hidden
159 - padding 0
160 - text-decoration none
161 - vertical-align middle
162 -
163 - > img
164 - height .8rem
165 - width .8rem
166 -
167 - /* Votes counter */
168 - > .count
169 - margin 0 .5rem
170 -
171 - .upvoted
172 - background-color #d1ffd5
173 - border 1px dashed
174 - border-color #00e313
175 - border-radius .5rem
176 - color #00a200
177 - font-weight bolder
178 -
179 - .downvoted
180 - background-color #ffd9d9
181 - border 1px dashed
182 - border-color #ff7171
183 - border-radius .5rem
184 - color #f00
185 - font-weight bolder
186 -
187 - /* Home page */
188 - .posts
189 -
190 - /* A singe post */
191 - > .post
192 - display grid
193 - grid-template-columns min-content auto
194 - grid-column-gap 1.5rem
195 -
196 - margin-bottom 2rem
197 -
198 - /* Show numbered position in the list */
199 - > .position
200 - color #555
201 - font-style italic
202 - line-height 1.9rem
203 - text-align right
204 -
205 - > .info
206 - > .title > a
207 - color #000
208 - font-size 1.6rem
209 -
210 - /* Some post info showed below the title */
211 - > .about
212 - color #666
213 - margin .5rem 0 0 0
214 -
215 - > .pagination
216 - > form
217 - width 100%
218 -
219 - /* New submission page */
220 - > form.submit
221 - width 100%
222 -
223 - /* Page for a post */
224 - > .post
225 -
226 - /* Style used to format Markdown tables */
227 - table
228 - background #fff
229 - border-collapse collapse
230 - text-align left
231 -
232 - th
233 - border-bottom 2px solid #6678b1
234 - color #039
235 - font-weight normal
236 - padding 0 1em
237 -
238 - td
239 - border-bottom 1px solid #ccc
240 - color #669
241 - padding .5em 1em
242 -
243 - tbody tr:hover td
244 - color #009
245 -
246 - /* The post title */
247 - > .title
248 - font-size 1.5em
249 -
250 - /* Info below post title */
251 - > .info
252 - margin 1em 0
253 -
254 - > .username
255 - margin-left 1rem
256 -
257 - /* Post text */
258 - > .text
259 - margin 2rem 0
260 - word-wrap break-word
261 -
262 - /* The "new comment" form for this post page */
263 - .new_comment
264 - > input[type=submit]
265 - margin 1em 0
266 -
267 - /* Comments tree for the Post page */
268 - > .comments
269 - margin 5em 0 0 0
270 -
271 - /* A single comment in the tree */
272 - > .comment
273 - margin 0 0 1rem 0
274 - overflow hidden
275 -
276 - /* Some info about this comment */
277 - > .info
278 - display inline-block
279 - font-size .9rem
280 -
281 - > .username
282 - display inline-block
283 - margin 0 1rem
284 -
285 - > a, a:hover, a:visited
286 - display inline-block
287 - text-decoration none
288 -
289 - > .op
290 - background-color rgb(255, 175, 50)
291 - border-radius 4px
292 - font-weight bold
293 - padding 0 1rem
294 -
295 - > a, a:hover, a:visited
296 - color #fff
297 -
298 - /* The comment text */
299 - > .text
300 - word-wrap break-word
301 -
302 - /* Give the comment that's linked to in the URL location hash a lightyellow background color */
303 - .comment:target
304 - background-color lightyellow
305 -
306 - > .search
307 - margin-bottom 3rem
308 -
309 - /* User home page */
310 - > form > .user
311 - display grid
312 - grid-column-gap 3rem
313 - grid-row-gap 1rem
314 - grid-template-columns max-content auto
315 -
316 - /* User activity */
317 - > .user_activity
318 -
319 - > *
320 - margin 0 0 2em 0
321 -
322 - > .info
323 - color #888
324 -
325 - /* Login page */
326 - > .login
327 - @media only screen and (min-width: 1024px)
328 - margin auto
329 - max-width 40%
330 -
331 - > form > div
332 - margin 1rem 0
333 -
334 - /* Page to edit a post or a comment */
335 - > .edit
336 - {
337 - }
338 -
339 - /* Page to reply to a comment */
340 - > .reply
341 - {
342 - }
343 -
344 - /* About page */
345 - > .about
346 -
347 - > h3
348 - margin 1em 0 .5em 0
349 -
350 - > p
351 - line-height 1.5em
352 -
353 - > footer
354 - border-top 1px solid #ccc
355 - margin 3em 0 0 0
356 - padding 2em 0
357 -
358 - img
359 - height 1.2em
360 - margin 0 .5em 0 0
361 - vertical-align middle
362 -
363 - > ul
364 - list-style none
365 - margin 0
366 - overflow hidden
367 - padding 0
368 -
369 - > li
370 - float left
371 - margin 0 2em 0 0
372 -
373 - @media only screen and (max-width: 1024px)
374 - float none
375 - margin 1rem 0

+0/-261 D   freepost/static/stylus/reset.styl
index 576df43..0000000
old size: 6K - new size: 0B
deleted file mode: -rw-r--r--
@@ -1,261 +0,0 @@
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 - @media only screen and (max-width: 800px)
69 - .button
70 - font-size 1.2rem
71 - padding .5em 1em
72 - width 100%
73 -
74 - .button_ok /* Green */
75 - .button_ok:hover,
76 - .button_ok:visited
77 - background-color #4caf50
78 - color #fff
79 -
80 - .button_info /* Blue */
81 - .button_info:hover,
82 - .button_info:visited
83 - background-color #008cba
84 - color #fff
85 -
86 - .button_alert /* Red */
87 - .button_alert:hover,
88 - .button_alert:visited
89 - background-color #f44336
90 - color #fff
91 -
92 - .button_default /* Gray */
93 - .button_default:hover,
94 - .button_default:visited
95 - background-color #e7e7e7
96 - color #000
97 -
98 - .button_default1, /* Black */
99 - .button_default1:hover,
100 - .button_default1:visited
101 - background-color #555
102 - color #fff
103 -
104 - img
105 - /* Prevent images from taking up too much space in comments */
106 - max-width 100%
107 -
108 - label
109 - cursor pointer
110 - font-weight normal
111 -
112 - /* Add light blue shadow to form controls */
113 - .form-control:focus
114 - border-color #66afe9
115 - outline 0
116 - -webkit-box-shadow inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)
117 - box-shadow inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)
118 -
119 - .form-control
120 - display block
121 - width 100%
122 - padding .5em 1em
123 - line-height 1.42857143
124 - color #555
125 - border 1px solid #ccc
126 - border-radius 4px
127 - -webkit-box-shadow inset 0 1px 1px rgba(0,0,0,.075)
128 - box-shadow inset 0 1px 1px rgba(0,0,0,.075)
129 - -webkit-transition border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s
130 - -o-transition border-color ease-in-out .15s,box-shadow ease-in-out .15s
131 - transition border-color ease-in-out .15s,box-shadow ease-in-out .15s
132 -
133 - textarea.form-control
134 - height 8rem
135 -
136 - .pagination
137 - > form
138 - display inline-block
139 -
140 - > .page_number
141 - font-size .7rem
142 - font-weight bold
143 - margin 0 1rem
144 -
145 - /* When users vote, this <iframe/> is used as target, such that
146 - * the page is not reloaded
147 - */
148 - .vote_sink
149 - height 1px;
150 - left -10px
151 - position fixed
152 - top -10px
153 - width 1px
154 -
155 - html, body
156 - background-color #fff
157 - font-size 1em
158 - height 100%
159 - line-height 1em
160 - margin 0
161 - padding 0
162 - width 100%
163 -
164 - pre
165 - background-color #f9f9f9
166 - font-family "Courier 10 Pitch", Courier, monospace
167 - font-size 95%
168 - line-height 140%
169 - white-space pre
170 - white-space pre-wrap
171 - white-space -moz-pre-wrap
172 - white-space -o-pre-wrap
173 -
174 - code
175 - font-family Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace
176 - font-size 95%
177 - line-height 140%
178 - white-space pre
179 - white-space pre-wrap
180 - white-space -moz-pre-wrap
181 - white-space -o-pre-wrap
182 -
183 - /* Monospace <pre/> to write some nice ASCII art in frontpage */
184 - pre.new_year
185 - background-color transparent
186 - color #BF0000
187 - font-family monospace
188 - font-size .8rem
189 - font-webkit bold
190 - margin 0 0 2em 0
191 - text-align center
192 - white-space pre
193 - white-space pre-wrap
194 - white-space -moz-pre-wrap
195 - white-space -o-pre-wrap
196 -
197 - /* Inline code */
198 - :not(pre)
199 - > code
200 - background-color #f5f5f5
201 - border-radius 3px
202 - display inline-block
203 - font-family Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace
204 - font-size 85%
205 - line-height 140%
206 - margin 0 .2em
207 - padding .2em
208 - white-space pre
209 - white-space pre-wrap
210 - white-space -moz-pre-wrap
211 - white-space -o-pre-wrap
212 -
213 - /* A <div> that respects \n without converting to <br> */
214 - div.pre
215 - white-space pre
216 -
217 - select
218 - -webkit-appearance none
219 - -moz-appearance none
220 - appearance none
221 - background transparent
222 - border 0
223 - cursor pointer
224 -
225 - ul, ol
226 - margin 1.2em 2em
227 -
228 - /* Burger menu icon
229 - *
230 - * How to use:
231 - * <div class="burger-icon">
232 - * <div class="line1"></div>
233 - * <div class="line2"></div>
234 - * <div class="line3"></div>
235 - * </div>
236 - */
237 - .burger-icon
238 - display inline-block
239 - cursor pointer
240 - position relative
241 -
242 - > .line1, .line2, .line3
243 - background-color #000
244 - height 4px
245 - margin 4px 0
246 - transition .5s
247 - width 36px
248 -
249 - &.open
250 - > .line1
251 - transform rotate(-45deg) translate(-0px, 11px);
252 -
253 - > .line2
254 - opacity 0
255 -
256 - > .line3
257 - transform rotate(45deg) translate(-0px, -11px)
258 -
259 - &.notify
260 - > .line1, .line2, .line3
261 - background-color #f00

+3/-0 M   freepost/templates/banner.html
index 2086610..bcedc22
old size: 1K - new size: 1K
@@ -41,7 +41,10 @@ _\(/_ .:.*_\/_* : /\ :
41 41 </div>
42 42 {% endif %}
43 43 #}
44 +
45 + {# Retirement 2023
44 46 <div class="bg-green" style="margin: 0 0 2em 0; padding: .5em;">
45 47 <img alt="" title="" src="images/pulse.gif" style="height: 1em;" />
46 48 freepost will be retiring on December <b>2023</b>
47 49 </div>
50 + #}

+26/-0 A   freepost/templates/communities.html
index 0000000..a927647
old size: 0B - new size: 1K
new file mode: -rw-r--r--
@@ -0,0 +1,26 @@
1 + {% extends 'layout.html' %}
2 +
3 + {# Set variables for base layour #}
4 + {% set active_page = "communities" %}
5 + {% set title = 'List of communities' %}
6 +
7 + {% block content %}
8 +
9 + {% for community in communities|sort(attribute="name")|sort(attribute="members_count", reverse=True) %}
10 + <p>
11 + <div><a href="{{ url('community', cmty=community['name']) }}" title="{{ community['members_count'] }} members">{{ community["name"] }} ({{ community['members_count'] }} 👤)</a></div>
12 + <div>{{ community["description"] }}</div>
13 + </p>
14 + {% endfor %}
15 +
16 + <br /><br />
17 +
18 + {% if user %}
19 + {# onkeydown="" prevents submitting the form by pressing Enter (user must click submit button) #}
20 + <form action="{{ url('communities') }}" method="post" onkeydown="return event.key != 'Enter';">
21 + <input type="text" class="form-control form-control-inline" name="community_name" placeholder="community name" required minlength="3" maxlength="100" pattern="[^\s]+" />
22 + <input type="submit" class="button button_info" value="Create new community" />
23 + </form>
24 + {% endif %}
25 +
26 + {% endblock %}

+62/-0 A   freepost/templates/community.html
index 0000000..1067f00
old size: 0B - new size: 2K
new file mode: -rw-r--r--
@@ -0,0 +1,62 @@
1 + {% extends 'layout.html' %}
2 +
3 + {% set title = 'c/' + community %}
4 +
5 + {% block content %}
6 +
7 + <div class="community">
8 +
9 + <div class="about">
10 + <div>
11 + <b>{{ community_data.name }}</b>
12 + </div>
13 +
14 + <p>
15 + <span title="Description">{{ community_data.description }}</span>
16 + </p>
17 +
18 + {% if user %}
19 + <div>
20 + {% if community_data.allow_new_posts %}
21 + <a href="{{ url('submit', community=community_data.name) }}" class="flex-item button button_ok">
22 + Submit
23 + </a>
24 + {% endif %}
25 +
26 + {% if is_member %}
27 + <form action="" method="POST" class="flex-item button">
28 + <input type="submit" name="leave" value="Leave community" class="button button_transparent" />
29 + </form>
30 + {% else %}
31 + <form action="" method="POST" class="flex-item button">
32 + <input type="submit" name="join" value="Join community" class="button button_transparent" />
33 + </form>
34 + {% endif %}
35 + </div>
36 + {% endif %}
37 +
38 + <div>
39 + Members: {{ community_data.members_count }}
40 + </div>
41 + <div>
42 + Moderators:
43 + {% for mod in moderators %}
44 + <a href="{{ url('user_public', username=mod.username) }}">{{ mod.username }}</a>
45 + {% endfor %}
46 + </div>
47 + <div>
48 + Since: <span title="{{ community_data.created }}">{{ community_data.created|ago }}</span>
49 + </div>
50 +
51 + {% if is_moderator %}
52 + <a href="{{ url('community_administration', community=community_data.name) }}">
53 + Administration
54 + </a>
55 + {% endif %}
56 + </div>
57 +
58 + {% include 'posts.html' %}
59 +
60 + </div>
61 +
62 + {% endblock %}

+40/-0 A   freepost/templates/community_administration.html
index 0000000..074a24d
old size: 0B - new size: 974B
new file mode: -rw-r--r--
@@ -0,0 +1,40 @@
1 + {% extends 'layout.html' %}
2 +
3 + {# Set variables for base layour #}
4 + {% set title = 'Community administration' %}
5 +
6 + {% block content %}
7 +
8 + <h3>
9 + Community administration
10 + </h3>
11 +
12 + <br />
13 +
14 + <form action="" method="POST">
15 +
16 + <p>
17 + <b>Name:</b> {{ community['name'] }}
18 + </p>
19 +
20 + <p>
21 + <b>Description:</b>
22 + <textarea name="description" class="form-control" maxlength=1024>{{ community['description'] }}</textarea>
23 + </p>
24 +
25 + <p>
26 + <b>Allow users to submit posts:</b>
27 + <input type="radio" name="allow_new_posts" value="yes" {{ 'checked' if community['allow_new_posts'] }} /> Yes
28 + <input type="radio" name="allow_new_posts" value="no" {{ 'checked' if not community['allow_new_posts'] }} /> No
29 + </p>
30 +
31 + <br />
32 +
33 + <p>
34 + <input type="submit" class="button button_info" value="Update settings" />
35 + <a href="{{ url('community', cmty=community['name']) }}">Cancel</a>
36 + </p>
37 +
38 + </form>
39 +
40 + {% endblock %}

+2/-4 M   freepost/templates/homepage.html
index c25d2eb..4b8a1a0
old size: 226B - new size: 188B
@@ -1,14 +1,12 @@
1 1 {% extends 'layout.html' %}
2 2
3 3 {# Set variables for base layour #}
4 - {% set active_page = sort %}
5 4 {% set title = '' %}
6 5
7 6 {% block content %}
8 -
7 +
9 8 {% include 'banner.html' %}
10 -
11 - {% include 'posts.html' %}
12 9
10 + {% include 'posts.html' %}
13 11
14 12 {% endblock %}

+30/-50 M   freepost/templates/layout.html
index 0377c17..667da0c
old size: 6K - new size: 5K
@@ -10,73 +10,51 @@
10 10 <head>
11 11 <meta charset="utf-8">
12 12 <meta name="viewport" content="width=device-width, initial-scale=1">
13 -
13 +
14 14 <link href="/css/freepost.css" rel="stylesheet">
15 -
15 +
16 16 <title>{{ title ~ ' - ' if title else '' }}freepost</title>
17 17 </head>
18 -
18 +
19 19 <body>
20 20 <div class="container">
21 -
21 +
22 22 <div class="header">
23 -
23 +
24 24 <div class="title-small">
25 -
26 -
27 - {% if topic %}
28 - <a href="{{ url ('topic', name=topic) }}" class="flex-item logo">
29 - topic / {{ topic }}
30 - </a>
31 - {% else %}
32 - <a href="{{ url ('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
33 - free
34 - <img alt="🐵&nbsp;" title="freepost" src="/images/freepost.svg" class="monkey" />
35 - post
36 - </a>
37 - {% endif %}
38 -
25 + <a href="{{ url ('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
26 + free
27 + <img alt="🐵&nbsp;" title="freepost" src="/images/freepost.svg" class="monkey" />
28 + post
29 + </a>
30 +
39 31 <div class="burger-icon {{ 'notify' if unread_messages }}">
40 32 <div class="line1"></div>
41 33 <div class="line2"></div>
42 34 <div class="line3"></div>
43 35 </div>
44 36 </div>
45 -
37 +
46 38 <div class="title-large">
47 - {% if topic %}
48 - <a href="{{ url ('topic', name=topic) }}" class="flex-item logo">
49 - topic / {{ topic }}
50 - </a>
51 - {% else %}
52 - <a href="{{ url ('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
53 - freepost
54 - </a>
55 - {% endif %}
56 -
57 - <a href="{{ url ('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
39 + <a href="{{ url('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
40 + freepost
41 + </a>
42 +
43 + <a href="{{ url('homepage') }}{{ '?sort=new' if user and user.preferred_feed == 'new' }}" class="flex-item logo">
58 44 <img alt="🐵&nbsp;" title="freepost" src="/images/freepost.svg" class="monkey" />
59 45 </a>
60 46 </div>
61 -
47 +
62 48 <div class="menu" id="menu">
63 - <a href="{{ url ('topic', name=topic) if topic else url ('homepage') }}" class="flex-item {{ 'active_page' if active_page == 'hot' }}">
64 - Hot
65 - </a>
66 - <a href="{{ url ('topic', name=topic) if topic else url ('homepage') }}?sort=new" class="flex-item {{ 'active_page' if active_page == 'new' }}">
67 - New
49 +
50 + <a href="{{ url('communities') }}" class="flex-item {{ 'active_page' if active_page == 'communities' }}">
51 + Communities
68 52 </a>
69 -
70 - {% if user %}
71 - <a href="{{ url ('submit') }}{{ '?topic=' ~ topic if topic }}" class="flex-item {{ 'active_page' if active_page == 'submit' }}">
72 - Submit
73 - </a>
74 - {% endif %}
75 -
53 +
76 54 <a href="{{ url ('search') }}" class="flex-item {{ 'active_page' if active_page == 'search' }}">Search</a>
77 -
55 +
78 56 <a href="{{ url ('about') }}" class="flex-item {{ 'active_page' if active_page == 'about' }}">About</a>
79 -
57 +
80 58 {% if user %}
81 59 {% if unread_messages %}
82 60 <a href="{{ url ('user_replies') }}" class="new_messages flex-item">
@@ -87,19 +65,21 @@
87 65 {{ user.username }}
88 66 </a>
89 67 {% endif %}
90 -
68 +
91 69 <a href="{{ url ('logout') }}" class="flex-item">Log out</a>
92 70 {% else %}
93 71 <a href="{{ url ('login') }}" class="flex-item {{ 'active_page' if active_page == 'login' }}">Log in</a>
94 72 {% endif %}
95 73 </div>
96 74 </div>
97 -
75 +
98 76 <div class="content">
99 77 {% block content %}{% endblock %}
100 78 </div>
101 79
102 80 <footer>
81 + <img alt="🐵&nbsp;" title="freepost" src="/images/freepost.svg" class="monkey" />
82 +
103 83 <ul>
104 84 <li>
105 85 <img alt="RSS" title="" src="/images/rss.png" />
@@ -124,8 +104,8 @@
124 104 </footer>
125 105 </div>
126 106
127 - {# When users vote, this <iframe/> is used as target, such that
128 - # the page is not reloaded
107 + {# When users vote, this <iframe/> is used as <form> target, such that
108 + # the page is not reloaded.
129 109 #}
130 110 <iframe name="vote_sink" class="vote_sink"></iframe>
131 111

+25/-20 M   freepost/templates/post.html
index d31344d..652d23d
old size: 4K - new size: 4K
@@ -19,44 +19,49 @@
19 19 {{ post.title }}
20 20 {% endif %}
21 21 </div>
22 -
22 +
23 23 {% if post.link %}
24 24 <div class="netloc">
25 25 {{ post.link|netloc }}
26 26 </div>
27 27 {% endif %}
28 -
28 +
29 + {#
29 30 <div class="topics">
30 31 {% for topic in topics %}
31 32 <a href="{{ url ('topic', name=topic.name) }}" class="topic">{{ topic.name }}</a>
32 33 {% endfor %}
33 34 </div>
34 -
35 + #}
36 +
35 37 <div class="info">
36 - {{ vote ('post', post, user) }}
37 -
38 - <a href="{{ url ('user_public', username=post.username) }}" class="username">
38 + {{ vote('post', post, user) }}
39 +
40 + <a href="{{ url('community', cmty=post['community_name']) }}">c/{{ post['community_name'] }}</a>
41 +
42 + <span class="username">Posted by</span>
43 + <a href="{{ url('user_public', username=post.username) }}">
39 44 {{ post.username }}
40 45 </a>
41 46 <time title="{{ post.created|title }}" datetime="{{ post.created|datetime }}">
42 47 <em>{{ post.created|ago }}</em>
43 48 </time>
44 -
49 +
45 50
46 -
51 +
47 52 {{ post.vote }} votes, <a href="#comments">{{ post.commentsCount }} comments</a>
48 -
53 +
49 54
50 -
55 +
51 56 {% if not show_source %}
52 57 <a href="{{ url ('post', hash_id=post.hashId) }}?source" title="Show unrendered text">Source</a>
53 58 {% endif%}
54 -
59 +
55 60 {% if user and post.userId == user.id %}
56 61 <a href="{{ url ('edit_post', hash_id=post.hashId) }}" title="Edit post">Edit</a>
57 62 {% endif %}
58 63 </div>
59 -
64 +
60 65 <div class="text">
61 66 {% if show_source %}
62 67 <div class="pre">{{ post.text }}</div>
@@ -64,7 +69,7 @@
64 69 {{ post.text|md2html|safe }}
65 70 {% endif %}
66 71 </div>
67 -
72 +
68 73 <form action="" method="post" class="new_comment">
69 74 <textarea
70 75 name="new_comment"
@@ -78,7 +83,7 @@
78 83 class="button button_info"
79 84 {{ 'disabled' if not user }}/>
80 85 </form>
81 -
86 +
82 87 {# id="" used as anchor #}
83 88 <div class="comments" id="comments">
84 89 {% for comment in comments %}
@@ -86,28 +91,28 @@
86 91 <div class="comment" style="margin-left:{{ comment.depth * 2 }}em" id="comment-{{ comment.hashId }}">
87 92 <div class="info">
88 93 {{ vote ('comment', comment, user) }}
89 -
94 +
90 95 {# Username #}
91 96 <span class="username {{ 'op' if post.userId == comment.userId else '' }}">
92 97 <a href="{{ url ('user_public', username=comment.username) }}">{{ comment.username }}</a>
93 98 </span>
94 -
99 +
95 100 {# DateTime #}
96 101 <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>
97 -
102 +
98 103 {% if user %}
99 104
100 -
105 +
101 106 {# Reply #}
102 107 <a href="{{ url ('reply', hash_id=comment.hashId) }}">Reply</a>
103 -
108 +
104 109 {# Edit #}
105 110 {% if comment.userId == user.id %}
106 111 <a href="{{ url ('edit_comment', hash_id=comment.hashId) }}">Edit</a>
107 112 {% endif %}
108 113 {% endif %}
109 114 </div>
110 -
115 +
111 116 <div class="text">
112 117 {% if show_source %}
113 118 <div class="pre">{{ comment.text }}</div>

+35/-22 M   freepost/templates/posts.html
index 25a33ab..445b60e
old size: 5K - new size: 5K
@@ -2,20 +2,29 @@
2 2 {% set page_number = request.query.page|int or 0 %}
3 3
4 4 <div class="posts">
5 + <div class="menu">
6 + <a href="?sort=hot" class="flex-item {{ 'active_page' if sort == 'hot' }}">
7 + Hot
8 + </a>
9 + <a href="?sort=new" class="flex-item {{ 'active_page' if sort == 'new' }}">
10 + New
11 + </a>
12 + </div>
13 +
5 14 {% for post in posts %}
6 15 {% set topics = split_topics (post.topics) %}
7 -
16 +
8 17 <div class="post">
9 18 {# Print the item position number #}
10 19 <div class="position">
11 20 {{ page_number * settings ('defaults', 'items_per_page') + loop.index }}.
12 21 </div>
13 -
22 +
14 23 <div class="info">
15 24 <div class="title">
16 25 <a href="{{ post.link if post.link and post.link|length > 0 else url ('post', hash_id=post.hashId) }}">
17 26 {{ post.title }}
18 -
27 +
19 28 {# Post content preview #}
20 29 {% if post.text %}
21 30 <img
@@ -25,7 +34,7 @@
25 34 class="text_preview" />
26 35 {% endif %}
27 36 </a>
28 -
37 +
29 38 {# URL hostname #}
30 39 {% if post.link %}
31 40 <span class="netloc">
@@ -33,16 +42,26 @@
33 42 </span>
34 43 {% endif %}
35 44 </div>
36 -
45 +
37 46 <div class="topics">
38 47 {% for topic in topics %}
39 48 <a href="{{ url ('topic', name=topic) }}" class="topic">{{ topic }}</a>
40 49 {% endfor %}
41 50 </div>
42 -
51 +
43 52 <div class="about">
44 53 {{ vote ('post', post, user) }}
45 -
54 +
55 + <a href="{{ url('community', cmty=post['community_name']) }}">c/{{ post['community_name'] }}</a>
56 +
57 + ·
58 +
59 + Posted by
60 +
61 + <a href="{{ url ('user_public', username=post.username) }}">
62 + {{ post.username }}
63 + </a>
64 +
46 65 <em class="username">
47 66 <a href="{{ url ('post', hash_id=post.hashId) }}">
48 67 <time title="{{ post.created|title }}" datetime="{{ post.created }}">
@@ -50,15 +69,9 @@
50 69 </time>
51 70 </a>
52 71 </em>
53 -
54 - by
55 -
56 - <a href="{{ url ('user_public', username=post.username) }}">
57 - {{ post.username }}
58 - </a>
59 -
72 +
60 73
61 -
74 +
62 75 <a href="{{ url ('post', hash_id=post.hashId) }}#comments">
63 76 {% if post.commentsCount > 0 %}
64 77 {{ post.commentsCount }} comments
@@ -69,9 +82,9 @@
69 82 </div>
70 83 </div>
71 84 </div>
72 -
85 +
73 86 {% endfor %}
74 -
87 +
75 88 <div class="pagination">
76 89 {% if page_number > 0 %}
77 90 <form>
@@ -80,14 +93,14 @@
80 93 <input type="hidden" name="{{ key }}" value="{{ value }}" />
81 94 {% endif %}
82 95 {% endfor %}
83 -
96 +
84 97 <button type="submit"
85 98 name="page"
86 99 value="{{ page_number - 1 }}"
87 100 class="button button_default1">
88 101 Previous
89 102 </button>
90 -
103 +
91 104 {% if page_number > 4 %}
92 105 <button type="submit"
93 106 name="page"
@@ -98,13 +111,13 @@
98 111 {% endif %}
99 112 </form>
100 113 {% endif %}
101 -
114 +
102 115 {% if page_number > 0 %}
103 116 <span class="page_number">
104 117 PAGE: {{ page_number }}
105 118 </span>
106 119 {% endif %}
107 -
120 +
108 121 {% if posts %}
109 122 <form>
110 123 {% for key, value in request.query.items () %}
@@ -112,7 +125,7 @@
112 125 <input type="hidden" name="{{ key }}" value="{{ value }}" />
113 126 {% endif %}
114 127 {% endfor %}
115 -
128 +
116 129 <button type="submit"
117 130 name="page"
118 131 value="{{ page_number + 5 }}"

+3/-3 M   freepost/templates/search.html
index a53f7d3..200e6bd
old size: 1011B - new size: 1K
@@ -11,8 +11,8 @@
11 11 <div class="search">
12 12 <form action="/search">
13 13 <p>
14 - <input type="text" name="q" value="{{ request.query.q or '' }}" placeholder="Search..." required="required" />
15 - <input type="submit" value="Search" />
14 + <input type="text" class="form-control form-control-inline" name="q" value="{{ request.query.q or '' }}" placeholder="Search..." required="required" />
15 + <input type="submit" class="button button_info" value="Search" />
16 16 </p>
17 17 <p>
18 18 Order by
@@ -27,7 +27,7 @@
27 27 </p>
28 28 </form>
29 29 </div>
30 -
30 +
31 31 {% include 'posts.html' %}
32 32
33 33 {% endblock %}

+5/-5 M   freepost/templates/submit.html
index 5613c42..c16121e
old size: 1K - new size: 1K
@@ -1,17 +1,16 @@
1 1 {% extends 'layout.html' %}
2 2
3 3 {# Set variables for base layour #}
4 - {% set active_page = 'submit' %}
5 4 {% set title = 'Submit' %}
6 5
7 6 {% block content %}
8 -
7 +
9 8 {% if flash %}
10 9 <div class="alert bg-red">
11 10 {{ flash|safe }}
12 11 </div>
13 12 {% endif %}
14 -
13 +
15 14 <form action="" method="post" class="submit">
16 15 <h3>Title</h3>
17 16 <div>
@@ -22,7 +21,7 @@
22 21 <div>
23 22 <input type="text" name="link" class="form-control" />
24 23 </div>
25 -
24 +
26 25 {% if settings ('defaults', 'topics_per_post') > 0 %}
27 26 <h3>Topics</h3>
28 27 <div>
@@ -35,12 +34,13 @@
35 34 {% endif %}
36 35
37 36 <h3>Text</h3>
37 + <div><em>Markdown is enabled</em></div>
38 38 <div>
39 39 <textarea name="text" rows=10 class="form-control"></textarea>
40 40 </div>
41 41
42 42 <div style="margin: 1em 0 0 0;">
43 - <input type="submit" class="button button_info" value="Submit post" />
43 + <input type="submit" class="button button_info" value="Submit post to c/{{ community }}" />
44 44 </div>
45 45 </form>
46 46

+1/-1 M   settings.yaml
index cb39d0f..ae6e1dc
old size: 869B - new size: 865B
@@ -23,7 +23,7 @@ email:
23 23 session:
24 24 # Name to use for the session cookie
25 25 name: freepost
26 -
26 +
27 27 # Timeout in seconds for the "remember me" option.
28 28 # By default, if the user doesn't click "remember me" during login the
29 29 # session will end when the browser is closed.