mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
view community, view posts in community
This commit is contained in:
parent
c8f76c2d54
commit
2c0fc55e35
18 changed files with 365 additions and 28 deletions
|
@ -5,7 +5,7 @@ starting any large pieces of work to ensure alignment with the roadmap, architec
|
|||
|
||||
The general style and philosphy behind the way things have been constructed is well described by
|
||||
[The Grug Brained Developer](https://grugbrain.dev/). If that page resonates with you then you'll
|
||||
probably enjoy your time here! Our code needs to be simple enough that new developers of all
|
||||
probably enjoy your time here! The codebase needs to be simple enough that new developers of all
|
||||
skill levels can easily understand what's going on and onboard quickly without a lot of upfront
|
||||
time investment. Sometimes this will mean writing slightly more verbose/boring code or avoiding the
|
||||
use of advanced design patterns.
|
||||
|
@ -33,6 +33,8 @@ VS Code coders are encouraged to try the free community edition of PyCharm but i
|
|||
|
||||
Use PEP 8 conventions for line length, naming, indentation. Use descriptive commit messages.
|
||||
|
||||
Database model classes are singular. As in "Car", not "Cars".
|
||||
|
||||
### Directory structure
|
||||
|
||||
Where possible, the structure should match the URL structure of the site. e.g. "domain.com/admin"
|
||||
|
@ -50,8 +52,6 @@ helpful functions that pertain to modules in that directory only.
|
|||
Changes to this file are turned into changes in the DB by using '[migrations](https://www.onlinetutorialspoint.com/flask/flask-how-to-upgrade-or-downgrade-database-migrations.html)'.
|
||||
- /community/* pertains to viewing, posting within and managing communities.
|
||||
|
||||
Python developers who are new to Flask will be able to quickly become productive with
|
||||
|
||||
# Code of conduct
|
||||
|
||||
## Our Pledge
|
||||
|
|
|
@ -271,6 +271,7 @@ def shared_inbox():
|
|||
|
||||
if post is not None:
|
||||
db.session.add(post)
|
||||
community.post_count += 1
|
||||
db.session.commit()
|
||||
else:
|
||||
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
|
||||
|
@ -294,6 +295,7 @@ def shared_inbox():
|
|||
|
||||
if post_reply is not None:
|
||||
db.session.add(post_reply)
|
||||
community.post_reply_count += 1
|
||||
db.session.commit()
|
||||
else:
|
||||
activity_log.exception_message = 'Unacceptable type: ' + object_type
|
||||
|
@ -398,6 +400,7 @@ def shared_inbox():
|
|||
if join_request:
|
||||
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||
db.session.add(member)
|
||||
community.subscriptions_count += 1
|
||||
db.session.commit()
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
|
|
|
@ -68,3 +68,8 @@ class CreatePost(FlaskForm):
|
|||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class NewReplyForm(FlaskForm):
|
||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?'})
|
||||
submit = SubmitField(_l('Comment'))
|
||||
|
|
|
@ -2,13 +2,13 @@ from datetime import date, datetime, timedelta
|
|||
|
||||
import markdown2
|
||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app import db
|
||||
from app.activitypub.signature import RsaKeys, HttpSignature
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post
|
||||
|
@ -70,8 +70,8 @@ def add_remote():
|
|||
def show_community(community: Community):
|
||||
mods = community.moderators()
|
||||
|
||||
is_moderator = any(mod.user_id == current_user.id for mod in mods)
|
||||
is_owner = any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
|
||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||
is_owner = current_user.is_authenticated and any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
|
||||
|
||||
if community.private_mods:
|
||||
mod_list = []
|
||||
|
@ -80,10 +80,11 @@ def show_community(community: Community):
|
|||
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
||||
|
||||
return render_template('community/community.html', community=community, title=community.title,
|
||||
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list)
|
||||
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=community.posts)
|
||||
|
||||
|
||||
@bp.route('/<actor>/subscribe', methods=['GET'])
|
||||
@login_required
|
||||
def subscribe(actor):
|
||||
remote = False
|
||||
actor = actor.strip()
|
||||
|
@ -137,6 +138,7 @@ def subscribe(actor):
|
|||
|
||||
|
||||
@bp.route('/<actor>/unsubscribe', methods=['GET'])
|
||||
@login_required
|
||||
def unsubscribe(actor):
|
||||
community = actor_to_community(actor)
|
||||
|
||||
|
@ -162,6 +164,7 @@ def unsubscribe(actor):
|
|||
|
||||
|
||||
@bp.route('/<actor>/submit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_post(actor):
|
||||
community = actor_to_community(actor)
|
||||
form = CreatePost()
|
||||
|
@ -194,8 +197,11 @@ def add_post(actor):
|
|||
else:
|
||||
raise Exception('invalid post type')
|
||||
db.session.add(post)
|
||||
community.post_count += 1
|
||||
db.session.commit()
|
||||
|
||||
# todo: federate post creation out to followers
|
||||
|
||||
flash('Post has been added')
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
|
@ -204,3 +210,13 @@ def add_post(actor):
|
|||
|
||||
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
images_disabled=images_disabled)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>')
|
||||
def show_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
mods = post.community.moderators()
|
||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||
form = NewReplyForm()
|
||||
return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator,
|
||||
canonical=post.ap_id, form=form)
|
||||
|
|
|
@ -2,13 +2,13 @@ from datetime import datetime
|
|||
|
||||
from app import db
|
||||
from app.main import bp
|
||||
from flask import g, jsonify, flash, request
|
||||
from flask import g, session, flash, request
|
||||
from flask_moment import moment
|
||||
from flask_login import current_user
|
||||
from flask_babel import _, get_locale
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy_searchable import search
|
||||
from app.utils import render_template, get_setting
|
||||
from app.utils import render_template, get_setting, gibberish
|
||||
|
||||
from app.models import Community, CommunityMember
|
||||
|
||||
|
@ -43,9 +43,3 @@ def list_local_communities():
|
|||
def list_subscribed_communities():
|
||||
communities = Community.query.join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all()
|
||||
return render_template('list_communities.html', communities=communities)
|
||||
|
||||
|
||||
|
||||
@bp.before_app_request
|
||||
def before_request():
|
||||
g.locale = str(get_locale())
|
|
@ -255,8 +255,8 @@ class Post(db.Model):
|
|||
nsfl = db.Column(db.Boolean, default=False)
|
||||
sticky = db.Column(db.Boolean, default=False)
|
||||
indexable = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) # this is when the content arrived here
|
||||
posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) # this is when the original server created it
|
||||
last_active = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
ip = db.Column(db.String(50))
|
||||
up_votes = db.Column(db.Integer, default=0)
|
||||
|
@ -435,6 +435,23 @@ class ActivityPubLog(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class Filter(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(50))
|
||||
filter_posts = db.Column(db.Boolean, default=True)
|
||||
filter_replies = db.Column(db.Boolean, default=False)
|
||||
hide_type = db.Column(db.Integer, default=0) # 0 = hide with warning, 1 = hide completely
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
|
||||
class FilterKeyword(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
keyword = db.Column(db.String(100))
|
||||
filter_id = db.Column(db.Integer, db.ForeignKey('filter.id'))
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
|
19
app/static/images/external_link_black.svg
Normal file
19
app/static/images/external_link_black.svg
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="6.3531513mm" height="6.3394589mm" viewBox="0 0 6.3531516 6.3394588" version="1.1" id="svg8" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="external_link_web.svg">
|
||||
<defs id="defs2"/>
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="3.959798" inkscape:cx="33.546238" inkscape:cy="62.365132" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="1872" inkscape:window-height="1039" inkscape:window-x="48" inkscape:window-y="0" inkscape:window-maximized="1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
|
||||
<metadata id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
|
||||
<path style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.4720007,1.0436168 -1.65871069,0.0019 c -0.2368211,-0.01848 -0.4529512,-0.0047 -0.4359367,0.373061 l -0.039823,4.025901 c -0.00912,0.444233 0.1987852,0.590785 0.5416462,0.552243 l 3.81418619,0.0014 c 0.425508,0.02448 0.594604,-0.164973 0.588101,-0.500924 l -0.0065,-1.611746" id="path24" sodipodi:nodetypes="cccccccc"/>
|
||||
<path style="fill:none;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 3.8509437,0.33937181 1.910781,-0.0021 c 0.171289,0.01557 0.273418,0.09464 0.252261,0.286884 l 0.0011,1.87940799" id="path851" sodipodi:nodetypes="cccc"/>
|
||||
<path style="fill:none;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.3884557,3.9829408 3.471423,-3.51892699 v 0" id="path853"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -2,3 +2,6 @@ $primary-colour: #0071CE;
|
|||
$primary-colour-hover: #0A5CA0;
|
||||
$green: #00b550;
|
||||
$green-hover: #83f188;
|
||||
$light-grey: #ddd;
|
||||
$grey: #bbb;
|
||||
$dark-grey: #777;
|
|
@ -277,4 +277,17 @@ fieldset legend {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.post_list .post_teaser {
|
||||
border-bottom: solid 2px #ddd;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.post_list .post_teaser h3 {
|
||||
font-size: 120%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.post_list .post_teaser h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=structure.css.map */
|
||||
|
|
|
@ -64,3 +64,21 @@ nav, etc which are used site-wide */
|
|||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.post_list {
|
||||
.post_teaser {
|
||||
|
||||
h3 {
|
||||
font-size: 120%;
|
||||
margin-top: 8px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: solid 2px $light-grey;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -288,4 +288,11 @@ nav.navbar {
|
|||
left: 26px;
|
||||
}
|
||||
|
||||
.external_link_icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=styles.css.map */
|
||||
|
|
|
@ -83,4 +83,11 @@ nav.navbar {
|
|||
top: 104px;
|
||||
left: 26px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.external_link_icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-config" content="/static/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% if canonical %}
|
||||
<link rel="canonical" href="{{ canonical }}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex flex-column" style="padding-top: 43px;">
|
||||
|
@ -77,8 +80,8 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ moment.include_moment() }}
|
||||
{{ moment.lang(g.locale) }}
|
||||
{{ str(moment.include_moment()).replace('<script>', '<script nonce="' + session['nonce'] + '">')|safe }}
|
||||
{{ str(moment.lang(g.locale)).replace('<script>', '<script nonce="' + session['nonce'] + '">')|safe }}
|
||||
{% endblock %}
|
||||
{{ bootstrap.load_js() }}
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
|
||||
|
|
|
@ -20,7 +20,43 @@
|
|||
{% else %}
|
||||
<h1 class="mt-3">{{ community.title }}</h1>
|
||||
{% endif %}
|
||||
<div class="post_list">
|
||||
{% for post in posts %}
|
||||
<div class="post_teaser">
|
||||
<div class="row">
|
||||
<div class="col">{{ post.author.user_name }} · {{ moment(post.posted_at).fromNow() }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col{% if post.image_id %}-8{% endif %}">
|
||||
<h3>
|
||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}</a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
{% if post.image_id %}
|
||||
<div class="col-4">
|
||||
<img src="{{ post.image.source_url}}" alt="{{ post.image.alt_text }}"
|
||||
width="100" />
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
up {{ post.score }} down
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">{{post.reply_count}}</a> additional tools
|
||||
</div>
|
||||
<div class="col-2">...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ _('No posts in this community yet.') }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
|
@ -28,7 +64,7 @@
|
|||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.subscribed(community) %}
|
||||
{% if current_user.is_authenticated and current_user.subscribed(community) %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
|
@ -39,7 +75,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="Search this community" />
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
120
app/templates/community/post.html
Normal file
120
app/templates/community/post.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-8 position-relative">
|
||||
<div class="row">
|
||||
{% if post.image_id %}
|
||||
<div class="col-8">
|
||||
<h1 class="mt-3">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if post.url %}
|
||||
<a href="post.url" rel="nofollow ugc"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% else %}
|
||||
<a href="post.image.source_url"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h1 class="mt-3">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}
|
||||
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" /></a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post.body_html %}
|
||||
<div class="row post_full">
|
||||
<div class="col">
|
||||
{{ post.body_html|safe }}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if post.comments_enabled %}
|
||||
<div class="row post_reply_form">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
<hr class="mt-4" />
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ _('Comments are disabled for this post.') }}</p>
|
||||
{% endif %}
|
||||
<div class="row post_replies">
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create a post') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('About community') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description|safe }}</p>
|
||||
<p>{{ post.community.rules|safe }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ol>
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
10
app/utils.py
10
app/utils.py
|
@ -143,3 +143,13 @@ def domain_from_url(url: str) -> Domain:
|
|||
domain = Domain.query.filter_by(name=parsed_url.hostname.lower()).first()
|
||||
return domain
|
||||
|
||||
|
||||
def shorten_string(input_str, max_length=50):
|
||||
if len(input_str) <= max_length:
|
||||
return input_str
|
||||
else:
|
||||
return input_str[:max_length - 3] + '…'
|
||||
|
||||
|
||||
def shorten_url(input: str, max_length=20):
|
||||
return shorten_string(input.replace('https://', '').replace('http://', ''))
|
||||
|
|
47
migrations/versions/b1d3fc38b1f7_filter_keywords.py
Normal file
47
migrations/versions/b1d3fc38b1f7_filter_keywords.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""filter keywords
|
||||
|
||||
Revision ID: b1d3fc38b1f7
|
||||
Revises: f8200275644a
|
||||
Create Date: 2023-09-20 19:35:04.332862
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b1d3fc38b1f7'
|
||||
down_revision = 'f8200275644a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('filter',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=50), nullable=True),
|
||||
sa.Column('filter_posts', sa.Boolean(), nullable=True),
|
||||
sa.Column('filter_replies', sa.Boolean(), nullable=True),
|
||||
sa.Column('hide_type', sa.Integer(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('filter_keyword',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('keyword', sa.String(length=100), nullable=True),
|
||||
sa.Column('filter_id', sa.Integer(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['filter_id'], ['filter.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('filter_keyword')
|
||||
op.drop_table('filter')
|
||||
# ### end Alembic commands ###
|
29
pyfedi.py
29
pyfedi.py
|
@ -1,11 +1,12 @@
|
|||
# This file is part of pyfedi, which is licensed under the GNU General Public License (GPL) version 3.0.
|
||||
# You should have received a copy of the GPL along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask_babel import get_locale
|
||||
|
||||
from app import create_app, db, cli
|
||||
import os, click
|
||||
|
||||
from app.utils import getmtime
|
||||
from flask import session, g
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url
|
||||
|
||||
app = create_app()
|
||||
cli.register(app)
|
||||
|
@ -15,7 +16,7 @@ cli.register(app)
|
|||
def app_context_processor(): # NB there needs to be an identical function in cb.wsgi to make this work in production
|
||||
def getmtime(filename):
|
||||
return os.path.getmtime('app/static/' + filename)
|
||||
return dict(getmtime=getmtime)
|
||||
return dict(getmtime=getmtime, post_type_link=POST_TYPE_LINK, post_type_image=POST_TYPE_IMAGE, post_type_article=POST_TYPE_ARTICLE)
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
|
@ -25,4 +26,22 @@ def make_shell_context():
|
|||
|
||||
with app.app_context():
|
||||
app.jinja_env.globals['getmtime'] = getmtime
|
||||
app.jinja_env.globals['len'] = len
|
||||
app.jinja_env.globals['len'] = len
|
||||
app.jinja_env.globals['str'] = str
|
||||
app.jinja_env.filters['shorten'] = shorten_string
|
||||
app.jinja_env.filters['shorten_url'] = shorten_url
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
session['nonce'] = gibberish()
|
||||
g.locale = str(get_locale())
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers['Content-Security-Policy'] = f"script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net 'nonce-{session['nonce']}'"
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains; preload'
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
return response
|
||||
|
|
Loading…
Reference in a new issue