Compare commits
4 commits
6b11bf4537
...
d1f33afe09
| Author | SHA1 | Date | |
|---|---|---|---|
| d1f33afe09 | |||
| 41be26d484 | |||
| 0a3cfccc0d | |||
| a4144c67a9 |
13 changed files with 220 additions and 24 deletions
|
|
@ -26,7 +26,7 @@ from suou import twocolon_list, WantsContentType
|
||||||
|
|
||||||
from .colors import color_themes, theme_classes
|
from .colors import color_themes, theme_classes
|
||||||
|
|
||||||
__version__ = '0.5.0-dev42'
|
__version__ = '0.5.0-dev43'
|
||||||
|
|
||||||
APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
@ -163,6 +163,8 @@ async def error_handler_for(status: int, message: str, template: str):
|
||||||
case WantsContentType.JSON:
|
case WantsContentType.JSON:
|
||||||
return jsonify({'error': f'{message}', 'status': status}), status
|
return jsonify({'error': f'{message}', 'status': status}), status
|
||||||
case WantsContentType.HTML:
|
case WantsContentType.HTML:
|
||||||
|
if request.path.startswith('/admin'):
|
||||||
|
return await render_template('admin/' + template, message=f'{message}'), status
|
||||||
return await render_template(template, message=f'{message}'), status
|
return await render_template(template, message=f'{message}'), status
|
||||||
case WantsContentType.PLAIN:
|
case WantsContentType.PLAIN:
|
||||||
return f'{message} (HTTP {status})', status, {'content-type': 'text/plain; charset=UTF-8'}
|
return f'{message} (HTTP {status})', status, {'content-type': 'text/plain; charset=UTF-8'}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import enum
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
from suou.sqlalchemy.asyncio import AsyncSession
|
||||||
from .models import User, db
|
from .models import User, db
|
||||||
from quart_auth import AuthUser, Action as _Action
|
from quart_auth import AuthUser, Action as _Action
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ class UserLoader(AuthUser):
|
||||||
def __init__(self, auth_id: str | None, action: _Action= _Action.PASS):
|
def __init__(self, auth_id: str | None, action: _Action= _Action.PASS):
|
||||||
self._auth_id = auth_id
|
self._auth_id = auth_id
|
||||||
self._auth_obj = None
|
self._auth_obj = None
|
||||||
self._auth_sess = None
|
self._auth_sess: AsyncSession | None = None
|
||||||
self.action = action
|
self.action = action
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -69,6 +70,10 @@ class UserLoader(AuthUser):
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return self._auth_obj is not None
|
return self._auth_obj is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
return self._auth_sess
|
||||||
|
|
||||||
async def _unload(self):
|
async def _unload(self):
|
||||||
# user is not expected to mutate
|
# user is not expected to mutate
|
||||||
if self._auth_sess:
|
if self._auth_sess:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import datetime
|
import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
@ -15,7 +16,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, an
|
||||||
SmallInteger, select, update, Table
|
SmallInteger, select, update, Table
|
||||||
from sqlalchemy.orm import Relationship, relationship
|
from sqlalchemy.orm import Relationship, relationship
|
||||||
from suou.sqlalchemy_async import SQLAlchemy
|
from suou.sqlalchemy_async import SQLAlchemy
|
||||||
from suou import SiqType, Snowflake, Wanted, deprecated, makelist, not_implemented
|
from suou import SiqType, Snowflake, Wanted, deprecated, makelist, not_implemented, want_isodate
|
||||||
from suou.sqlalchemy import create_session, declarative_base, id_column, parent_children, snowflake_column
|
from suou.sqlalchemy import create_session, declarative_base, id_column, parent_children, snowflake_column
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
|
@ -663,6 +664,17 @@ class Post(Base):
|
||||||
created_at = self.created_at
|
created_at = self.created_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def feed_info_counts(self):
|
||||||
|
pj = self.feed_info()
|
||||||
|
if self.is_text_post():
|
||||||
|
pj['content'] = self.text_content[:181]
|
||||||
|
(pj['comment_count'], pj['votes'], pj['my_vote']) = await asyncio.gather(
|
||||||
|
self.comment_count(),
|
||||||
|
self.upvotes(),
|
||||||
|
self.upvoted_by(current_user.user)
|
||||||
|
)
|
||||||
|
return pj
|
||||||
|
|
||||||
class Comment(Base):
|
class Comment(Base):
|
||||||
__tablename__ = 'freak_comment'
|
__tablename__ = 'freak_comment'
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
|
|
@ -694,6 +706,18 @@ class Comment(Base):
|
||||||
def url(self):
|
def url(self):
|
||||||
return self.parent_post.url() + f'/comment/{Snowflake(self.id):l}'
|
return self.parent_post.url() + f'/comment/{Snowflake(self.id):l}'
|
||||||
|
|
||||||
|
async def is_parent_locked(self):
|
||||||
|
if self.is_locked:
|
||||||
|
return True
|
||||||
|
if self.parent_comment_id == None:
|
||||||
|
return False
|
||||||
|
async with db as session:
|
||||||
|
parent = (await session.execute(select(Comment).where(Comment.id == self.parent_comment_id))).scalar()
|
||||||
|
try:
|
||||||
|
return parent.is_parent_locked()
|
||||||
|
except RecursionError:
|
||||||
|
return True
|
||||||
|
|
||||||
def report_url(self) -> str:
|
def report_url(self) -> str:
|
||||||
return f'/report/comment/{Snowflake(self.id):l}'
|
return f'/report/comment/{Snowflake(self.id):l}'
|
||||||
|
|
||||||
|
|
@ -709,6 +733,21 @@ class Comment(Base):
|
||||||
def not_removed(cls):
|
def not_removed(cls):
|
||||||
return Post.removed_at == None
|
return Post.removed_at == None
|
||||||
|
|
||||||
|
async def section_info(self):
|
||||||
|
obj = dict(
|
||||||
|
id = Snowflake(self.id).to_b32l(),
|
||||||
|
parent = dict(id=Snowflake(self.parent_comment_id)) if self.parent_comment_id else None,
|
||||||
|
locked = await self.is_parent_locked(),
|
||||||
|
created_at = want_isodate(self.created_at)
|
||||||
|
)
|
||||||
|
if self.is_removed:
|
||||||
|
obj['removed'] = self.removed_reason
|
||||||
|
else:
|
||||||
|
obj['content'] = self.text_content
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class PostReport(Base):
|
class PostReport(Base):
|
||||||
__tablename__ = 'freak_postreport'
|
__tablename__ = 'freak_postreport'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Iterable
|
from typing import Iterable, TypeVar
|
||||||
|
|
||||||
from quart import session
|
from quart import session
|
||||||
from quart import abort, Blueprint, redirect, request, url_for
|
from quart import abort, Blueprint, redirect, request, url_for
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from quart_auth import AuthUser, current_user, login_required, login_user, logout_user
|
from quart_auth import current_user, login_required, login_user, logout_user
|
||||||
from quart_schema import QuartSchema, validate_request, validate_response
|
from quart_schema import validate_request, validate_response
|
||||||
from sqlalchemy import select
|
from sqlalchemy import delete, insert, select
|
||||||
from suou import Snowflake, deprecated, makelist, not_implemented, want_isodate
|
from suou import Snowflake, deprecated, makelist, not_implemented, want_isodate
|
||||||
|
|
||||||
|
from suou.classtools import MISSING, MissingType
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from suou.quart import add_rest
|
from suou.quart import add_rest
|
||||||
|
|
||||||
|
|
@ -17,9 +18,11 @@ from freak.accounts import LoginStatus, check_login
|
||||||
from freak.algorithms import public_timeline, top_guilds_query, topic_timeline, user_timeline
|
from freak.algorithms import public_timeline, top_guilds_query, topic_timeline, user_timeline
|
||||||
from freak.search import SearchQuery
|
from freak.search import SearchQuery
|
||||||
|
|
||||||
from ..models import Guild, Post, User, db
|
from ..models import Comment, Guild, Post, PostUpvote, User, db
|
||||||
from .. import UserLoader, app, app_config, __version__ as freak_version, csrf
|
from .. import UserLoader, app, app_config, __version__ as freak_version, csrf
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
bp = Blueprint('rest', __name__, url_prefix='/v1')
|
bp = Blueprint('rest', __name__, url_prefix='/v1')
|
||||||
rest = add_rest(app, '/v1', '/ajax')
|
rest = add_rest(app, '/v1', '/ajax')
|
||||||
|
|
||||||
|
|
@ -116,7 +119,7 @@ async def user_feed_get(id: int):
|
||||||
algo = user_timeline(u)
|
algo = user_timeline(u)
|
||||||
posts = await db.paginate(algo)
|
posts = await db.paginate(algo)
|
||||||
async for p in posts:
|
async for p in posts:
|
||||||
feed.append(p.feed_info())
|
feed.append(await p.feed_info_counts())
|
||||||
|
|
||||||
return dict(users={f'{Snowflake(id):l}': uj}, feed=feed)
|
return dict(users={f'{Snowflake(id):l}': uj}, feed=feed)
|
||||||
|
|
||||||
|
|
@ -155,8 +158,63 @@ async def get_post(id: int):
|
||||||
if p.is_text_post():
|
if p.is_text_post():
|
||||||
pj['content'] = p.text_content
|
pj['content'] = p.text_content
|
||||||
|
|
||||||
|
pj['comment_count'] = await p.comment_count()
|
||||||
|
pj['votes'] = await p.upvotes()
|
||||||
|
pj['my_vote'] = await p.upvoted_by(current_user.user)
|
||||||
|
|
||||||
return dict(posts={f'{Snowflake(id):l}': pj})
|
return dict(posts={f'{Snowflake(id):l}': pj})
|
||||||
|
|
||||||
|
class VoteIn(BaseModel):
|
||||||
|
vote: int
|
||||||
|
|
||||||
|
@bp.post('/post/<b32l:id>/upvote')
|
||||||
|
@validate_request(VoteIn)
|
||||||
|
async def upvote_post(id: int, data: VoteIn):
|
||||||
|
async with db as session:
|
||||||
|
p: Post | None = (await session.execute(select(Post).where(Post.id == id))).scalar()
|
||||||
|
|
||||||
|
if p is None:
|
||||||
|
return { 'status': 404, 'error': 'Post not found' }, 404
|
||||||
|
|
||||||
|
cur_score = await p.upvoted_by(current_user.user)
|
||||||
|
|
||||||
|
match (data.vote, cur_score):
|
||||||
|
case (1, 0) | (1, -1):
|
||||||
|
await session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == True))
|
||||||
|
await session.execute(insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = False))
|
||||||
|
case (0, _):
|
||||||
|
await session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id))
|
||||||
|
case (-1, 1) | (-1, 0):
|
||||||
|
await session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == False))
|
||||||
|
await session.execute(insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = True))
|
||||||
|
case (1, 1) | (1, -1):
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
await session.rollback()
|
||||||
|
return { 'status': 400, 'error': 'Invalid score' }, 400
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return { 'votes': await p.upvotes() }
|
||||||
|
|
||||||
|
## COMMENTS ##
|
||||||
|
|
||||||
|
@bp.get('/post/<b32l:id>/comments')
|
||||||
|
async def post_comments (id: int):
|
||||||
|
async with db as session:
|
||||||
|
p: Post | None = (await session.execute(select(Post).where(Post.id == id))).scalar()
|
||||||
|
|
||||||
|
if p is None:
|
||||||
|
return { 'status': 404, 'error': 'Post not found' }, 404
|
||||||
|
|
||||||
|
l = []
|
||||||
|
for com in await p.top_level_comments():
|
||||||
|
com: Comment
|
||||||
|
l.append(await com.section_info())
|
||||||
|
|
||||||
|
return dict(has=l)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## GUILDS ##
|
## GUILDS ##
|
||||||
|
|
||||||
async def _guild_info(gu: Guild):
|
async def _guild_info(gu: Guild):
|
||||||
|
|
@ -204,7 +262,7 @@ async def guild_feed(gname: str):
|
||||||
algo = topic_timeline(gname)
|
algo = topic_timeline(gname)
|
||||||
posts = await db.paginate(algo)
|
posts = await db.paginate(algo)
|
||||||
async for p in posts:
|
async for p in posts:
|
||||||
feed.append(p.feed_info())
|
feed.append(await p.feed_info_counts())
|
||||||
|
|
||||||
return dict(guilds={f'{Snowflake(gu.id):l}': gj}, feed=feed)
|
return dict(guilds={f'{Snowflake(gu.id):l}': gj}, feed=feed)
|
||||||
|
|
||||||
|
|
@ -253,7 +311,7 @@ async def home_feed():
|
||||||
posts = await db.paginate(public_timeline())
|
posts = await db.paginate(public_timeline())
|
||||||
feed = []
|
feed = []
|
||||||
async for post in posts:
|
async for post in posts:
|
||||||
feed.append(post.feed_info())
|
feed.append(await post.feed_info_counts())
|
||||||
|
|
||||||
return dict(feed=feed)
|
return dict(feed=feed)
|
||||||
|
|
||||||
|
|
@ -277,7 +335,7 @@ async def search_top(data: QueryIn):
|
||||||
async with db as session:
|
async with db as session:
|
||||||
sq = SearchQuery(data.query)
|
sq = SearchQuery(data.query)
|
||||||
|
|
||||||
result: Iterable[Post] = (await session.execute(sq.select(Post, [Post.title]).limit(20))).scalars()
|
result = (await session.execute(sq.select(Post, [Post.title]).limit(20))).scalars()
|
||||||
|
|
||||||
return dict(has = [p.feed_info() for p in result])
|
return dict(has = [p.feed_info() for p in result])
|
||||||
|
|
||||||
|
|
@ -295,6 +353,42 @@ async def suggest_guild(data: QueryIn):
|
||||||
|
|
||||||
result: Iterable[Guild] = (await session.execute(sq.limit(10))).scalars()
|
result: Iterable[Guild] = (await session.execute(sq.limit(10))).scalars()
|
||||||
|
|
||||||
return dict(has = [g.simple_info() for g in result])
|
return dict(has = [g.simple_info() for g in result if await g.allows_posting(current_user.user)])
|
||||||
|
|
||||||
|
|
||||||
|
## SETTINGS
|
||||||
|
|
||||||
|
@bp.get("/settings/appearance")
|
||||||
|
@login_required
|
||||||
|
async def get_settings_appearance():
|
||||||
|
return dict(
|
||||||
|
color_theme = current_user.user.color_theme
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsAppearanceIn(BaseModel):
|
||||||
|
color_theme : int | None = None
|
||||||
|
color_scheme : int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_or(obj: _T | MissingType, obj2: _T) -> _T:
|
||||||
|
if obj is None:
|
||||||
|
return obj2
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@bp.patch("/settings/appearance")
|
||||||
|
@login_required
|
||||||
|
@validate_request(SettingsAppearanceIn)
|
||||||
|
async def patch_settings_appearance(data: SettingsIn):
|
||||||
|
u = current_user.user
|
||||||
|
if u is None:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
u.color_theme = (
|
||||||
|
_missing_or(data.color_theme, u.color_theme % (1 << 8)) % 256 +
|
||||||
|
_missing_or(data.color_scheme, u.color_theme >> 8) << 8
|
||||||
|
)
|
||||||
|
current_user.session.add(u)
|
||||||
|
await current_user.session.commit()
|
||||||
|
|
||||||
|
return '', 204
|
||||||
|
|
|
||||||
10
freak/templates/admin/400.html
Normal file
10
freak/templates/admin/400.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "admin/admin_base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="centered">
|
||||||
|
<h2>Bad Request</h2>
|
||||||
|
|
||||||
|
<p><a href="/">Back to homepage.</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
12
freak/templates/admin/403.html
Normal file
12
freak/templates/admin/403.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "admin/admin_base.html" %}
|
||||||
|
{% from "macros/title.html" import title_tag with context %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="centered">
|
||||||
|
<h2>Access Denied</h2>
|
||||||
|
|
||||||
|
<p><a href="/">Back to homepage.</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
12
freak/templates/admin/404.html
Normal file
12
freak/templates/admin/404.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "admin/admin_base.html" %}
|
||||||
|
{% from "macros/title.html" import title_tag with context %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="centered">
|
||||||
|
<h2>Not Found</h2>
|
||||||
|
|
||||||
|
<p><a href="/admin/">Back</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
11
freak/templates/admin/500.html
Normal file
11
freak/templates/admin/500.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "admin/admin_base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="centered">
|
||||||
|
<h2>Internal Server Error</h2>
|
||||||
|
|
||||||
|
<p>It's on us. <a href="javascript:history.go(0)">Refresh the page</a>.</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
{{ title_tag("Admin") }}
|
{{ title_tag("Admin") }}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/style.css">
|
<link rel="stylesheet" type="text/css" href="/admin/style.css">
|
||||||
{% for private_style in private_styles %}
|
{% for private_style in private_styles %}
|
||||||
<link rel="stylesheet" href="{{ private_style }}" />
|
<link rel="stylesheet" href="{{ private_style }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% macro embed_post(p) %}
|
{% macro embed_post(p) %}
|
||||||
<div id="post-{{ p.id | to_b32l }}" class="post-frame" data-endpoint="{{ p.id | to_b32l }}">
|
<div id="post-{{ p.id | to_b32l }}" class="post-frame" data-endpoint="{{ p.id | to_b32l }}">
|
||||||
<h3 class="message-title"><a href="{{ p.url() }}">{{ p.title }}</a></h3>
|
<h3 class="message-title"><a href="/={{ p.id | to_b32l }}">{{ p.title }}</a></h3>
|
||||||
<div class="message-meta">Posted by <a href="{{ p.author.url() }}">@{{ p.author.username }}</a>
|
<div class="message-meta">Posted by <a href="{{ p.author.url() }}">@{{ p.author.username }}</a>
|
||||||
{% if p.parent_post %}
|
{% if p.parent_post %}
|
||||||
as a comment on <a href="{{ p.parent_post.url() }}">post “{{ p.parent_post.title }}”</a>
|
as a comment on <a href="{{ p.parent_post.url() }}">post “{{ p.parent_post.title }}”</a>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
import warnings
|
import warnings
|
||||||
from quart import Blueprint, abort, redirect, render_template, request, url_for
|
from quart import Blueprint, abort, redirect, render_template, request, send_from_directory, url_for
|
||||||
from quart_auth import current_user
|
from quart_auth import current_user
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from sqlalchemy import insert, select, update
|
from sqlalchemy import insert, select, update
|
||||||
|
|
@ -23,11 +24,11 @@ current_user: UserLoader
|
||||||
|
|
||||||
def admin_required(func: Callable):
|
def admin_required(func: Callable):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*a, **ka):
|
async def wrapper(*a, **ka):
|
||||||
user: User = current_user.user
|
user: User = current_user.user
|
||||||
if not user or not user.is_administrator:
|
if not user or not user.is_administrator:
|
||||||
abort(403)
|
abort(403)
|
||||||
return func(*a, **ka)
|
return await func(*a, **ka)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,10 +156,14 @@ def escalate_report(target, source: PostReport):
|
||||||
async def homepage():
|
async def homepage():
|
||||||
return await render_template('admin/admin_home.html')
|
return await render_template('admin/admin_home.html')
|
||||||
|
|
||||||
|
@bp.route('/admin/style.css')
|
||||||
|
async def style_css():
|
||||||
|
return await send_from_directory(os.path.dirname(os.path.dirname(__file__)) + '/static/css', 'style.css')
|
||||||
|
|
||||||
@bp.route('/admin/reports/')
|
@bp.route('/admin/reports/')
|
||||||
@admin_required
|
@admin_required
|
||||||
async def reports():
|
async def reports():
|
||||||
report_list = db.paginate(select(PostReport).order_by(PostReport.id.desc()))
|
report_list = await db.paginate(select(PostReport).order_by(PostReport.id.desc()))
|
||||||
return await render_template('admin/admin_reports.html',
|
return await render_template('admin/admin_reports.html',
|
||||||
report_list=report_list, report_reasons=REPORT_REASON_STRINGS)
|
report_list=report_list, report_reasons=REPORT_REASON_STRINGS)
|
||||||
|
|
||||||
|
|
@ -169,10 +174,13 @@ async def report_detail(id: int):
|
||||||
report = (await session.execute(select(PostReport).where(PostReport.id == id))).scalar()
|
report = (await session.execute(select(PostReport).where(PostReport.id == id))).scalar()
|
||||||
if report is None:
|
if report is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
target = await report.target()
|
||||||
|
if target is None:
|
||||||
|
abort(404)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = await get_request_form()
|
form = await get_request_form()
|
||||||
action = REPORT_ACTIONS[form['do']]
|
action = REPORT_ACTIONS[form['do']]
|
||||||
await action(report.target(), report)
|
await action(target, report)
|
||||||
return redirect(url_for('admin.reports'))
|
return redirect(url_for('admin.reports'))
|
||||||
return await render_template('admin/admin_report_detail.html', report=report,
|
return await render_template('admin/admin_report_detail.html', report=report,
|
||||||
report_reasons=REPORT_REASON_STRINGS)
|
report_reasons=REPORT_REASON_STRINGS)
|
||||||
|
|
@ -188,7 +196,7 @@ async def strikes():
|
||||||
@bp.route('/admin/users/')
|
@bp.route('/admin/users/')
|
||||||
@admin_required
|
@admin_required
|
||||||
async def users():
|
async def users():
|
||||||
user_list = db.paginate(select(User).order_by(User.joined_at.desc()))
|
user_list = await db.paginate(select(User).order_by(User.joined_at.desc()))
|
||||||
return await render_template('admin/admin_users.html',
|
return await render_template('admin/admin_users.html',
|
||||||
user_list=user_list, account_status_string=colorized_account_status_string)
|
user_list=user_list, account_status_string=colorized_account_status_string)
|
||||||
|
|
||||||
|
|
@ -219,5 +227,7 @@ async def user_detail(id: int):
|
||||||
else:
|
else:
|
||||||
abort(400)
|
abort(400)
|
||||||
strikes = (await session.execute(select(UserStrike).where(UserStrike.user_id == id).order_by(UserStrike.id.desc()))).scalars()
|
strikes = (await session.execute(select(UserStrike).where(UserStrike.user_id == id).order_by(UserStrike.id.desc()))).scalars()
|
||||||
return render_template('admin/admin_user_detail.html', u=u,
|
return await render_template('admin/admin_user_detail.html', u=u,
|
||||||
report_reasons=REPORT_REASON_STRINGS, account_status_string=colorized_account_status_string, strikes=strikes)
|
report_reasons=REPORT_REASON_STRINGS, account_status_string=colorized_account_status_string, strikes=strikes)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||||
from quart import Blueprint, render_template, request
|
from quart import Blueprint, render_template, request
|
||||||
from quart_auth import current_user, login_required
|
from quart_auth import current_user, login_required
|
||||||
from sqlalchemy import insert, select
|
from sqlalchemy import insert, select
|
||||||
|
from suou import Snowflake
|
||||||
|
|
||||||
from freak import UserLoader
|
from freak import UserLoader
|
||||||
from ..models import REPORT_TARGET_COMMENT, REPORT_TARGET_POST, ReportReason, User, post_report_reasons, Comment, Post, PostReport, REPORT_REASONS, db
|
from ..models import REPORT_TARGET_COMMENT, REPORT_TARGET_POST, ReportReason, User, post_report_reasons, Comment, Post, PostReport, REPORT_REASONS, db
|
||||||
|
|
@ -35,7 +36,7 @@ async def report_post(id: int):
|
||||||
reason_code = REPORT_REASONS[reason]
|
reason_code = REPORT_REASONS[reason]
|
||||||
))
|
))
|
||||||
session.commit()
|
session.commit()
|
||||||
return await render_template('reports/report_done.html', back_to_url=p.url())
|
return await render_template('reports/report_done.html', back_to_url='/=' + Snowflake(p.id).to_b32l())
|
||||||
return await render_template('reports/report_post.html', id = id,
|
return await render_template('reports/report_post.html', id = id,
|
||||||
report_reasons = post_report_reasons, description_text=description_text)
|
report_reasons = post_report_reasons, description_text=description_text)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ dependencies = [
|
||||||
"libsass",
|
"libsass",
|
||||||
"setuptools>=78.1.0",
|
"setuptools>=78.1.0",
|
||||||
"Hypercorn",
|
"Hypercorn",
|
||||||
"suou>=0.6.1"
|
"suou[sqlalchemy]>=0.7.5"
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue