diff --git a/freak/__init__.py b/freak/__init__.py index 7b98de3..eaa67c4 100644 --- a/freak/__init__.py +++ b/freak/__init__.py @@ -26,7 +26,7 @@ from suou import twocolon_list, WantsContentType from .colors import color_themes, theme_classes -__version__ = '0.5.0-dev43' +__version__ = '0.5.0-dev42' APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -163,8 +163,6 @@ async def error_handler_for(status: int, message: str, template: str): case WantsContentType.JSON: return jsonify({'error': f'{message}', 'status': status}), status 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 case WantsContentType.PLAIN: return f'{message} (HTTP {status})', status, {'content-type': 'text/plain; charset=UTF-8'} diff --git a/freak/accounts.py b/freak/accounts.py index db34516..5ae845c 100644 --- a/freak/accounts.py +++ b/freak/accounts.py @@ -5,7 +5,6 @@ import enum from sqlalchemy import select from sqlalchemy.orm import selectinload -from suou.sqlalchemy.asyncio import AsyncSession from .models import User, db from quart_auth import AuthUser, Action as _Action @@ -43,7 +42,7 @@ class UserLoader(AuthUser): def __init__(self, auth_id: str | None, action: _Action= _Action.PASS): self._auth_id = auth_id self._auth_obj = None - self._auth_sess: AsyncSession | None = None + self._auth_sess = None self.action = action @property @@ -70,10 +69,6 @@ class UserLoader(AuthUser): def __bool__(self): return self._auth_obj is not None - @property - def session(self): - return self._auth_sess - async def _unload(self): # user is not expected to mutate if self._auth_sess: diff --git a/freak/models.py b/freak/models.py index d34d567..392f6ad 100644 --- a/freak/models.py +++ b/freak/models.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections import namedtuple import datetime from functools import partial @@ -16,7 +15,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, an SmallInteger, select, update, Table from sqlalchemy.orm import Relationship, relationship from suou.sqlalchemy_async import SQLAlchemy -from suou import SiqType, Snowflake, Wanted, deprecated, makelist, not_implemented, want_isodate +from suou import SiqType, Snowflake, Wanted, deprecated, makelist, not_implemented from suou.sqlalchemy import create_session, declarative_base, id_column, parent_children, snowflake_column from werkzeug.security import check_password_hash @@ -664,17 +663,6 @@ class Post(Base): 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): __tablename__ = 'freak_comment' __table_args__ = ( @@ -706,18 +694,6 @@ class Comment(Base): def url(self): 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: return f'/report/comment/{Snowflake(self.id):l}' @@ -733,21 +709,6 @@ class Comment(Base): def not_removed(cls): 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): __tablename__ = 'freak_postreport' diff --git a/freak/rest/__init__.py b/freak/rest/__init__.py index e89dc85..1f66eb5 100644 --- a/freak/rest/__init__.py +++ b/freak/rest/__init__.py @@ -1,16 +1,15 @@ from __future__ import annotations -from typing import Iterable, TypeVar +from typing import Iterable from quart import session from quart import abort, Blueprint, redirect, request, url_for from pydantic import BaseModel -from quart_auth import current_user, login_required, login_user, logout_user -from quart_schema import validate_request, validate_response -from sqlalchemy import delete, insert, select +from quart_auth import AuthUser, current_user, login_required, login_user, logout_user +from quart_schema import QuartSchema, validate_request, validate_response +from sqlalchemy import select from suou import Snowflake, deprecated, makelist, not_implemented, want_isodate -from suou.classtools import MISSING, MissingType from werkzeug.security import check_password_hash from suou.quart import add_rest @@ -18,11 +17,9 @@ from freak.accounts import LoginStatus, check_login from freak.algorithms import public_timeline, top_guilds_query, topic_timeline, user_timeline from freak.search import SearchQuery -from ..models import Comment, Guild, Post, PostUpvote, User, db +from ..models import Guild, Post, User, db from .. import UserLoader, app, app_config, __version__ as freak_version, csrf -_T = TypeVar('_T') - bp = Blueprint('rest', __name__, url_prefix='/v1') rest = add_rest(app, '/v1', '/ajax') @@ -119,7 +116,7 @@ async def user_feed_get(id: int): algo = user_timeline(u) posts = await db.paginate(algo) async for p in posts: - feed.append(await p.feed_info_counts()) + feed.append(p.feed_info()) return dict(users={f'{Snowflake(id):l}': uj}, feed=feed) @@ -158,63 +155,8 @@ async def get_post(id: int): if p.is_text_post(): 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}) -class VoteIn(BaseModel): - vote: int - -@bp.post('/post//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//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 ## async def _guild_info(gu: Guild): @@ -262,7 +204,7 @@ async def guild_feed(gname: str): algo = topic_timeline(gname) posts = await db.paginate(algo) async for p in posts: - feed.append(await p.feed_info_counts()) + feed.append(p.feed_info()) return dict(guilds={f'{Snowflake(gu.id):l}': gj}, feed=feed) @@ -311,7 +253,7 @@ async def home_feed(): posts = await db.paginate(public_timeline()) feed = [] async for post in posts: - feed.append(await post.feed_info_counts()) + feed.append(post.feed_info()) return dict(feed=feed) @@ -335,7 +277,7 @@ async def search_top(data: QueryIn): async with db as session: sq = SearchQuery(data.query) - result = (await session.execute(sq.select(Post, [Post.title]).limit(20))).scalars() + result: Iterable[Post] = (await session.execute(sq.select(Post, [Post.title]).limit(20))).scalars() return dict(has = [p.feed_info() for p in result]) @@ -353,42 +295,6 @@ async def suggest_guild(data: QueryIn): result: Iterable[Guild] = (await session.execute(sq.limit(10))).scalars() - return dict(has = [g.simple_info() for g in result if await g.allows_posting(current_user.user)]) + return dict(has = [g.simple_info() for g in result]) -## 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 diff --git a/freak/templates/admin/400.html b/freak/templates/admin/400.html deleted file mode 100644 index 302b34c..0000000 --- a/freak/templates/admin/400.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "admin/admin_base.html" %} - - -{% block content %} -
-

