From b154cc9f2f657e866121e31a9e8ea2e0975a51bd Mon Sep 17 00:00:00 2001
From: Brage <40792825+Terbau@users.noreply.github.com>
Date: Sun, 24 Mar 2024 20:35:31 +0100
Subject: [PATCH] Fix wall of shame (#349)
* fix: tabnav being dumb
* fix: wall of shame loading slow
* fix: various small bugs
---
.gitignore | 1 +
backend/app/api/__init__.py | 2 +-
backend/app/api/endpoints/user.py | 30 ++-
backend/app/api/init_api.py | 6 +-
backend/app/db/groups.py | 1 +
backend/app/db/users.py | 182 +++++++++---------
backend/app/models/punishment.py | 5 +
backend/app/models/user.py | 7 +
frontend/package.json | 1 +
frontend/pnpm-lock.yaml | 15 +-
.../leaderboardtable/LeaderboardTable.tsx | 35 ++--
.../leaderboardtable/LeaderboardTableItem.tsx | 101 ++++------
frontend/src/components/menu/Menu.tsx | 2 +-
.../components/punishment/PunishmentList.tsx | 7 +
.../punishment/SkeletonPunishmentItem.tsx | 9 +
frontend/src/helpers/api.ts | 12 +-
frontend/src/helpers/types.ts | 4 +-
frontend/src/index.css | 37 +++-
.../src/pages/wallOfShame/WallOfShame.tsx | 6 +-
frontend/src/views/groups/GroupView.tsx | 66 ++++---
.../groups/tabnav/AdditionalGroupNavItem.tsx | 4 +-
frontend/src/views/groups/tabnav/TabNav.tsx | 4 +-
.../src/views/groups/tabnav/TabNavItem.tsx | 23 +--
frontend/tailwind.config.cjs | 2 +-
24 files changed, 307 insertions(+), 255 deletions(-)
create mode 100644 frontend/src/components/punishment/SkeletonPunishmentItem.tsx
diff --git a/.gitignore b/.gitignore
index cad5a6cd..0cf3fd0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,4 @@ dist-ssr
tmpMock.ts
.python-version
+.env
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
index d84cd9b9..c2780f30 100644
--- a/backend/app/api/__init__.py
+++ b/backend/app/api/__init__.py
@@ -28,7 +28,7 @@
oidc = OpenIdConnect(
- openIdConnectUrl="https://old.online.ntnu.no/openid/.well-known/openid-configuration",
+ openIdConnectUrl="https://auth.online.ntnu.no/openid/.well-known/openid-configuration",
)
diff --git a/backend/app/api/endpoints/user.py b/backend/app/api/endpoints/user.py
index dcc47517..7d3812b5 100644
--- a/backend/app/api/endpoints/user.py
+++ b/backend/app/api/endpoints/user.py
@@ -2,13 +2,15 @@
User endpoints
"""
+from app.models.punishment import LeaderboardPunishmentRead
from fastapi import APIRouter, Depends, HTTPException, Query
from app.api import APIRoute, Request, oidc
from app.exceptions import NotFound
from app.models.group import UserWithGroups
-from app.models.user import LeaderboardUser
+from app.models.user import MinifiedLeaderboardUser
from app.utils.pagination import Page, Pagination
+from app.types import UserId
router = APIRouter(
prefix="/users",
@@ -61,14 +63,14 @@ async def get_me(
@router.get(
"/leaderboard",
- response_model=Page[LeaderboardUser],
+ response_model=Page[MinifiedLeaderboardUser],
dependencies=[Depends(oidc)],
)
async def get_leadeboard(
request: Request,
page: int = Query(title="Page number", default=0, ge=0),
page_size: int = Query(title="Page size", default=30, ge=1, le=50),
-) -> Page[LeaderboardUser]:
+) -> Page[MinifiedLeaderboardUser]:
access_token = request.raise_if_missing_authorization()
app = request.app
@@ -81,11 +83,29 @@ async def get_leadeboard(
status_code=403, detail="Du har ikke tilgang til denne ressursen"
)
- pagination = Pagination[LeaderboardUser](
+ pagination = Pagination[MinifiedLeaderboardUser](
request=request,
total_coro=app.db.users.get_leaderboard_count,
- results_coro=app.db.users.get_leaderboard,
+ # results_coro=app.db.users.get_leaderboard,
+ results_coro=app.db.users.get_minified_leaderboard,
page=page,
page_size=page_size,
)
return await pagination.paginate(conn=conn)
+
+@router.get(
+ "/leaderboard/punishments/{user_id}",
+ response_model=list[LeaderboardPunishmentRead],
+ dependencies=[Depends(oidc)],
+)
+async def get_user_punishments(
+ request: Request,
+ user_id: UserId,
+) -> list[LeaderboardPunishmentRead]:
+ access_token = request.raise_if_missing_authorization()
+
+ app = request.app
+ _, _ = await app.ow_sync.sync_for_access_token(access_token)
+
+ async with app.db.pool.acquire() as conn:
+ return await app.db.users.get_punishments_for_leaderboard_user(user_id, conn=conn)
diff --git a/backend/app/api/init_api.py b/backend/app/api/init_api.py
index 74cadc34..5587b667 100644
--- a/backend/app/api/init_api.py
+++ b/backend/app/api/init_api.py
@@ -111,7 +111,7 @@ async def shutdown_handler() -> None:
def init_api(**db_settings: str) -> FastAPI:
oauth = {
- "clientId": "219919",
+ "clientId": "5rOMfB8Ztegz",
"appName": "Vengeful Vineyard Docs",
"usePkceWithAuthorizationCodeGrant": True,
"scopes": "openid email profile onlineweb4",
@@ -122,10 +122,6 @@ def init_api(**db_settings: str) -> FastAPI:
swagger_ui_oauth2_redirect_url="/docs/oauth2-redirect",
)
- @app.get("/crash")
- async def crash():
- raise Exception("wow i crashed")
-
app.openapi_version = "3.0.0"
app.router.route_class = APIRoute
init_middlewares(app)
diff --git a/backend/app/db/groups.py b/backend/app/db/groups.py
index 9f959438..b9596851 100644
--- a/backend/app/db/groups.py
+++ b/backend/app/db/groups.py
@@ -314,6 +314,7 @@ async def insert_members(
FROM
unnest($1::group_members[]) as m
)
+ ON CONFLICT (group_id, user_id) DO NOTHING
RETURNING *
"""
diff --git a/backend/app/db/users.py b/backend/app/db/users.py
index 86b689e4..1f0b73f5 100644
--- a/backend/app/db/users.py
+++ b/backend/app/db/users.py
@@ -5,7 +5,8 @@
from app.exceptions import DatabaseIntegrityException, NotFound
from app.models.group import Group
-from app.models.user import LeaderboardUser, User, UserCreate, UserUpdate
+from app.models.user import LeaderboardUser, MinifiedLeaderboardUser, User, UserCreate, UserUpdate
+from app.models.punishment import LeaderboardPunishmentRead
from app.types import InsertOrUpdateUser, OWUserId, UserId
from app.utils.db import MaybeAcquire
@@ -44,93 +45,6 @@ async def get_leaderboard_count(
assert isinstance(res, int)
return res
- async def get_leaderboard(
- self,
- offset: int,
- limit: int,
- force_include_reasons: bool = False,
- conn: Optional[Pool] = None,
- ) -> list[LeaderboardUser]:
- async with MaybeAcquire(conn, self.db.pool) as conn:
- query = """
- WITH punishments_with_reactions AS (
- SELECT
- gp.*,
- COALESCE(NULLIF(u.first_name, ''), u.email) || ' ' || u.last_name as created_by_name,
- COALESCE(json_agg(json_build_object(
- 'punishment_reaction_id', pr.punishment_reaction_id,
- 'punishment_id', pr.punishment_id,
- 'emoji', pr.emoji,
- 'created_at', pr.created_at,
- 'created_by', pr.created_by,
- 'created_by_name', (SELECT COALESCE(NULLIF(first_name, ''), email) || ' ' || last_name FROM users WHERE user_id = pr.created_by)
- )) FILTER (WHERE pr.punishment_reaction_id IS NOT NULL), '[]') as reactions
- FROM group_punishments gp
- LEFT JOIN punishment_reactions pr
- ON pr.punishment_id = gp.punishment_id
- LEFT JOIN users u
- ON u.user_id = gp.created_by
- LEFT JOIN groups g
- ON g.group_id = gp.group_id
- WHERE g.ow_group_id IS NOT NULL OR special
- GROUP BY gp.punishment_id, created_by_name
- )
- SELECT u.*,
- COALESCE(json_agg(
- json_build_object(
- 'punishment_id', pwr.punishment_id,
- 'user_id', pwr.user_id,
- 'punishment_type_id', pwr.punishment_type_id,
- 'reason', pwr.reason,
- 'reason_hidden', pwr.reason_hidden,
- 'amount', pwr.amount,
- 'created_by', pwr.created_by,
- 'created_by_name', pwr.created_by_name,
- 'created_at', pwr.created_at,
- 'group_id', pwr.group_id,
- 'paid', pwr.paid,
- 'paid_at', pwr.paid_at,
- 'marked_paid_by', pwr.marked_paid_by,
- 'reactions', pwr.reactions,
- 'punishment_type', (SELECT json_build_object(
- 'punishment_type_id', pt.punishment_type_id,
- 'name', pt.name,
- 'value', pt.value,
- 'emoji', pt.emoji,
- 'created_at', pt.created_at,
- 'created_by', pt.created_by,
- 'updated_at', pt.updated_at
- ) FROM punishment_types pt WHERE pt.punishment_type_id = pwr.punishment_type_id)
- )
- ) FILTER (WHERE pwr.punishment_id IS NOT NULL), '[]') AS punishments,
- COALESCE(SUM(pwr.amount * pt.value), 0) as total_value
- FROM users u
- LEFT JOIN punishments_with_reactions pwr
- ON pwr.user_id = u.user_id
- LEFT JOIN punishment_types pt
- ON pt.punishment_type_id = pwr.punishment_type_id
- INNER JOIN groups g
- ON g.group_id = pwr.group_id AND g.ow_group_id IS NOT NULL OR special
- GROUP BY u.user_id
- ORDER BY total_value DESC, u.first_name ASC
- OFFSET $1
- LIMIT $2"""
- res = await conn.fetch(
- query,
- offset,
- limit,
- )
-
- users = [LeaderboardUser(**r) for r in res]
-
- if not force_include_reasons:
- for user in users:
- for punishment in user.punishments:
- if punishment.reason_hidden:
- punishment.reason = ""
-
- return users
-
async def get_all_raw(
self,
conn: Optional[Pool] = None,
@@ -432,3 +346,95 @@ async def get_groups(
result = await conn.fetch(query, user_id)
return [Group(**row) for row in result]
+
+ async def get_minified_leaderboard(
+ self,
+ offset: int,
+ limit: int,
+ conn: Optional[Pool] = None,
+ ) -> list[MinifiedLeaderboardUser]:
+ async with MaybeAcquire(conn, self.db.pool) as conn:
+ query = """
+ SELECT
+ DISTINCT u.user_id,
+ u.first_name,
+ u.last_name,
+ u.email,
+ u.ow_user_id,
+ COALESCE(p.total_value, 0) AS total_value,
+ COALESCE(p.emojis, '') AS emojis,
+ COALESCE(p.amount_punishments, 0) AS amount_punishments,
+ COALESCE(p.amount_unique_punishments, 0) AS amount_unique_punishments
+ FROM users u
+ LEFT JOIN (
+ SELECT
+ p.user_id,
+ SUM(pt.value * p.amount) AS total_value,
+ STRING_AGG(REPEAT(pt.emoji, p.amount), '') AS emojis,
+ SUM(p.amount) AS amount_punishments,
+ COUNT(DISTINCT p.punishment_type_id) AS amount_unique_punishments
+ FROM group_punishments p
+ LEFT JOIN punishment_types pt
+ ON pt.punishment_type_id = p.punishment_type_id
+ LEFT JOIN groups g
+ ON g.group_id = p.group_id
+ WHERE g.ow_group_id IS NOT NULL
+ GROUP BY p.user_id
+ ) p ON p.user_id = u.user_id
+ LEFT JOIN group_members gm
+ ON gm.user_id = u.user_id
+ LEFT JOIN groups g
+ ON g.group_id = gm.group_id
+ WHERE g.ow_group_id IS NOT NULL OR g.special
+ ORDER BY total_value DESC, u.first_name ASC
+ OFFSET $1
+ LIMIT $2
+ """
+ res = await conn.fetch(
+ query,
+ offset,
+ limit,
+ )
+
+ return [MinifiedLeaderboardUser(**r) for r in res]
+
+ async def get_punishments_for_leaderboard_user(
+ self,
+ user_id: UserId,
+ conn: Optional[Pool] = None,
+ ) -> list[LeaderboardPunishmentRead]:
+ async with MaybeAcquire(conn, self.db.pool) as conn:
+ query = """
+ SELECT
+ gp.*,
+ CONCAT(COALESCE(NULLIF(users.first_name, ''), users.email), ' ', users.last_name) AS created_by_name,
+ COALESCE(json_agg(pr) FILTER (WHERE pr.punishment_reaction_id IS NOT NULL), '[]') as reactions,
+ json_build_object(
+ 'punishment_type_id', pt.punishment_type_id,
+ 'name', pt.name,
+ 'value', pt.value,
+ 'emoji', pt.emoji,
+ 'created_at', pt.created_at,
+ 'created_by', pt.created_by,
+ 'updated_at', pt.updated_at
+ ) AS punishment_type
+ FROM group_punishments gp
+ LEFT JOIN punishment_types pt
+ ON pt.punishment_type_id = gp.punishment_type_id
+ LEFT JOIN (
+ SELECT pr1.*
+ FROM punishment_reactions pr1
+ JOIN group_members gm ON pr1.created_by = gm.user_id
+ GROUP BY pr1.punishment_reaction_id
+ ) pr ON pr.punishment_id = gp.punishment_id
+ LEFT JOIN users ON gp.created_by = users.user_id
+ WHERE gp.user_id = $1
+ GROUP BY gp.punishment_id, created_by_name, pt.punishment_type_id
+ ORDER BY gp.created_at DESC
+ """
+ res = await conn.fetch(
+ query,
+ user_id,
+ )
+
+ return [LeaderboardPunishmentRead(**r) for r in res]
diff --git a/backend/app/models/punishment.py b/backend/app/models/punishment.py
index 419b3136..6d0d4261 100644
--- a/backend/app/models/punishment.py
+++ b/backend/app/models/punishment.py
@@ -5,6 +5,7 @@
from datetime import datetime
from typing import Optional
+from app.models.punishment_type import PunishmentTypeRead
from pydantic import BaseModel # pylint: disable=no-name-in-module
from app.models.punishment_reaction import PunishmentReactionRead
@@ -42,6 +43,10 @@ class PunishmentRead(PunishmentOut):
user_id: UserId
+class LeaderboardPunishmentRead(PunishmentRead):
+ punishment_type: PunishmentTypeRead
+
+
class PunishmentStreaks(BaseModel):
current_streak: int
longest_streak: int
diff --git a/backend/app/models/user.py b/backend/app/models/user.py
index c470d862..1d6d1478 100644
--- a/backend/app/models/user.py
+++ b/backend/app/models/user.py
@@ -41,3 +41,10 @@ class LeaderboardPunishmentOut(PunishmentOut):
class LeaderboardUser(User):
punishments: list[LeaderboardPunishmentOut]
total_value: int
+
+
+class MinifiedLeaderboardUser(User):
+ total_value: int
+ emojis: str
+ amount_punishments: int
+ amount_unique_punishments: int
diff --git a/frontend/package.json b/frontend/package.json
index 4c81f98a..49de1849 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -36,6 +36,7 @@
"react-router-dom": "^6.15.0",
"react-simple-oauth2-login": "^0.5.4",
"react-table": "^7.8.0",
+ "tailwind-scrollbar-hide": "^1.1.7",
"zod": "^3.22.4"
},
"devDependencies": {
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index ad231807..f3b57731 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -1,9 +1,5 @@
lockfileVersion: '6.0'
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
dependencies:
'@headlessui/react':
specifier: ^1.7.10
@@ -68,6 +64,9 @@ dependencies:
react-table:
specifier: ^7.8.0
version: 7.8.0(react@18.2.0)
+ tailwind-scrollbar-hide:
+ specifier: ^1.1.7
+ version: 1.1.7
zod:
specifier: ^3.22.4
version: 3.22.4
@@ -3042,6 +3041,10 @@ packages:
tslib: 2.6.2
dev: false
+ /tailwind-scrollbar-hide@1.1.7:
+ resolution: {integrity: sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==}
+ dev: false
+
/tailwindcss@3.2.4(postcss@8.4.31):
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'}
@@ -3297,3 +3300,7 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
diff --git a/frontend/src/components/leaderboardtable/LeaderboardTable.tsx b/frontend/src/components/leaderboardtable/LeaderboardTable.tsx
index 84cfd637..bd863a8f 100644
--- a/frontend/src/components/leaderboardtable/LeaderboardTable.tsx
+++ b/frontend/src/components/leaderboardtable/LeaderboardTable.tsx
@@ -5,7 +5,7 @@ import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from "@tanst
import { SkeletonTableItem } from "./SkeletonTableItem"
import { LeaderboardTableItem } from "./LeaderboardTableItem"
-import { useEffect, useRef } from "react"
+import { useState } from "react"
interface LeaderboardTableProps {
leaderboardUsers?: LeaderboardUser[] | undefined
@@ -13,38 +13,29 @@ interface LeaderboardTableProps {
dataRefetch:
+
- {leaderboardUser.punishments.map((punishment) => - Array.from({ length: punishment.amount }, (_, i) => ( - - {punishment.punishment_type.emoji} - - )) - )} -
-- {Object.entries(punishmentTypes) - .sort(([, a], [, b]) => b.punishment.punishment_type.value - a.punishment.punishment_type.value) - .map(([_, { amount, punishment }], i) => [ - - {amount}x - {punishment.punishmentType.emoji} - , - ])} +
{emojis}
++ {sortedEmojis.map(({ emoji, count }) => ( + + {emoji} + {count} + + ))}