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) |
-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 |
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. |
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') |
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. |
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 |
index f09e9e3..0000000 | |||
old size: 259B - new size: 0B | |||
deleted file mode: -rw-r--r-- |
Binary file |
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 | }); |
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 |
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 |
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 | + | #} |
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 %} |
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 %} |
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 %} |
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 %} |
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="🐵 " 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="🐵 " 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="🐵 " 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="🐵 " 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 |
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> |
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 }}" |
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 %} |
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 |
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. |