Bad Request

- -

Back to homepage.

-
-{% endblock %} diff --git a/freak/templates/admin/403.html b/freak/templates/admin/403.html deleted file mode 100644 index d48cc53..0000000 --- a/freak/templates/admin/403.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/admin_base.html" %} -{% from "macros/title.html" import title_tag with context %} - - - -{% block content %} -
-

Access Denied

- -

Back to homepage.

-
-{% endblock %} diff --git a/freak/templates/admin/404.html b/freak/templates/admin/404.html deleted file mode 100644 index b7dd7ff..0000000 --- a/freak/templates/admin/404.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/admin_base.html" %} -{% from "macros/title.html" import title_tag with context %} - - - -{% block content %} -
-

Not Found

- -

Back

-
-{% endblock %} diff --git a/freak/templates/admin/500.html b/freak/templates/admin/500.html deleted file mode 100644 index 23e2f50..0000000 --- a/freak/templates/admin/500.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "admin/admin_base.html" %} - - -{% block content %} -
-

Internal Server Error

- -

It's on us. Refresh the page.

-
-{% endblock %} - diff --git a/freak/templates/admin/admin_base.html b/freak/templates/admin/admin_base.html index 46d9655..5288f35 100644 --- a/freak/templates/admin/admin_base.html +++ b/freak/templates/admin/admin_base.html @@ -5,7 +5,7 @@ {{ title_tag("Admin") }} - + {% for private_style in private_styles %} {% endfor %} diff --git a/freak/templates/macros/embed.html b/freak/templates/macros/embed.html index 8b5633c..c96db78 100644 --- a/freak/templates/macros/embed.html +++ b/freak/templates/macros/embed.html @@ -1,6 +1,6 @@ {% macro embed_post(p) %}
-

{{ p.title }}

+

{{ p.title }}

