improve registration and add community topics/categories

This commit is contained in:
rimu 2024-01-04 16:00:19 +13:00
parent 276a937799
commit 781694e023
13 changed files with 247 additions and 11 deletions

View file

@ -67,6 +67,7 @@ class EditCommunityForm(FlaskForm):
(3650, _l('10 years')),
]
content_retention = SelectField(_l('Retain content'), choices=options, default=1, coerce=int)
topic = SelectField(_l('Topic'), coerce=int, validators=[Optional()])
submit = SubmitField(_l('Save'))
def validate(self, extra_validators=None):
@ -82,6 +83,11 @@ class EditCommunityForm(FlaskForm):
return True
class EditTopicForm(FlaskForm):
name = StringField(_l('Name'), validators=[DataRequired()])
submit = SubmitField(_l('Save'))
class EditUserForm(FlaskForm):
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)])

View file

@ -10,11 +10,12 @@ from app import db, celery
from app.activitypub.routes import process_inbox_request, process_delete_request
from app.activitypub.signature import post_request
from app.activitypub.util import default_context
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
EditTopicForm
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community
from app.community.util import save_icon_file, save_banner_file
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
User, Instance, File, Report
User, Instance, File, Report, Topic
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html
from app.admin import bp
@ -182,12 +183,21 @@ def admin_communities():
communities=communities)
def topics_for_form():
topics = Topic.query.order_by(Topic.name).all()
result = [(0, _('None'))]
for topic in topics:
result.append((topic.id, topic.name))
return result
@bp.route('/community/<int:community_id>/edit', methods=['GET', 'POST'])
@login_required
@permission_required('administer all communities')
def admin_community_edit(community_id):
form = EditCommunityForm()
community = Community.query.get_or_404(community_id)
form.topic.choices = topics_for_form()
if form.validate_on_submit():
community.name = form.url.data
community.title = form.title.data
@ -202,6 +212,7 @@ def admin_community_edit(community_id):
community.show_all = form.show_all.data
community.low_quality = form.low_quality.data
community.content_retention = form.content_retention.data
community.topic_id = form.topic.data if form.topic.data != 0 else None
icon_file = request.files['icon_file']
if icon_file and icon_file.filename != '':
if community.icon_id:
@ -216,6 +227,7 @@ def admin_community_edit(community_id):
file = save_banner_file(banner_file)
if file:
community.image = file
db.session.commit()
flash(_('Saved'))
return redirect(url_for('admin.admin_communities'))
@ -235,6 +247,7 @@ def admin_community_edit(community_id):
form.show_all.data = community.show_all
form.low_quality.data = community.low_quality
form.content_retention.data = community.content_retention
form.topic.data = community.topic_id if community.topic_id else None
return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community)
@ -278,6 +291,61 @@ def unsubscribe_everyone_then_delete_task(community_id):
db.session.delete(community) # todo: when a remote community is deleted it will be able to be re-created by using the 'Add remote' function. Not ideal. Consider soft-delete.
@bp.route('/topics', methods=['GET'])
@login_required
@permission_required('administer all communities')
def admin_topics():
topics = Topic.query.order_by(Topic.name).all()
return render_template('admin/topics.html', title=_('Topics'), topics=topics)
@bp.route('/topic/add', methods=['GET', 'POST'])
@login_required
@permission_required('administer all communities')
def admin_topic_add():
form = EditTopicForm()
if form.validate_on_submit():
topic = Topic(name=form.name.data, num_communities=0)
db.session.add(topic)
db.session.commit()
flash(_('Saved'))
return redirect(url_for('admin.admin_topics'))
return render_template('admin/edit_topic.html', title=_('Add topic'), form=form)
@bp.route('/topic/<int:topic_id>/edit', methods=['GET', 'POST'])
@login_required
@permission_required('administer all communities')
def admin_topic_edit(topic_id):
form = EditTopicForm()
topic = Topic.query.get_or_404(topic_id)
if form.validate_on_submit():
topic.name = form.name.data
topic.num_communities = topic.communities.count()
db.session.commit()
flash(_('Saved'))
return redirect(url_for('admin.admin_topics'))
else:
form.name.data = topic.name
return render_template('admin/edit_topic.html', title=_('Edit topic'), form=form, topic=topic)
@bp.route('/topic/<int:topic_id>/delete', methods=['GET'])
@login_required
@permission_required('administer all communities')
def admin_topic_delete(topic_id):
topic = Topic.query.get_or_404(topic_id)
topic.num_communities = topic.communities.count()
if topic.num_communities == 0:
db.session.delete(topic)
flash(_('Topic deleted'))
else:
flash(_('Cannot delete topic with communities assigned to it.', 'error'))
db.session.commit()
return redirect(url_for('admin.admin_topics'))
@bp.route('/users', methods=['GET'])
@login_required
@permission_required('administer all users')

