diff --git a/db/queries.py b/db/queries.py index 785ede3..2e98e99 100644 --- a/db/queries.py +++ b/db/queries.py @@ -1,4 +1,5 @@ import datetime +import json from peewee import JOIN, fn from util import sqlite_time @@ -171,5 +172,13 @@ def timeline_event_put(type, text, user, orders_pool, order_status=None, extra=N user=user, orders_pool=orders_pool, order_status=order_status, - extra=extra + extra=json.dumps(extra) if extra is not None else None ) + +def timeline_event_recent(user_id): + return (TimelineEvent + .select() + .where(TimelineEvent.user_id == user_id) + .order_by(TimelineEvent.updated_at.desc()) + .limit(10) + ) \ No newline at end of file diff --git a/main.py b/main.py index d6bff8a..4fb806c 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ async def do_order_issue(order_pool_id): logger.info(f'Issued order id {order_status.id}') timeline_event_put( TIMELINE_ORDER_ISSUED, - f"Order issued from {orders_pool.name} - {order_status.text.split("\n")[0]}", + order_status.text.split("\n")[0], user=orders_pool.user, orders_pool=orders_pool, order_status=order_status, diff --git a/orders.py b/orders.py index 226f629..2581082 100644 --- a/orders.py +++ b/orders.py @@ -199,7 +199,7 @@ async def order_check(order_status_id): logger.info('Confirmed order %s' % (order_status.id)) timeline_event_put( TIMELINE_ORDER_CONFIRMED, - f'Order confirmed - {order_status.text.split("\n")[0]}', + order_status.text.split("\n")[0], order_status.user, order_status.pool, order_status, @@ -222,7 +222,7 @@ async def order_check(order_status_id): punishment_status = issue_result['order_status'] timeline_event_put( TIMELINE_ORDER_PUNISHED, - f'Order punished - {punishment_status.text.split("\n")[0]}', + punishment_status.text.split("\n")[0], punishment_status.user, punishment_status.pool, punishment_status, @@ -233,7 +233,7 @@ async def order_check(order_status_id): elif 'reason' in issue_result: timeline_event_put( TIMELINE_ORDER_NOT_PUNISHED, - f'Order not punished - {issue_result['reason']}', + issue_result['reason'], order_status.user, order_status.pool, order_status diff --git a/scheduling.py b/scheduling.py index b45ddaf..1bad1ee 100644 --- a/scheduling.py +++ b/scheduling.py @@ -99,7 +99,7 @@ class OrderScheduler(): ) timeline_event_put( TIMELINE_ORDER_ISSUED, - f"Order issued - {order_status.text.split("\n")[0]}", + order_status.text.split("\n")[0], user=orders_pool.user, orders_pool=orders_pool, order_status=order_status, diff --git a/util.py b/util.py index c7c592c..fa8664c 100644 --- a/util.py +++ b/util.py @@ -13,6 +13,9 @@ def timezone(): def sqlite_time(dt): return dt.astimezone(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S') +def time_sqlite(dt): + return dt.replace(tzinfo=datetime.UTC) + def order_time(str): order_time_arr = list(map(int, str.split(':'))) return datetime.time( diff --git a/web/api.py b/web/api.py index 1084368..3db9286 100644 --- a/web/api.py +++ b/web/api.py @@ -1,11 +1,13 @@ import re import requests +import json from functools import wraps from flask import Blueprint, jsonify, abort, request from flask_login import login_required, current_user from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer -from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set +from db.queries import timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE +from util import time_sqlite RE_MASTODON_ATTN_LIST = re.compile(r'^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$') @@ -44,6 +46,31 @@ def profile(): return ('', 204) +def maybe_json(str): + try: + return json.loads(str) + except: + return None + +@api.route('/timeline') +@login_required +def timeline(): + return jsonify([ + { + "id": t.id, + "updated_at": time_sqlite(t.updated_at).isoformat(), + "type": t.type, + "text": t.text, + "extra": maybe_json(t.extra), + "username": t.user.telegram_username, + "orders_pool": { + "id": t.orders_pool.id, + "name": t.orders_pool.name, + }, + "order_status": t.order_status.text if t.order_status is not None else None, + } for t in timeline_event_recent(current_user.db_user.id) + ]) + @api.route('/mastodon_oauth') @login_required def mastodon_oauth(): @@ -287,7 +314,8 @@ def sub_order_set(username, set_id, sub): ) update_add_ons(order_to_update, updated_order['add_ons']) - except: + except Exception as e: + print(e) transaction.rollback() abort(500) elif request.method == 'DELETE': diff --git a/web/vite/package.json b/web/vite/package.json index d74e0ce..a23e891 100644 --- a/web/vite/package.json +++ b/web/vite/package.json @@ -20,6 +20,7 @@ "@mantine/form": "^8.3.13", "@mantine/notifications": "^8.3.13", "@tabler/icons-react": "^3.36.1", + "moment": "^2.30.1", "postcss": "^8.5.6", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/web/vite/src/Dashboard.tsx b/web/vite/src/Dashboard.tsx index b908871..50303be 100644 --- a/web/vite/src/Dashboard.tsx +++ b/web/vite/src/Dashboard.tsx @@ -1,15 +1,17 @@ import React from "react"; -import { Container, Text, Title, Flex, Card, Image, Box } from "@mantine/core"; +import { Text, Title, Flex, Card, Image } from "@mantine/core"; import { useLoaderData } from "react-router"; import { IconPencil } from "@tabler/icons-react"; import { NavigateButton } from "./NavigateButton"; import { OrderSetProps, OrderSets } from "./OrderSets"; import { useUserContext } from "./UserContext"; +import { TimelineList } from "./TimelineList"; export const subsListLoader = () => Promise.all([ fetch("/api/orders/").then((response) => response.json()), fetch("/api/subs").then((response) => response.json()), + fetch("/api/timeline").then((response) => response.json()), ]); interface SubsListProps { @@ -58,14 +60,17 @@ const SubsList: React.FC = ({ subs }) => ( ); export const Dashboard: React.FC = () => { - const [orderSets, subs] = - useLoaderData<[OrderSetProps["orderSets"], SubsListProps["subs"]]>(); + const [orderSets, subs, timeline] = + useLoaderData< + [OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]] + >(); const { username } = useUserContext(); return ( <> {subs.length > 0 ? : null} + {timeline.length > 0 ? : null} ); }; diff --git a/web/vite/src/SubOrderSets.tsx b/web/vite/src/SubOrderSets.tsx index b6554bb..df246a4 100644 --- a/web/vite/src/SubOrderSets.tsx +++ b/web/vite/src/SubOrderSets.tsx @@ -1,16 +1,5 @@ -import { Container, Title, Card, Text, Box, Flex, Badge } from "@mantine/core"; -import { TimeValue } from "@mantine/dates"; -import { IconPencil, IconPlus, IconTrash } from "@tabler/icons-react"; import React from "react"; -import { - Params, - useLoaderData, - useParams, - useFetcher, - Link, -} from "react-router"; -import { ConfirmDialogButton } from "./ConfirmDialogButton"; -import { NavigateButton } from "./NavigateButton"; +import { Params, useLoaderData, useParams, Link } from "react-router"; import { OrderSetProps, OrderSets } from "./OrderSets"; export const subOrderSetsLoader = async ({ diff --git a/web/vite/src/TimelineList.tsx b/web/vite/src/TimelineList.tsx new file mode 100644 index 0000000..d093d58 --- /dev/null +++ b/web/vite/src/TimelineList.tsx @@ -0,0 +1,75 @@ +import { Timeline, Text, Title, Box, Flex } from "@mantine/core"; +import React from "react"; +import moment from "moment"; +import { Link } from "react-router"; +import { + IconAlertCircle, + IconArrowBadgeRightFilled, + IconCheck, + IconExternalLink, + IconX, +} from "@tabler/icons-react"; + +const TIMELINE_TYPE = { + ORDER_NOT_ISSUED: { + title: "Order not issued", + color: "gray.6", + bullet: , + }, + ORDER_ISSUED: { + title: "Order issued", + color: "yellow.6", + bullet: , + }, + ORDER_CONFIRMED: { + title: "Order confirmed", + color: "green.6", + bullet: , + }, + ORDER_NOT_PUNISHED: { + title: "Order not punished", + color: "gray.6", + bullet: null, + }, + ORDER_PUNISHED: { + title: "Order punished", + color: "red.6", + bullet: , + }, +}; + +export const TimelineList: React.FC<{ + timeline: TimelineEvent[]; +}> = ({ timeline }) => ( + + + Timeline + + + {timeline.map( + ({ id, updated_at, type, text, username, orders_pool, extra }) => ( + + {text} + + {extra?.mastodon_status_url ? ( + + + Mastodon Post + + + ) : null} + {orders_pool ? ( + + + {orders_pool.name} + + + ) : null} + {moment(updated_at).fromNow()} + + + ), + )} + + +); diff --git a/web/vite/src/index.d.ts b/web/vite/src/index.d.ts index b7d0ea8..cc08831 100644 --- a/web/vite/src/index.d.ts +++ b/web/vite/src/index.d.ts @@ -24,3 +24,14 @@ type OrderSet = { confirm_delay: string; punishment_pool_id?: number; } + +type TimelineEvent = { + id: number; + updated_at: string; + type: string; + text: string; + extra: Record; + username: string; + orders_pool: Pick; + order_status: string; +} \ No newline at end of file