Posted by @{{ p.author.username }} {% if p.parent_post %} as a comment on post “{{ p.parent_post.title }}” diff --git a/freak/website/admin.py b/freak/website/admin.py index accb25e..46cb9b0 100644 --- a/freak/website/admin.py +++ b/freak/website/admin.py @@ -2,10 +2,9 @@ import datetime from functools import wraps -import os from typing import Callable import warnings -from quart import Blueprint, abort, redirect, render_template, request, send_from_directory, url_for +from quart import Blueprint, abort, redirect, render_template, request, url_for from quart_auth import current_user from markupsafe import Markup from sqlalchemy import insert, select, update @@ -24,11 +23,11 @@ current_user: UserLoader def admin_required(func: Callable): @wraps(func) - async def wrapper(*a, **ka): + def wrapper(*a, **ka): user: User = current_user.user if not user or not user.is_administrator: abort(403) - return await func(*a, **ka) + return func(*a, **ka) return wrapper @@ -156,14 +155,10 @@ def escalate_report(target, source: PostReport): async def homepage(): 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/') @admin_required async def reports(): - report_list = await db.paginate(select(PostReport).order_by(PostReport.id.desc())) + report_list = db.paginate(select(PostReport).order_by(PostReport.id.desc())) return await render_template('admin/admin_reports.html', report_list=report_list, report_reasons=REPORT_REASON_STRINGS) @@ -174,13 +169,10 @@ async def report_detail(id: int): report = (await session.execute(select(PostReport).where(PostReport.id == id))).scalar() if report is None: abort(404) - target = await report.target() - if target is None: - abort(404) if request.method == 'POST': form = await get_request_form() action = REPORT_ACTIONS[form['do']] - await action(target, report) + await action(report.target(), report) return redirect(url_for('admin.reports')) return await render_template('admin/admin_report_detail.html', report=report, report_reasons=REPORT_REASON_STRINGS) @@ -196,7 +188,7 @@ async def strikes(): @bp.route('/admin/users/') @admin_required async def users(): - user_list = await db.paginate(select(User).order_by(User.joined_at.desc())) + user_list = db.paginate(select(User).order_by(User.joined_at.desc())) return await render_template('admin/admin_users.html', user_list=user_list, account_status_string=colorized_account_status_string) @@ -227,7 +219,5 @@ async def user_detail(id: int): else: abort(400) strikes = (await session.execute(select(UserStrike).where(UserStrike.user_id == id).order_by(UserStrike.id.desc()))).scalars() - return await render_template('admin/admin_user_detail.html', u=u, + return render_template('admin/admin_user_detail.html', u=u, report_reasons=REPORT_REASON_STRINGS, account_status_string=colorized_account_status_string, strikes=strikes) - - diff --git a/freak/website/reports.py b/freak/website/reports.py index 21b08c5..f24273c 100644 --- a/freak/website/reports.py +++ b/freak/website/reports.py @@ -5,7 +5,6 @@ from __future__ import annotations from quart import Blueprint, render_template, request from quart_auth import current_user, login_required from sqlalchemy import insert, select -from suou import Snowflake from freak import UserLoader from ..models import REPORT_TARGET_COMMENT, REPORT_TARGET_POST, ReportReason, User, post_report_reasons, Comment, Post, PostReport, REPORT_REASONS, db @@ -36,7 +35,7 @@ async def report_post(id: int): reason_code = REPORT_REASONS[reason] )) session.commit() - return await render_template('reports/report_done.html', back_to_url='/=' + Snowflake(p.id).to_b32l()) + return await render_template('reports/report_done.html', back_to_url=p.url()) return await render_template('reports/report_post.html', id = id, report_reasons = post_report_reasons, description_text=description_text) diff --git a/pyproject.toml b/pyproject.toml index 6f60780..dab79c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "libsass", "setuptools>=78.1.0", "Hypercorn", - "suou[sqlalchemy]>=0.7.5" + "suou>=0.6.1" ] requires-python = ">=3.10" classifiers = [