View file

@ -92,7 +92,7 @@ def register():
verification_token = random_token(16)
form.user_name.data = form.user_name.data.strip()
user = User(user_name=form.user_name.data, title=form.user_name.data, email=form.real_email.data,
verification_token=verification_token, instance=1, ip_address=ip_address(),
verification_token=verification_token, instance_id=1, ip_address=ip_address(),
banned=user_ip_banned() or user_cookie_banned())
user.set_password(form.password.data)
db.session.add(user)
@ -104,9 +104,9 @@ def register():
if current_app.config['MODE'] == 'development':
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!'))
flash(_('Great, you are now a registered user! Choose some communities to subscribe to. Use the topic filter to narrow things down.'))
resp = make_response(redirect(url_for('main.index')))
resp = make_response(redirect(url_for('main.list_communities')))
if user_ip_banned():
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
return resp

View file

@ -15,7 +15,7 @@ from sqlalchemy import select, desc
from sqlalchemy_searchable import search
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
ap_datetime, ip_address, retrieve_block_list
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic
@bp.route('/', methods=['HEAD', 'GET', 'POST'])
@ -133,15 +133,23 @@ def top_posts():
def list_communities():
verification_warning()
search_param = request.args.get('search', '')
topic_id = int(request.args.get('topic_id', 0))
topics = Topic.query.order_by(Topic.name).all()
if search_param == '':
communities = Community.query.filter_by(banned=False).all()
pass
else:
query = search(select(Community), search_param, sort=True) # todo: exclude banned communities from search
communities = db.session.scalars(query).all()
flash('Sorry, no search function yet. Use the topic filter for now.', 'warning')
communities = Community.query.filter_by(banned=False).all()
#query = search(select(Community), search_param, sort=True) # todo: exclude banned communities from search
#communities = db.session.scalars(query).all()
return render_template('list_communities.html', communities=communities, search=search_param, title=_('Communities'),
communities = Community.query.filter_by(banned=False)
if topic_id != 0:
communities = communities.filter_by(topic_id=topic_id)
return render_template('list_communities.html', communities=communities.all(), search=search_param, title=_('Communities'),
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER)
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, topics=topics, topic_id=topic_id)
@bp.route('/communities/local', methods=['GET'])

View file

