mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
organise communities under topics
This commit is contained in:
parent
04a4abe9d5
commit
241fe8ec38
18 changed files with 383 additions and 29 deletions
|
@ -74,6 +74,9 @@ def create_app(config_class=Config):
|
||||||
from app.domain import bp as domain_bp
|
from app.domain import bp as domain_bp
|
||||||
app.register_blueprint(domain_bp)
|
app.register_blueprint(domain_bp)
|
||||||
|
|
||||||
|
from app.topic import bp as topic_bp
|
||||||
|
app.register_blueprint(topic_bp)
|
||||||
|
|
||||||
def get_resource_as_string(name, charset='utf-8'):
|
def get_resource_as_string(name, charset='utf-8'):
|
||||||
with app.open_resource(name) as f:
|
with app.open_resource(name) as f:
|
||||||
return f.read().decode(charset)
|
return f.read().decode(charset)
|
||||||
|
|
|
@ -57,7 +57,10 @@ def login():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
if not next_page or url_parse(next_page).netloc != '':
|
if not next_page or url_parse(next_page).netloc != '':
|
||||||
next_page = url_for('main.index')
|
if len(current_user.communities()) == 0:
|
||||||
|
next_page = url_for('topic.choose_topics')
|
||||||
|
else:
|
||||||
|
next_page = url_for('main.index')
|
||||||
response = make_response(redirect(next_page))
|
response = make_response(redirect(next_page))
|
||||||
if form.low_bandwidth_mode.data:
|
if form.low_bandwidth_mode.data:
|
||||||
response.set_cookie('low_bandwidth', '1', expires=datetime(year=2099, month=12, day=30))
|
response.set_cookie('low_bandwidth', '1', expires=datetime(year=2099, month=12, day=30))
|
||||||
|
@ -108,9 +111,9 @@ def register():
|
||||||
if current_app.config['MODE'] == 'development':
|
if current_app.config['MODE'] == 'development':
|
||||||
current_app.logger.info('Verify account:' + url_for('auth.verify_email', token=user.verification_token, _external=True))
|
current_app.logger.info('Verify account:' + url_for('auth.verify_email', token=user.verification_token, _external=True))
|
||||||
|
|
||||||
flash(_('Great, you are now a registered user! Choose some communities to join. Use the topic filter to narrow things down.'))
|
flash(_('Great, you are now a registered user!'))
|
||||||
|
|
||||||
resp = make_response(redirect(url_for('main.list_communities')))
|
resp = make_response(redirect(url_for('topic.choose_topics')))
|
||||||
if user_ip_banned():
|
if user_ip_banned():
|
||||||
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||||
return resp
|
return resp
|
||||||
|
@ -176,7 +179,10 @@ def verify_email(token):
|
||||||
flash(_('Thank you for verifying your email address.'))
|
flash(_('Thank you for verifying your email address.'))
|
||||||
else:
|
else:
|
||||||
flash(_('Email address validation failed.'), 'error')
|
flash(_('Email address validation failed.'), 'error')
|
||||||
return redirect(url_for('main.index'))
|
if len(user.communities()) == 0:
|
||||||
|
return redirect(url_for('topic.choose_topics'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/validation_required')
|
@bp.route('/validation_required')
|
||||||
|
|
|
@ -315,7 +315,7 @@ def unsubscribe(actor):
|
||||||
activity.result = 'success'
|
activity.result = 'success'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
if not success:
|
if not success:
|
||||||
flash('There was a problem while trying to join', 'error')
|
flash('There was a problem while trying to unsubscribe', 'error')
|
||||||
|
|
||||||
if proceed:
|
if proceed:
|
||||||
db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete()
|
db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete()
|
||||||
|
|
|
@ -121,6 +121,7 @@ class File(db.Model):
|
||||||
|
|
||||||
class Topic(db.Model):
|
class Topic(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
machine_name = db.Column(db.String(50), index=True)
|
||||||
name = db.Column(db.String(50))
|
name = db.Column(db.String(50))
|
||||||
num_communities = db.Column(db.Integer, default=0)
|
num_communities = db.Column(db.Integer, default=0)
|
||||||
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
|
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
|
||||||
|
@ -540,7 +541,7 @@ class User(UserMixin, db.Model):
|
||||||
|
|
||||||
def communities(self) -> List[Community]:
|
def communities(self) -> List[Community]:
|
||||||
return Community.query.filter(Community.banned == False).\
|
return Community.query.filter(Community.banned == False).\
|
||||||
join(CommunityMember).filter(CommunityMember.is_banned == False).all()
|
join(CommunityMember).filter(CommunityMember.is_banned == False, CommunityMember.user_id == self.id).all()
|
||||||
|
|
||||||
def profile_id(self):
|
def profile_id(self):
|
||||||
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
||||||
|
|
|
@ -478,6 +478,16 @@ fieldset legend {
|
||||||
content: ">";
|
content: ">";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.communities_table tbody tr th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.communities_table tbody tr th a {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.community_header {
|
.community_header {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
|
@ -528,6 +538,25 @@ fieldset legend {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#choose_topics_card label.form-control-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#choose_topics_card .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
#choose_topics_card ul.form-control {
|
||||||
|
border: none;
|
||||||
|
list-style-type: none;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
#choose_topics_card ul.form-control li {
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
#choose_topics_card ul.form-control li label {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-check .form-check-input {
|
.form-check .form-check-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
|
|
@ -82,6 +82,18 @@ nav, etc which are used site-wide */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.communities_table {
|
||||||
|
tbody tr th {
|
||||||
|
padding: 0;
|
||||||
|
a {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.community_header {
|
.community_header {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
|
@ -133,6 +145,27 @@ nav, etc which are used site-wide */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#choose_topics_card {
|
||||||
|
label.form-control-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
ul.form-control {
|
||||||
|
border: none;
|
||||||
|
list-style-type: none;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
li {
|
||||||
|
vertical-align: center;
|
||||||
|
label {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-check .form-check-input {
|
.form-check .form-check-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
|
|
@ -122,10 +122,10 @@
|
||||||
<li><a class="dropdown-item{% if active_child == 'all_posts' %} active{% endif %}" href="/all"><span class="fe fe-all"></span>{{ _('All posts') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'all_posts' %} active{% endif %}" href="/all"><span class="fe fe-all"></span>{{ _('All posts') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<li class="nav-item dropdown{% if active_parent == 'communities' %} active{% endif %}">
|
<li class="nav-item dropdown{% if active_parent == 'communities' %} active{% endif %}">
|
||||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/communities" aria-haspopup="true" aria-expanded="false">{{ _('Communities') }}</a>
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/topics" aria-haspopup="true" aria-expanded="false">{{ _('Topics') }}</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/communities">{{ _('Browse all') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/topics">{{ _('Browse by topic') }}</a></li>
|
||||||
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/topics">{{ _('By topic') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/communities">{{ _('All communities') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/auth/login">{{ _('Log in') }}</a></li>
|
<li class="nav-item"><a class="nav-link" href="/auth/login">{{ _('Log in') }}</a></li>
|
||||||
|
@ -140,10 +140,10 @@
|
||||||
<li><a class="dropdown-item{% if active_child == 'all_posts' %} active{% endif %}" href="/all"><span class="fe fe-all"></span>{{ _('All posts') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'all_posts' %} active{% endif %}" href="/all"><span class="fe fe-all"></span>{{ _('All posts') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<li class="nav-item dropdown{% if active_parent == 'communities' %} active{% endif %}">
|
<li class="nav-item dropdown{% if active_parent == 'communities' %} active{% endif %}">
|
||||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/communities" aria-haspopup="true" aria-expanded="false">{{ _('Communities') }}</a>
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/topics" aria-haspopup="true" aria-expanded="false">{{ _('Topics') }}</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/communities">{{ _('Browse all') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/topics">{{ _('Browse by topic') }}</a></li>
|
||||||
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/topics">{{ _('By topic') }}</a></li>
|
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/communities">{{ _('All communities') }}</a></li>
|
||||||
{% if moderating_communities %}
|
{% if moderating_communities %}
|
||||||
<li><h6 class="dropdown-header">{{ _('Moderating') }}</h6></li>
|
<li><h6 class="dropdown-header">{{ _('Moderating') }}</h6></li>
|
||||||
{% for community in moderating_communities %}
|
{% for community in moderating_communities %}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<div class="mobile_create_post d-md-none mt-1">
|
{% if community %}
|
||||||
<a class="btn btn-primary" href="/community/{{ community.link() }}/submit">{{ _('Create post') }}</a>
|
<div class="mobile_create_post d-md-none mt-1">
|
||||||
</div>
|
<a class="btn btn-primary" href="/community/{{ community.link() }}/submit">{{ _('Create post') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="btn-group mt-1 mb-2">
|
<div class="btn-group mt-1 mb-2">
|
||||||
<a href="?sort=hot&layout={{ post_layout }}" class="btn {{ 'btn-primary' if sort == '' or sort == 'hot' else 'btn-outline-secondary' }}" rel="nofollow">
|
<a href="?sort=hot&layout={{ post_layout }}" class="btn {{ 'btn-primary' if sort == '' or sort == 'hot' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
{{ _('Hot') }}
|
{{ _('Hot') }}
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
|
||||||
{% if community.topic_id %}
|
{% if community.topic_id %}
|
||||||
<li class="breadcrumb-item"><a href="/communities?topic_id={{ community.topic.id }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/topic/{{ community.topic.machine_name }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
|
||||||
{% if community.topic_id %}
|
{% if community.topic_id %}
|
||||||
<li class="breadcrumb-item"><a href="/communities?topic_id={{ community.topic.id }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/topic/{{ community.topic.machine_name }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -50,9 +50,9 @@
|
||||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
|
||||||
{% if community.topic_id %}
|
{% if community.topic_id %}
|
||||||
<li class="breadcrumb-item"><a href="/communities?topic_id={{ community.topic.id }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/topic/{{ community.topic.machine_name }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<table class="communities_table table table-striped table-hover w-100">
|
<table class="communities_table table table-striped table-hover w-100">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for topic in topics %}
|
{% for topic in topics %}
|
||||||
<tr class="">
|
<tr>
|
||||||
<td><a href="/communities?topic_id={{ topic.id }}">{{ topic.name }}</a></td>
|
<th class="pl-2"><a href="/topic/{{ topic.machine_name }}">{{ topic.name }}</a></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -18,4 +18,5 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ _('There are no communities yet.') }}</p>
|
<p>{{ _('There are no communities yet.') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<p><a href="/communities" class="btn btn-primary">{{ _('Explore communities') }}</a></p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
{% if community.topic_id %}
|
||||||
{% if post.community.topic_id %}
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities?topic_id={{ post.community.topic.id }}" rel="nofollow">{{ post.community.topic.name }}</a></li>
|
<li class="breadcrumb-item"><a href="/topic/{{ community.topic.machine_name }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||||
|
@ -51,9 +51,9 @@
|
||||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
{% if community.topic_id %}
|
||||||
{% if post.community.topic_id %}
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/communities?topic_id={{ post.community.topic.id }}" rel="nofollow">{{ post.community.topic.name }}</a></li>
|
<li class="breadcrumb-item"><a href="/topic/{{ community.topic.machine_name }}" rel="nofollow">{{ community.topic.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||||
|
|
16
app/templates/topic/choose_topics.html
Normal file
16
app/templates/topic/choose_topics.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6" id="choose_topics_card">
|
||||||
|
<div class="card-title text-center">{{ _('Please choose at least 3 topics that interest you.') }}</div>
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
77
app/templates/topic/show_topic.html
Normal file
77
app/templates/topic/show_topic.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/topics">{{ _('Topics') }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ topic.name|shorten }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1 class="mt-2">{{ topic.name }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% include "community/_community_nav.html" %}
|
||||||
|
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
|
||||||
|
<div class="post_list_{{ post_layout }}">
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include 'post/_post_teaser_masonry.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts in this topic yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="post_list">
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts in this topic yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4 side_pane" role="complementary">
|
||||||
|
{% if topic_communities %}
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Topic communities') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for community in topic_communities %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="/c/{{ community.link() }}" aria-label="{{ _('Go to community') }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" alt="" />
|
||||||
|
{{ community.display_name() }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% include "_inoculation_links.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
5
app/topic/__init__.py
Normal file
5
app/topic/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('topic', __name__)
|
||||||
|
|
||||||
|
from app.topic import routes
|
14
app/topic/forms.py
Normal file
14
app/topic/forms.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from flask import request
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField
|
||||||
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||||
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
from app.utils import MultiCheckboxField
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseTopicsForm(FlaskForm):
|
||||||
|
chosen_topics = MultiCheckboxField(_l('Choose some topics you are interested in'), coerce=int)
|
||||||
|
submit = SubmitField(_l('Choose'))
|
131
app/topic/routes.py
Normal file
131
app/topic/routes.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from flask import request, flash, json, url_for, current_app, redirect, abort
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from flask_babel import _
|
||||||
|
from sqlalchemy import text, desc
|
||||||
|
|
||||||
|
from app.activitypub.signature import post_request
|
||||||
|
from app.constants import SUBSCRIPTION_NONMEMBER
|
||||||
|
from app.inoculation import inoculation
|
||||||
|
from app.models import Topic, Community, Post, utcnow, CommunityMember, CommunityJoinRequest
|
||||||
|
from app.topic import bp
|
||||||
|
from app import db, celery, cache
|
||||||
|
from app.topic.forms import ChooseTopicsForm
|
||||||
|
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
||||||
|
community_membership
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/topic/<topic_name>', methods=['GET'])
|
||||||
|
def show_topic(topic_name):
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
sort = request.args.get('sort', '' if current_user.is_anonymous else current_user.default_sort)
|
||||||
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
|
post_layout = request.args.get('layout', 'list' if not low_bandwidth else None)
|
||||||
|
|
||||||
|
# translate topic_name from /topic/fediverse to topic_id
|
||||||
|
topic = Topic.query.filter(Topic.machine_name == topic_name.strip().lower()).first()
|
||||||
|
|
||||||
|
if topic:
|
||||||
|
# get posts from communities in that topic
|
||||||
|
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id, Community.banned == False)
|
||||||
|
if sort == '' or sort == 'hot':
|
||||||
|
posts = posts.order_by(desc(Post.ranking))
|
||||||
|
elif sort == 'top':
|
||||||
|
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.score))
|
||||||
|
elif sort == 'new':
|
||||||
|
posts = posts.order_by(desc(Post.posted_at))
|
||||||
|
elif sort == 'active':
|
||||||
|
posts = posts.order_by(desc(Post.last_active))
|
||||||
|
if current_user.is_anonymous or current_user.ignore_bots:
|
||||||
|
posts = posts.filter(Post.from_bot == False)
|
||||||
|
content_filters = {}
|
||||||
|
else:
|
||||||
|
content_filters = user_filters_posts(current_user.id)
|
||||||
|
per_page = 100
|
||||||
|
if post_layout == 'masonry':
|
||||||
|
per_page = 200
|
||||||
|
elif post_layout == 'masonry_wide':
|
||||||
|
per_page = 300
|
||||||
|
posts = posts.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
topic_communities = Community.query.filter(Community.topic_id == topic.id).order_by(Community.name)
|
||||||
|
|
||||||
|
next_url = url_for('topic.show_topic',
|
||||||
|
topic_name=topic_name,
|
||||||
|
page=posts.next_num, sort=sort, layout=post_layout) if posts.has_next else None
|
||||||
|
prev_url = url_for('topic.show_topic',
|
||||||
|
topic_name=topic_name,
|
||||||
|
page=posts.prev_num, sort=sort, layout=post_layout) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('topic/show_topic.html', title=_(topic.name), posts=posts, topic=topic, sort=sort,
|
||||||
|
page=page, post_layout=post_layout, next_url=next_url, prev_url=prev_url,
|
||||||
|
topic_communities=topic_communities, content_filters=content_filters,
|
||||||
|
show_post_community=True, moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/choose_topics', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def choose_topics():
|
||||||
|
form = ChooseTopicsForm()
|
||||||
|
form.chosen_topics.choices = topics_for_form()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if form.chosen_topics.data:
|
||||||
|
for topic_id in form.chosen_topics.data:
|
||||||
|
join_topic(topic_id)
|
||||||
|
flash(_('You have joined some communities relating to those interests. Find them on the Topics menu or browse the home page.'))
|
||||||
|
cache.delete_memoized(joined_communities, current_user.id)
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
else:
|
||||||
|
flash(_('You did not choose any topics. Would you like to choose individual communities instead?'))
|
||||||
|
return redirect(url_for('main.list_communities'))
|
||||||
|
else:
|
||||||
|
return render_template('topic/choose_topics.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
def topics_for_form():
|
||||||
|
topics = Topic.query.order_by(Topic.name).all()
|
||||||
|
result = []
|
||||||
|
for topic in topics:
|
||||||
|
result.append((topic.id, topic.name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def join_topic(topic_id):
|
||||||
|
communities = Community.query.filter_by(topic_id=topic_id, banned=False).all()
|
||||||
|
for community in communities:
|
||||||
|
if not community.user_is_banned(current_user) and community_membership(current_user, community) == SUBSCRIPTION_NONMEMBER:
|
||||||
|
if not community.is_local():
|
||||||
|
join_request = CommunityJoinRequest(user_id=current_user.id, community_id=community.id)
|
||||||
|
db.session.add(join_request)
|
||||||
|
db.session.commit()
|
||||||
|
if current_app.debug:
|
||||||
|
send_community_follow(community.id, join_request)
|
||||||
|
else:
|
||||||
|
send_community_follow.delay(community.id, join_request.id)
|
||||||
|
|
||||||
|
member = CommunityMember(user_id=current_user.id, community_id=community.id)
|
||||||
|
db.session.add(member)
|
||||||
|
db.session.commit()
|
||||||
|
cache.delete_memoized(community_membership, current_user, community)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task
|
||||||
|
def send_community_follow(community_id, join_request_id):
|
||||||
|
with current_app.app_context():
|
||||||
|
community = Community.query.get(community_id)
|
||||||
|
follow = {
|
||||||
|
"actor": f"https://{current_app.config['SERVER_NAME']}/u/{current_user.user_name}",
|
||||||
|
"to": [community.ap_profile_id],
|
||||||
|
"object": community.ap_profile_id,
|
||||||
|
"type": "Follow",
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{join_request_id}"
|
||||||
|
}
|
||||||
|
success = post_request(community.ap_inbox_url, follow, current_user.private_key,
|
||||||
|
current_user.profile_id() + '#main-key')
|
|
@ -30,3 +30,5 @@ class Config(object):
|
||||||
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL') or 'redis://localhost:6379/0'
|
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL') or 'redis://localhost:6379/0'
|
||||||
RESULT_BACKEND = os.environ.get('RESULT_BACKEND') or 'redis://localhost:6379/0'
|
RESULT_BACKEND = os.environ.get('RESULT_BACKEND') or 'redis://localhost:6379/0'
|
||||||
SQLALCHEMY_ECHO = False # set to true to see SQL in console
|
SQLALCHEMY_ECHO = False # set to true to see SQL in console
|
||||||
|
WTF_CSRF_TIME_LIMIT = None # a value of None ensures csrf token is valid for the lifetime of the session
|
||||||
|
|
||||||
|
|
34
migrations/versions/52e8d73b69ba_topic_machine_name.py
Normal file
34
migrations/versions/52e8d73b69ba_topic_machine_name.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"""topic machine name
|
||||||
|
|
||||||
|
Revision ID: 52e8d73b69ba
|
||||||
|
Revises: 86b6fd708bd0
|
||||||
|
Create Date: 2024-01-27 20:40:15.535403
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '52e8d73b69ba'
|
||||||
|
down_revision = '86b6fd708bd0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('topic', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('machine_name', sa.String(length=50), nullable=True))
|
||||||
|
batch_op.create_index(batch_op.f('ix_topic_machine_name'), ['machine_name'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('topic', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_topic_machine_name'))
|
||||||
|
batch_op.drop_column('machine_name')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in a new issue