From ebe84c38966ad541b4e79b1411ad38256f18ea17 Mon Sep 17 00:00:00 2001
From: zPlus
Date: Mon, 19 Jun 2023 13:52:30 +0200
Subject: [PATCH] Add basic support for communities.
---
README.md => README | 13 +-
freepost.cgi | 12 +-
freepost/__init__.py | 318 +++++++++++----
freepost/database.py | 265 ++++++++++---
freepost/static/images/libre.exchange.png | Bin 259 -> 0 bytes
freepost/static/javascript/freepost.js | 47 +--
freepost/static/stylus/freepost.styl | 375 ------------------
freepost/static/stylus/reset.styl | 261 ------------
freepost/templates/banner.html | 3 +
freepost/templates/communities.html | 26 ++
freepost/templates/community.html | 62 +++
.../templates/community_administration.html | 40 ++
freepost/templates/homepage.html | 6 +-
freepost/templates/layout.html | 80 ++--
freepost/templates/post.html | 45 ++-
freepost/templates/posts.html | 57 ++-
freepost/templates/search.html | 6 +-
freepost/templates/submit.html | 10 +-
settings.yaml | 2 +-
19 files changed, 714 insertions(+), 914 deletions(-)
rename README.md => README (83%)
delete mode 100644 freepost/static/images/libre.exchange.png
delete mode 100644 freepost/static/stylus/freepost.styl
delete mode 100644 freepost/static/stylus/reset.styl
create mode 100644 freepost/templates/communities.html
create mode 100644 freepost/templates/community.html
create mode 100644 freepost/templates/community_administration.html
diff --git a/README.md b/README
similarity index 83%
rename from README.md
rename to README
index 827f0591..601ab52d 100644
--- a/README.md
+++ b/README
@@ -18,24 +18,17 @@ users can read and comment.
source venv/bin/activate
python3 -m bottle --debug --reload --bind 127.0.0.1:8000 freepost
-## Build stylesheets
-
-Build CSS files
-
- stylus --watch --compress --disable-cache --out freepost/static/css/ freepost/static/stylus/freepost.styl
-
# Deployment
-- Build CSS stylesheets (see `Development` above)
- Copy all files to your `public_html` folder
-- Make sure `settings.yaml` has restricted permissions, for instance `0600`
-- If the SQLite database is located in the same HTML folder, make sure this too has
- restricted access
- Rename `.htaccess.wsgi` or `.htaccess.cgi` to `.htaccess` (if you use CGI or WSGI)
- Change settings in `settings.yaml` if needed
- Create Python virtual environment
For tuxfamily only: run `newgrp freepost` before creating the virtenv, for quota reasons
- Create a new empty SQLite database: `cat database.schema.sql | sqlite3 database.sqlite`
+- Make sure `settings.yaml` has restricted permissions, for instance `0600`.
+ If the SQLite database is located in the same HTML folder, make sure this too has
+ restricted access
Everything should be setup and working. Make sure your CGI or WSGI server is
configured correctly.
diff --git a/freepost.cgi b/freepost.cgi
index 6047e4f1..c6d4c2ee 100755
--- a/freepost.cgi
+++ b/freepost.cgi
@@ -1,4 +1,14 @@
#!./venv/bin/python3
+import os
from freepost import bottle
-bottle.run (server='cgi')
+
+# freepost uses Bottle's function get_url() extensively (see https://bottlepy.org/docs/dev/_modules/bottle.html#Bottle.get_url
+# for a description). This function uses the env variable SCRIPT_NAME internally,
+# which is set by Apache to "/freepost.cgi" when redirecting URLs from .htaccess.
+# The result is that all the URLs created by get_url() will start with "/freepost.cgi",
+# for example "/freepost.cgi/post/" instead of "/post/".
+# So, here it's overwritten to an empty string in order to remove the script name from the URLs.
+os.environ['SCRIPT_NAME'] = ''
+
+bottle.run(server='cgi')
diff --git a/freepost/__init__.py b/freepost/__init__.py
index a0bd7a59..29c4da07 100644
--- a/freepost/__init__.py
+++ b/freepost/__init__.py
@@ -33,18 +33,20 @@ template = functools.partial (
template_settings = {
'filters': {
'ago': lambda date: timeago.format(date),
- 'datetime': lambda date: date,# date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'),
+ 'datetime': lambda date: date, #date.strftime('%b %-d, %Y - %H:%M%p%z%Z'),
# TODO this should be renamed. It's only a way to pretty print dates
'title': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
# Convert markdown to plain text
'md2txt': lambda text: bleach.clean (markdown.markdown(text),
tags=[], attributes={}, strip=True),
# Convert markdown to html
- 'md2html': lambda text: bleach.clean (bleach.linkify (markdown.markdown (
- text,
- # https://python-markdown.github.io/extensions/
- extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ],
- output_format='html5'))),
+ 'md2html': lambda text: bleach.clean(
+ markdown.markdown(
+ text,
+ # https://python-markdown.github.io/extensions/
+ extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ],
+ output_format='html5'),
+ tags = list(bleach.sanitizer.ALLOWED_TAGS) + [ 'br', 'img', 'p', 'pre', 'h1', 'h2', 'h3', 'hr' ]),
# Get the domain part of a URL
'netloc': lambda url: urlparse (url).netloc
},
@@ -63,11 +65,6 @@ template = functools.partial (
'autoescape': True
})
-# "bleach" library is used to sanitize the HTML output of jinja2's "md2html"
-# filter. The library has only a very restrictive list of white-listed
-# tags, so we add some more here.
-# The list() casting is required because it's of type "frozenlist"
-bleach.sanitizer.ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [ 'br', 'img', 'p', 'pre', 'h1', 'h2', 'h3', 'hr' ]
bleach.sanitizer.ALLOWED_ATTRIBUTES.update ({
'img': [ 'src' ]
})
@@ -76,31 +73,31 @@ from freepost import database, mail, session
# Decorator.
# Make sure user is logged in
-def requires_login (controller):
- def wrapper ():
- session_token = request.get_cookie (
+def requires_login(controller):
+ def wrapper(*args, **kwargs):
+ session_token = request.get_cookie(
key = settings['session']['name'],
secret = settings['cookies']['secret'])
- if database.is_valid_session (session_token):
- return controller ()
+ if database.is_valid_session(session_token):
+ return controller(*args, **kwargs)
else:
- redirect (application.get_url ('login'))
+ redirect(application.get_url('login'))
return wrapper
# Decorator.
# Make sure user is logged out
-def requires_logout (controller):
- def wrapper ():
- session_token = request.get_cookie (
+def requires_logout(controller):
+ def wrapper(*args, **kwargs):
+ session_token = request.get_cookie(
key = settings['session']['name'],
secret = settings['cookies']['secret'])
- if database.is_valid_session (session_token):
- redirect (application.get_url ('user_settings'))
+ if database.is_valid_session(session_token):
+ redirect(application.get_url('user_settings'))
else:
- return controller ()
+ return controller(*args, **kwargs)
return wrapper
@@ -470,15 +467,165 @@ def user_public_homepage (username):
return template ('user_public_homepage.html', account=account)
-@get ('/post/', name='post')
-def post_thread (hash_id):
+@get('/c', name='communities')
+def communities():
+ """
+ List communities.
+ """
+
+ communities_list = database.get_communities_list()
+
+ return template('communities.html', communities=communities_list)
+
+@post('/c')
+@requires_login
+def community_create():
+ name = request.forms.getunicode('community_name').strip().replace(' ', '').lower()
+
+ if len(name) < 3 or len(name) > 100:
+ redirect(application.get_url('communities'))
+
+ user = session.user()
+
+ community = database.get_community(name)
+ if community:
+ redirect(application.get_url('community', cmty=name))
+
+ database.create_community(name)
+
+ community = database.get_community(name)
+
+ # The community wasn't created for some reasons?
+ if not community:
+ redirect(application.get_url('communities'))
+ else:
+ database.add_community_member(community['id'], user['id'], True)
+ redirect(application.get_url('community', cmty=name))
+
+@get('/c/', name='community')
+def community(cmty):
+ """
+ Show a community.
+ """
+
+ # Sort order
+ sort = request.query.sort or 'hot'
+
+ user = session.user()
+ cmty = database.get_community(cmty)
+
+ if not cmty:
+ abort(404, "This community doesn't exist.")
+
+ mods = database.get_community_mods(cmty['id'])
+
+ try:
+ is_moderator = database.is_community_moderator(cmty['id'], user['id'])
+ is_member = database.is_community_member(cmty['id'], user['id'])
+ except:
+ is_moderator = False
+ is_member = False
+
+ # Page number
+ page = int(request.query.page or 0)
+
+ if page < 0:
+ redirect(application.get_url('homepage'))
+
+ if sort in [ 'hot', 'new' ]:
+ posts = database.get_posts(
+ page, user['id'] if user else None, sort,
+ topic=None, community_id=cmty['id'])
+ else:
+ posts = []
+
+ return template(
+ 'community.html',
+ community=cmty['name'],
+ community_data=cmty,
+ is_member=is_member,
+ is_moderator=is_moderator,
+ moderators=mods,
+ posts=posts,
+ sort=sort)
+
+@get('/c//administration', name='community_administration')
+@requires_login
+def community_administration(community):
+ """
+ Administration page of community.
+ """
+
+ user = session.user()
+ community = database.get_community(community)
+
+ try:
+ is_moderator = database.is_community_moderator(community['id'], user['id'])
+ assert is_moderator
+ except:
+ redirect(application.get_url('homepage'))
+ abort()
+
+ mods = database.get_community_mods(community['id'])
+
+ return template(
+ 'community_administration.html',
+ community=community,
+ moderators=mods)
+
+@post('/c//administration')
+@requires_login
+def community_administration_update(community):
+ """
+ Update community settings.
+ """
+
+ user = session.user()
+ community = database.get_community(community)
+
+ try:
+ is_moderator = database.is_community_moderator(community['id'], user['id'])
+ assert is_moderator
+ except:
+ redirect(application.get_url('homepage'))
+ abort()
+
+ # Truncate at 1K
+ description = request.forms.getunicode('description').strip()[:1024]
+ allow_new_posts = request.forms.getunicode('allow_new_posts') == 'yes'
+
+ database.update_community_settings(
+ community['id'], description, allow_new_posts)
+
+ redirect(application.get_url('community', cmty=community['name']))
+
+@post('/c/')
+@requires_login
+def community_join(community):
+ user = session.user()
+ cmty = database.get_community(community)
+
+ if not cmty:
+ abort(500, "Community doesn't exist.")
+ return
+
+ if 'join' in request.forms:
+ database.add_community_member(cmty['id'], user['id'])
+
+ if 'leave' in request.forms:
+ database.remove_community_member(cmty['id'], user['id'])
+
+ redirect(application.get_url('community', cmty=community))
+
+@get('/post/', name='post')
+def post_thread(hash_id):
"""
Display a single post with all its comments.
"""
- user = session.user ()
- post = database.get_post (hash_id, user['id'] if user else None)
- comments = database.get_post_comments (post['id'], user['id'] if user else None)
+ user = session.user()
+ post = database.get_post(hash_id, user['id'] if user else None)
+ comments = database.get_post_comments(post['id'], user['id'] if user else None)
topics = database.get_post_topics (post['id'])
# Group comments by parent
@@ -509,7 +656,7 @@ def post_thread (hash_id):
return temp_comments
- comments = children ()
+ comments = children()
# Show posts/comments Markdown instead of rendered text
show_source = 'source' in request.query
@@ -522,6 +669,7 @@ def post_thread (hash_id):
return template (
'post.html',
+ community=post['community_name'],
post=post,
comments=comments,
topics=topics,
@@ -531,15 +679,15 @@ def post_thread (hash_id):
'comment': {}
})
+@post('/post/')
@requires_login
-@post ('/post/')
-def new_comment (hash_id):
+def new_comment(hash_id):
# The comment text
- comment = request.forms.getunicode ('new_comment').strip ()
+ comment = request.forms.getunicode('new_comment').strip()
# Empty comment?
- if len (comment) == 0:
- redirect (application.get_url ('post', hash_id=hash_id))
+ if len(comment) == 0:
+ redirect(application.get_url('post', hash_id=hash_id))
# Retrieve the post
post = database.get_post (hash_id)
@@ -560,45 +708,45 @@ def new_comment (hash_id):
redirect (application.get_url ('post', hash_id=hash_id))
-@requires_login
@get ('/edit/post/', name='edit_post')
-def edit_post (hash_id):
- user = session.user ()
- post = database.get_post (hash_id, user['id'])
+@requires_login
+def edit_post(hash_id):
+ user = session.user()
+ post = database.get_post(hash_id, user['id'])
# Make sure the session user is the actual poster/commenter
if post['userId'] != user['id']:
- redirect (application.get_url ('post', hash_id=hash_id))
+ redirect(application.get_url('post', hash_id=hash_id))
- return template ('edit_post.html', post=post)
+ return template('edit_post.html', community=post['community_name'], post=post)
-@requires_login
@post ('/edit/post/')
-def edit_post_check (hash_id):
- user = session.user ()
- post = database.get_post (hash_id, user['id'])
+@requires_login
+def edit_post_check(hash_id):
+ user = session.user()
+ post = database.get_post(hash_id, user['id'])
# Make sure the session user is the actual poster/commenter
if post['userId'] != user['id']:
- redirect (application.get_url ('homepage'))
+ redirect(application.get_url('homepage'))
# MUST have a title. If empty, use original title
- title = request.forms.getunicode ('title').strip ()
+ title = request.forms.getunicode('title').strip()
if len (title) == 0: title = post['title']
- link = request.forms.getunicode ('link').strip () if 'link' in request.forms else ''
- text = request.forms.getunicode ('text').strip () if 'text' in request.forms else ''
+ link = request.forms.getunicode('link').strip() if 'link' in request.forms else ''
+ text = request.forms.getunicode('text').strip() if 'text' in request.forms else ''
# If there is a URL, make sure it has a "scheme"
- if len (link) > 0 and urlparse (link).scheme == '':
+ if len(link) > 0 and urlparse(link).scheme == '':
link = 'http://' + link
# Update post
- database.update_post (title, link, text, hash_id, user['id'])
+ database.update_post(title, link, text, hash_id, user['id'])
- redirect (application.get_url ('post', hash_id=hash_id))
+ redirect(application.get_url ('post', hash_id=hash_id))
-@requires_login
@get ('/edit/comment/', name='edit_comment')
+@requires_login
def edit_comment (hash_id):
user = session.user ()
comment = database.get_comment (hash_id, user['id'])
@@ -609,8 +757,8 @@ def edit_comment (hash_id):
return template ('edit_comment.html', comment=comment)
-@requires_login
@post ('/edit/comment/')
+@requires_login
def edit_comment_check (hash_id):
user = session.user ()
comment = database.get_comment (hash_id, user['id'])
@@ -627,37 +775,37 @@ def edit_comment_check (hash_id):
redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + hash_id)
-@get ('/submit')
+@get('/c//submit', name='submit')
@requires_login
-def submit ():
+def submit(community):
"""
Submit a new post.
"""
- return template ('submit.html')
+ return template('submit.html', community=community)
-@post ('/submit', name='submit')
+@post('/c//submit')
@requires_login
-def submit_check ():
+def submit_check(community):
"""
Check submission of new post.
"""
# Somebody sent a
+ {% endfor %}
+
+
+
+ {% if user %}
+ {# onkeydown="" prevents submitting the form by pressing Enter (user must click submit button) #}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/freepost/templates/community.html b/freepost/templates/community.html
new file mode 100644
index 00000000..1067f002
--- /dev/null
+++ b/freepost/templates/community.html
@@ -0,0 +1,62 @@
+{% extends 'layout.html' %}
+
+{% set title = 'c/' + community %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/freepost/templates/community_administration.html b/freepost/templates/community_administration.html
new file mode 100644
index 00000000..074a24d1
--- /dev/null
+++ b/freepost/templates/community_administration.html
@@ -0,0 +1,40 @@
+{% extends 'layout.html' %}
+
+{# Set variables for base layour #}
+{% set title = 'Community administration' %}
+
+{% block content %}
+
+
+ Community administration
+
+
+
+
+
+
+{% endblock %}
diff --git a/freepost/templates/homepage.html b/freepost/templates/homepage.html
index c25d2eba..4b8a1a07 100644
--- a/freepost/templates/homepage.html
+++ b/freepost/templates/homepage.html
@@ -1,14 +1,12 @@
{% extends 'layout.html' %}
{# Set variables for base layour #}
-{% set active_page = sort %}
{% set title = '' %}
{% block content %}
-
+
{% include 'banner.html' %}
-
- {% include 'posts.html' %}
+ {% include 'posts.html' %}
{% endblock %}
diff --git a/freepost/templates/layout.html b/freepost/templates/layout.html
index 0377c17a..667da0cb 100644
--- a/freepost/templates/layout.html
+++ b/freepost/templates/layout.html
@@ -10,73 +10,51 @@
-
+
-
+
{{ title ~ ' - ' if title else '' }}freepost
-
+
-
+
-
+
{% block content %}{% endblock %}
+
+
@@ -124,8 +104,8 @@
- {# When users vote, this is used as target, such that
- # the page is not reloaded
+ {# When users vote, this is used as target, such that
+ # the page is not reloaded.
#}
diff --git a/freepost/templates/post.html b/freepost/templates/post.html
index d31344dd..652d23d4 100644
--- a/freepost/templates/post.html
+++ b/freepost/templates/post.html
@@ -19,44 +19,49 @@
{{ post.title }}
{% endif %}
-
+
{% if post.link %}
{{ post.link|netloc }}
{% endif %}
-
+
+ {#
-
+ #}
+
-
+
{% if show_source %}
{{ post.text }}
@@ -64,7 +69,7 @@
{{ post.text|md2html|safe }}
{% endif %}
-
+
-
+
{# id="" used as anchor #}
Title
Topics
Text
+