@ -113,6 +113,13 @@ class File(db.Model):
os.unlink(self.thumbnail_path)
class Topic(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
num_communities = db.Column(db.Integer, default=0)
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
class Community(db.Model):
query_class = FullTextSearchQuery
id = db.Column(db.Integer, primary_key=True)
@ -138,6 +145,7 @@ class Community(db.Model):
public_key = db.Column(db.Text)
private_key = db.Column(db.Text)
content_retention = db.Column(db.Integer, default=-1)
topic_id = db.Column(db.Integer, db.ForeignKey('topic.id'), index=True)
ap_id = db.Column(db.String(255), index=True)
ap_profile_id = db.Column(db.String(255), index=True)

View file

@ -3,6 +3,7 @@ document.addEventListener("DOMContentLoaded", function () {
setupCommunityNameInput();
setupShowMoreLinks();
setupConfirmFirst();
setupSubmitOnInputChange();
setupTimeTracking();
setupMobileNav();
});
@ -88,6 +89,32 @@ function setupConfirmFirst() {
});
})
}
function setupSubmitOnInputChange() {
const inputElements = document.querySelectorAll('.submit_on_change');
inputElements.forEach(element => {
element.addEventListener("change", function() {
const form = findParentForm(element);
if (form) {
form.submit();
}
});
});
}
// Find the parent form of an element
function findParentForm(element) {
let currentElement = element;
while (currentElement) {
if (currentElement.tagName === 'FORM') {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
}
function setupShowMoreLinks() {
const comments = document.querySelectorAll('.comment');

View file

@ -3,6 +3,7 @@
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
<a href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a> |
<a href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a> |
<a href="{{ url_for('admin.admin_topics') }}">{{ _('Topics') }}</a> |
<a href="{{ url_for('admin.admin_users') }}">{{ _('Users') }}</a> |
<a href="{{ url_for('admin.admin_reports') }}">{{ _('Moderation') }}</a> |
<a href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a> |

View file

@ -17,6 +17,7 @@
<tr>
<th>Name</th>
<th>Title</th>
<th>Topic</th>
<th># Posts</th>
<th>Home</th>
<th>Popular</th>
@ -28,6 +29,7 @@
<td>{{ community.name }}</td>
<td><img src="{{ community.icon_image('tiny') }}" class="community_icon rounded-circle" loading="lazy" />
{{ community.display_name() }}</td>
<td>{{ community.topic.name }}</td>
<td>{{ community.post_count }}</td>
<th>{{ '&check;'|safe if community.show_home else '&cross;'|safe }}</th>
<th>{{ '&check;'|safe if community.show_popular else '&cross;'|safe }}</th>

View file

@ -45,6 +45,7 @@
{{ render_field(form.show_all) }}
{{ render_field(form.low_quality) }}
{{ render_field(form.content_retention) }}
{{ render_field(form.topic) }}
{% if not community.is_local() %}
</fieldset>
{% endif %}

View file

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<div class="row">
<div class="col">
{% include 'admin/_nav.html' %}
</div>
</div>
<div class="row">
<div class="col col-login mx-auto">
{% if topic %}
<h3>{{ _('Edit %(topic_name)s', topic_name=topic.name) }}</h3>
{% endif %}
{{ render_form(form) }}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<div class="row">
<div class="col">
{% include 'admin/_nav.html' %}
</div>
</div>
<div class="row">
<div class="col">
<p><a href="{{ url_for('admin.admin_topic_add') }}" class="btn btn-primary">{{ _('Add topic') }}</a></p>
<table class="table table-striped">
<tr>
<th>Name</th>
<th># Communities</th>
<th>Actions</th>
</tr>
{% for topic in topics %}
<tr>
<td>{{ topic.name }}</td>
<td>{{ topic.num_communities }}</td>
<td><a href="{{ url_for('admin.admin_topic_edit', topic_id=topic.id) }}">Edit</a> |
<a href="{{ url_for('admin.admin_topic_delete', topic_id=topic.id) }}" class="confirm_first">Delete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View file

@ -14,7 +14,20 @@
<a href="/communities/subscribed" class="btn {{ 'btn-primary' if request.path == '/communities/subscribed' else 'btn-outline-secondary' }}">
{{ _('Subscribed') }}
</a>
{% if topics %}
<span class="mt-1 pl-4">
<form method="get">Topic:
<select name="topic_id" class="form-control-sm submit_on_change">
<option value="0">All</option>
{% for topic in topics %}
<option value="{{ topic.id }}" {{ 'selected' if topic.id == topic_id }}>{{ topic.name }}</option>
{% endfor %}
</select>
</form>
</span>
{% endif %}
</div>
</div>
<div class="col-auto">
<div class="btn-group">

View file

@ -0,0 +1,51 @@
"""community topics
Revision ID: fd5d3a9cb584
Revises: 328c9990e53e
Create Date: 2024-01-04 15:13:29.499990
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fd5d3a9cb584'
down_revision = '328c9990e53e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('topic',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=True),
sa.Column('num_communities', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.add_column(sa.Column('topic_id', sa.Integer(), nullable=True))
batch_op.create_index(batch_op.f('ix_community_topic_id'), ['topic_id'], unique=False)
batch_op.create_foreign_key(None, 'topic', ['topic_id'], ['id'])
with op.batch_alter_table('instance', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_instance_dormant'), ['dormant'], unique=False)
batch_op.create_index(batch_op.f('ix_instance_gone_forever'), ['gone_forever'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('instance', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_instance_gone_forever'))
batch_op.drop_index(batch_op.f('ix_instance_dormant'))
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_community_topic_id'))
batch_op.drop_column('topic_id')
op.drop_table('topic')
# ### end Alembic commands ###