add most mod routes
This commit is contained in:
@@ -231,6 +231,7 @@ def create_app():
|
||||
|
||||
@app.before_request
|
||||
def make_session_permanent():
|
||||
if is_logged_in():
|
||||
session.permanent = True
|
||||
|
||||
commit = ''
|
||||
|
||||
12
app/auth.py
12
app/auth.py
@@ -1,4 +1,4 @@
|
||||
from flask import session, flash, redirect, url_for
|
||||
from flask import session, flash, redirect, url_for, abort
|
||||
from .models import Sessions, Users
|
||||
from argon2 import PasswordHasher
|
||||
from functools import wraps
|
||||
@@ -51,3 +51,13 @@ def login_required(view_func):
|
||||
return redirect(url_for('users.log_in_page'))
|
||||
return view_func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def mod_only(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not is_logged_in():
|
||||
abort(403)
|
||||
if not get_active_user().is_mod():
|
||||
abort(403)
|
||||
return view_func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@@ -400,7 +400,7 @@ def tag_image(children, attr):
|
||||
|
||||
def tag_quote(children, attr):
|
||||
if attr:
|
||||
quotee = attr.strip()
|
||||
quotee = f'Quoting: {attr.strip()}'
|
||||
else:
|
||||
quotee = 'Quote'
|
||||
|
||||
|
||||
@@ -123,6 +123,22 @@ class Topics(Model):
|
||||
GROUP BY topics.id ORDER BY topics.sort_order ASC"""
|
||||
return db.query(q)
|
||||
|
||||
@classmethod
|
||||
def new(_cls, name: str, description: str) -> Topics:
|
||||
from slugify import slugify
|
||||
name = name.strip()
|
||||
description = description.strip()
|
||||
now = int(time.time())
|
||||
slug = f'{slugify(name)}-{now}'
|
||||
|
||||
topic_count = Topics.count()
|
||||
return Topics.create({
|
||||
'name': name,
|
||||
'description': description,
|
||||
'slug': slug,
|
||||
'sort_order': topic_count + 1,
|
||||
})
|
||||
|
||||
def get_threads(self, per_page, page, sort_by = 'activity'):
|
||||
order_clause = ''
|
||||
if sort_by == 'thread':
|
||||
@@ -202,6 +218,9 @@ class Topics(Model):
|
||||
|
||||
return db.query(q, self.id)
|
||||
|
||||
def locked(self):
|
||||
return bool(self.is_locked)
|
||||
|
||||
|
||||
class Threads(Model):
|
||||
table = 'threads'
|
||||
|
||||
@@ -1,27 +1,81 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from flask import Blueprint, abort, redirect, url_for, request, render_template
|
||||
from ..auth import is_logged_in, get_active_user
|
||||
from ..models import Topics, Threads
|
||||
bp = Blueprint('mod', __name__, url_prefix='/mod/')
|
||||
|
||||
@bp.before_request
|
||||
def mod_only():
|
||||
if not is_logged_in():
|
||||
abort(403)
|
||||
if not get_active_user().is_mod():
|
||||
abort(403)
|
||||
|
||||
@bp.get('/')
|
||||
def index():
|
||||
return 'stub'
|
||||
|
||||
@bp.get('/topics/new')
|
||||
def new_topic():
|
||||
return 'stub'
|
||||
return render_template('mod/new_topic.html')
|
||||
|
||||
@bp.post('/topics/new')
|
||||
def new_topic_post():
|
||||
topic = Topics.new(request.form.get('name'), request.form.get('description'))
|
||||
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||
|
||||
@bp.get('/topics/sort')
|
||||
def sort_topics():
|
||||
return 'stub'
|
||||
|
||||
@bp.get('/topics/<int:topic_id>/edit')
|
||||
def edit_topic(topic_id):
|
||||
topic = Topics.find({'id': topic_id})
|
||||
if not topic:
|
||||
abort(404)
|
||||
return render_template('mod/edit_topic.html', topic=topic)
|
||||
|
||||
@bp.post('/topics/<int:topic_id>/edit')
|
||||
def edit_topic_post(topic_id):
|
||||
topic = Topics.find({'id': topic_id})
|
||||
if not topic:
|
||||
abort(404)
|
||||
topic.update({
|
||||
'name': request.form.get('name').strip(),
|
||||
'description': request.form.get('description').strip(),
|
||||
})
|
||||
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||
|
||||
@bp.post('/topics/<int:topic_id>/lock')
|
||||
def lock_topic(topic_id):
|
||||
topic = Topics.find({'id': topic_id})
|
||||
if not topic:
|
||||
abort(404)
|
||||
topic.update({'is_locked': request.form.get('lock', default=0)})
|
||||
return redirect(url_for('topics.topic', slug=topic.slug))
|
||||
|
||||
@bp.post('/threads/<int:thread_id>/move')
|
||||
def move_thread(thread_id):
|
||||
return 'stub'
|
||||
thread = Threads.find({'id': thread_id})
|
||||
if not thread:
|
||||
abort(404)
|
||||
target_topic = Topics.find({'id': request.form.get('new_topic_id', default=None)})
|
||||
if not target_topic:
|
||||
abort(404)
|
||||
thread.update({'topic_id': target_topic.id})
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
@bp.post('/threads/<int:thread_id>/lock')
|
||||
def lock_thread(thread_id):
|
||||
return 'stub'
|
||||
thread = Threads.find({'id': thread_id})
|
||||
if not thread:
|
||||
abort(404)
|
||||
thread.update({'is_locked': request.form.get('lock')})
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
@bp.post('/threads/<int:thread_id>/sticky')
|
||||
def sticky_thread(thread_id):
|
||||
return 'stub'
|
||||
thread = Threads.find({'id': thread_id})
|
||||
if not thread:
|
||||
abort(404)
|
||||
thread.update({'is_stickied': request.form.get('sticky')})
|
||||
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||
|
||||
@@ -52,5 +52,11 @@ def feed(slug):
|
||||
return 'stub'
|
||||
|
||||
@bp.get('/new')
|
||||
@login_required
|
||||
def new():
|
||||
return 'stub'
|
||||
topics = Topics.select()
|
||||
try:
|
||||
selected_topic = int(request.args.get('topic_id', default=None))
|
||||
except ValueError, TypeError:
|
||||
selected_topic = None
|
||||
return render_template('threads/new_thread.html', topics=topics, selected_topic=selected_topic)
|
||||
|
||||
@@ -12,10 +12,8 @@
|
||||
{%- endif -%}
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
{%- include 'common/topnav.html' -%}
|
||||
{%- block content -%}{%- endblock -%}
|
||||
{%- include 'common/footer.html' -%}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -83,7 +83,9 @@
|
||||
|
||||
{% macro babycode_editor_component(
|
||||
placeholder='Post content',
|
||||
prefill=''
|
||||
prefill='',
|
||||
required=true,
|
||||
id='babycode-content'
|
||||
) -%}
|
||||
{%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview']) -%}
|
||||
{%- if idx == 0 -%}
|
||||
@@ -101,7 +103,7 @@
|
||||
</span>
|
||||
<a href="##">babycode help</a>
|
||||
</span>
|
||||
<textarea name="babycode_content" class="babycode-editor" placeholder="{{placeholder}}" required>{{ prefill }}</textarea>
|
||||
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}}>{{ prefill }}</textarea>
|
||||
{%- endif -%}
|
||||
{%- endcall -%}
|
||||
{%- endmacro %}
|
||||
|
||||
13
app/templates/mod/edit_topic.html
Normal file
13
app/templates/mod/edit_topic.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{%- from 'common/macros.html' import subheader -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}editing topic {{topic.name}}{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{{subheader('Editing topic %s' % topic.name, 'To preserve history, the URL of the topic can not be changed.')}}
|
||||
<form class="plank primary-bg full-width" method="POST">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required value="{{topic.name}}">
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" id="description" rows="5" required>{{topic.description}}</textarea>
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
{%- endblock -%}
|
||||
13
app/templates/mod/new_topic.html
Normal file
13
app/templates/mod/new_topic.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{%- from 'common/macros.html' import subheader -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}creating a topic{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{{subheader('Create topic', 'The new topic will appear at the bottom of the current topic list. You can sort it later.')}}
|
||||
<form class="plank primary-bg full-width" method="POST">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" id="description" rows="5" required></textarea>
|
||||
<input type="submit" value="Create">
|
||||
</form>
|
||||
{%- endblock -%}
|
||||
19
app/templates/threads/new_thread.html
Normal file
19
app/templates/threads/new_thread.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{%- from 'common/macros.html' import subheader, babycode_editor_component -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}drafting a thread{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{{subheader('New thread')}}
|
||||
<form class="plank primary-bg full-width" method="POST">
|
||||
<label for="topic">Topic</label>
|
||||
<select name="topic_id" id="topic" autocomplete="off">
|
||||
{%- for topic in topics -%}
|
||||
<option value="{{topic.id}}" {{'selected' if selected_topic == topic.id else ''}} {{'disabled' if not get_active_user().can_post_to_topic(topic) else ''}}>{{topic.name}}</option>
|
||||
{%- endfor -%}
|
||||
</select>
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
<label for="babycode-content">Starting post</label>
|
||||
{{ babycode_editor_component() }}
|
||||
<input type="submit" value="Create">
|
||||
</form>
|
||||
{%- endblock -%}
|
||||
@@ -1,10 +1,20 @@
|
||||
{%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%}
|
||||
{%- from 'common/macros.html' import full_post with context -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}{%- endblock -%}
|
||||
{%- block title -%}{{thread.title}}{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{%- set td -%}
|
||||
Started by <a href="{{url_for('users.user_page', username=started_by.username)}}">{{started_by.get_readable_name()}}</a> in topic <a href="{{url_for('topics.topic', slug=topic.slug)}}">{{topic.name}}</a>
|
||||
<ul class="horizontal">
|
||||
<li>Started by <a href="{{url_for('users.user_page', username=started_by.username)}}">{{started_by.get_readable_name()}}</a> in topic <a href="{{url_for('topics.topic', slug=topic.slug)}}">{{topic.name}}</a></li>
|
||||
{%- if thread.locked() or thread.stickied() -%}
|
||||
{%- if thread.locked() -%}
|
||||
<li class="visible">Locked</li>
|
||||
{%- endif -%}
|
||||
{%- if thread.stickied() -%}
|
||||
<li class="visible">Stickied</li>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
{%- endset -%}
|
||||
{%- call() subheader(thread.title, td) -%}
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
@@ -19,13 +29,13 @@ Started by <a href="{{url_for('users.user_page', username=started_by.username)}}
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
<legend>Moderation actions</legend>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="lock" value="{{not thread.locked()}}">
|
||||
<input type="hidden" name="sticky" value="{{not thread.stickied()}}">
|
||||
<input type="hidden" name="lock" value="{{(not thread.locked()) | int}}">
|
||||
<input type="hidden" name="sticky" value="{{(not thread.stickied()) | int}}">
|
||||
<input type="submit" class="warn" value="{{'Unlock' if thread.locked() else 'Lock'}}" formaction="{{url_for('mod.lock_thread', thread_id=thread.id)}}">
|
||||
<input type="submit" class="warn" value="{{'Unsticky' if thread.stickied() else 'Sticky'}}" formaction="{{url_for('mod.sticky_thread', thread_id=thread.id)}}">
|
||||
</form>
|
||||
<form class="horizontal wrap" method="POST" action="{{url_for('mod.move_thread', thread_id=thread.id)}}">
|
||||
<select name="new_topic_id" id="new_topic_id">
|
||||
<select name="new_topic_id" id="new-topic-id">
|
||||
{%- for t in topics -%}
|
||||
<option value="{{t.id}}" {{'selected disabled' if t.id == topic.id else ''}} autocomplete="off">{{t.name}}</option>
|
||||
{%- endfor -%}
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}browsing topic {{topic.name}}{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{%- call() subheader(('Threads in "%s"' % topic.name), topic.description) -%}
|
||||
{%- set td -%}
|
||||
<ul class="horizontal">
|
||||
<li>{{topic.description}}</li>
|
||||
{%- if topic.locked() -%}
|
||||
<li class="visible">Locked</li>
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
{%- endset -%}
|
||||
{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
<legend>Actions</legend>
|
||||
{%- if is_logged_in() -%}
|
||||
{%- if is_logged_in() and get_active_user().can_post_to_topic(topic) -%}
|
||||
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
||||
{%- endif -%}
|
||||
<a href="{{url_for('topics.feed', slug=topic.slug)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||
@@ -20,13 +28,23 @@
|
||||
{%- if is_mod() -%}
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
<legend>Moderation actions</legend>
|
||||
<a href="{{url_for('mod.edit_topic', topic_id=topic.id)}}" class="linkbutton">Edit</a>
|
||||
<form action="{{url_for('mod.lock_topic', topic_id=topic.id)}}" method="POST">
|
||||
<input type="hidden" value="{{(not topic.locked()) | int}}" name="lock">
|
||||
<input type="submit" class="warn" value="{{'Unlock' if topic.locked() else 'Lock'}}">
|
||||
</form>
|
||||
</fieldset>
|
||||
{%- endif -%}
|
||||
{%- if threads | length > 0 -%}
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
<legend>Page</legend>
|
||||
{{- pager(page, page_count, args=request.args) -}}
|
||||
</fieldset>
|
||||
{%- endif -%}
|
||||
{%- endcall -%}
|
||||
{%- if threads | length == 0 -%}
|
||||
<div class="plank"><p>There are no threads in this topic yet.{%- if is_logged_in() and get_active_user().can_post_to_topic(topic) %} Be the first to start a discussion!{%- endif -%}</p></div>
|
||||
{%- endif -%}
|
||||
{%- for thread in threads -%}
|
||||
<div class="topic-info plank">
|
||||
<div class="title-container">
|
||||
@@ -41,10 +59,12 @@
|
||||
<span>Latest post by <a href="{{get_post_url(thread.latest_post_id, _anchor=true)}}">{{thread.latest_post_display_name if thread.latest_post_display_name else thread.latest_post_username}} on {{timestamp(thread.latest_post_created_at)}}</a>{{' (OP)' if thread.posts_count == 1 else ''}}</span>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
{%- if threads | length > 0 -%}
|
||||
<div class="plank secondary-bg">
|
||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||
<legend>Page</legend>
|
||||
{{- pager(page, page_count, args=request.args) -}}
|
||||
</fieldset>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endblock -%}
|
||||
|
||||
@@ -82,9 +82,6 @@ body {
|
||||
background-color: var(--bg-color-tertiary);
|
||||
font-family: Cadman;
|
||||
color: var(--font-color-main);
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
margin: var(--big-padding) var(--wrapper-side-margin);
|
||||
}
|
||||
|
||||
@@ -164,6 +161,10 @@ button, .linkbutton, input[type="submit"] {
|
||||
}
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: var(--base-padding);
|
||||
@@ -187,7 +188,6 @@ button, .linkbutton, input[type="submit"] {
|
||||
.babycode-editor {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.post-edit-form {
|
||||
@@ -202,6 +202,7 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
background-color: var(--main-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: solid var(--border-thickness) var(--border-color);
|
||||
resize: vertical;
|
||||
|
||||
padding: var(--small-padding) var(--medium-padding);
|
||||
margin: var(--base-padding) 0px;
|
||||
@@ -211,6 +212,10 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'Atkinson Hyperlegible Mono'
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -324,10 +329,14 @@ ul.horizontal, ol.horizontal {
|
||||
padding: 0;
|
||||
gap: var(--base-padding);
|
||||
|
||||
& li {
|
||||
& li:not(.visible) {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
& li.visible {
|
||||
margin-left: var(--big-padding);
|
||||
}
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -456,7 +465,6 @@ footer {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--base-padding);
|
||||
width: fit-content;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions-group {
|
||||
@@ -584,6 +592,15 @@ footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
form.full-width {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
&> textarea, &> select, &> input[type="text"], &> input[type="password"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* babycode tags */
|
||||
.inline-code {
|
||||
background-color: var(--code-bg-color);
|
||||
@@ -688,7 +705,7 @@ a.mention {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#wrapper {
|
||||
body {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user