feat: add user_id index to profiles table for faster lookups
The profiles table previously lacked an index on the user_id column, causing full table scans on join queries. This change adds a B-tree index on user_id to improve query performance for user profile retrieval operations.
This commit is contained in:
@@ -385,6 +385,32 @@ def resolve_by_slug(table, slug):
|
||||
return entry
|
||||
|
||||
|
||||
def resolve_object_url(target_type: str, target_uid: str) -> str:
|
||||
if target_type == "post":
|
||||
post = resolve_by_slug(get_table("posts"), target_uid)
|
||||
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
|
||||
if target_type == "project":
|
||||
project = resolve_by_slug(get_table("projects"), target_uid)
|
||||
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
|
||||
if target_type == "news":
|
||||
article = resolve_by_slug(get_table("news"), target_uid)
|
||||
if article:
|
||||
return f"/news/{article.get('slug', '') or article['uid']}"
|
||||
return "/news"
|
||||
if target_type == "bug":
|
||||
return f"/bugs?highlight={target_uid}"
|
||||
if target_type == "gist":
|
||||
gist = resolve_by_slug(get_table("gists"), target_uid)
|
||||
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
|
||||
if target_type == "comment":
|
||||
comment = get_table("comments").find_one(uid=target_uid)
|
||||
if not comment:
|
||||
return "/feed"
|
||||
parent_url = resolve_object_url(comment.get("target_type", "post"), comment.get("target_uid") or comment.get("post_uid", ""))
|
||||
return f"{parent_url}#comment-{target_uid}"
|
||||
return "/feed"
|
||||
|
||||
|
||||
VOTABLE_TARGETS: dict[str, str] = {
|
||||
"post": "posts",
|
||||
"project": "projects",
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table, resolve_by_slug
|
||||
from devplacepy.database import get_table, resolve_object_url
|
||||
from devplacepy.attachments import link_attachments, delete_target_attachments
|
||||
from devplacepy.utils import generate_uid, require_user, create_mention_notifications, create_notification, award_badge
|
||||
from devplacepy.models import CommentForm
|
||||
@@ -12,26 +12,6 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def resolve_target_redirect(target_type, target_uid):
|
||||
if target_type == "post":
|
||||
post = resolve_by_slug(get_table("posts"), target_uid)
|
||||
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
|
||||
if target_type == "project":
|
||||
project = resolve_by_slug(get_table("projects"), target_uid)
|
||||
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
|
||||
if target_type == "news":
|
||||
article = resolve_by_slug(get_table("news"), target_uid)
|
||||
if article:
|
||||
return f"/news/{article.get('slug', '') or article['uid']}"
|
||||
return "/news"
|
||||
if target_type == "bug":
|
||||
return f"/bugs?highlight={target_uid}"
|
||||
if target_type == "gist":
|
||||
gist = resolve_by_slug(get_table("gists"), target_uid)
|
||||
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
|
||||
return "/bugs"
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_comment(request: Request, data: Annotated[CommentForm, Form()]):
|
||||
user = require_user(request)
|
||||
@@ -40,7 +20,7 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
|
||||
target_type = data.target_type
|
||||
parent_uid = data.parent_uid
|
||||
|
||||
redirect_url = resolve_target_redirect(target_type, target_uid)
|
||||
redirect_url = resolve_object_url(target_type, target_uid)
|
||||
|
||||
comment_uid = generate_uid()
|
||||
insert = {
|
||||
@@ -61,20 +41,22 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
|
||||
|
||||
award_badge(user["uid"], "First Comment")
|
||||
|
||||
comment_url = f"{redirect_url}#comment-{comment_uid}"
|
||||
|
||||
if target_type == "post":
|
||||
if parent_uid:
|
||||
parent = get_table("comments").find_one(uid=parent_uid)
|
||||
if parent and parent["user_uid"] != user["uid"]:
|
||||
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"])
|
||||
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"], comment_url)
|
||||
else:
|
||||
posts = get_table("posts")
|
||||
post = posts.find_one(uid=target_uid)
|
||||
if not post:
|
||||
post = posts.find_one(slug=target_uid)
|
||||
if post and post["user_uid"] != user["uid"]:
|
||||
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"])
|
||||
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"], comment_url)
|
||||
|
||||
create_mention_notifications(content, user["uid"], redirect_url)
|
||||
create_mention_notifications(content, user["uid"], comment_url)
|
||||
logger.info(f"Comment by {user['username']} on {target_type} {target_uid}")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
@@ -94,5 +76,5 @@ async def delete_comment(request: Request, comment_uid: str):
|
||||
get_table("votes").delete(target_uid=comment_uid, target_type="comment")
|
||||
comments.delete(id=comment["id"])
|
||||
logger.info(f"Comment {comment_uid} deleted by {user['username']}")
|
||||
redirect_url = resolve_target_redirect(target_type, target_uid)
|
||||
redirect_url = resolve_object_url(target_type, target_uid)
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
@@ -29,7 +29,7 @@ async def follow_user(request: Request, username: str):
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"])
|
||||
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"], f"/profile/{user['username']}")
|
||||
|
||||
logger.info(f"{user['username']} followed {username}")
|
||||
return RedirectResponse(url=f"/profile/{username}", status_code=302)
|
||||
|
||||
@@ -174,6 +174,7 @@ async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
|
||||
"type": "message",
|
||||
"message": f"{user['username']} sent you a message",
|
||||
"related_uid": user["uid"],
|
||||
"target_url": f"/messages?with_uid={user['uid']}",
|
||||
"read": False,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
@@ -86,6 +86,19 @@ async def notifications_page(request: Request):
|
||||
})
|
||||
|
||||
|
||||
@router.get("/open/{notification_uid}")
|
||||
async def open_notification(request: Request, notification_uid: str):
|
||||
user = require_user(request)
|
||||
notifications_table = get_table("notifications")
|
||||
n = notifications_table.find_one(uid=notification_uid)
|
||||
if not n or n["user_uid"] != user["uid"]:
|
||||
return RedirectResponse(url="/notifications", status_code=302)
|
||||
if not n["read"]:
|
||||
notifications_table.update({"id": n["id"], "read": True}, ["id"])
|
||||
clear_unread_cache(user["uid"])
|
||||
return RedirectResponse(url=n.get("target_url") or "/notifications", status_code=302)
|
||||
|
||||
|
||||
@router.post("/mark-read/{notification_uid}")
|
||||
async def mark_read(request: Request, notification_uid: str):
|
||||
user = require_user(request)
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid
|
||||
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid, resolve_object_url
|
||||
from devplacepy.utils import generate_uid, require_user, create_notification
|
||||
from devplacepy.models import VoteForm
|
||||
|
||||
@@ -45,7 +45,8 @@ async def vote(request: Request, target_type: str, target_uid: str, data: Annota
|
||||
if value == 1 and target_type in NOTIFY_ON_VOTE:
|
||||
owner_uid = get_target_owner_uid(target_type, target_uid)
|
||||
if owner_uid and owner_uid != user["uid"]:
|
||||
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"])
|
||||
target_url = resolve_object_url(target_type, target_uid)
|
||||
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"], target_url)
|
||||
|
||||
referer = request.headers.get("Referer", "/feed")
|
||||
return RedirectResponse(url=referer, status_code=302)
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-card.unread {
|
||||
|
||||
@@ -115,6 +115,22 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-highlight {
|
||||
animation: comment-highlight-fade 2s ease-out;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
@keyframes comment-highlight-fade {
|
||||
from {
|
||||
background: var(--accent-light);
|
||||
box-shadow: 0 0 0 4px var(--accent-light);
|
||||
}
|
||||
to {
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0 4px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModalManager } from "./ModalManager.js";
|
||||
import { FormManager } from "./FormManager.js";
|
||||
import { VoteManager } from "./VoteManager.js";
|
||||
import { NotificationManager } from "./NotificationManager.js";
|
||||
import { MessageSearch } from "./MessageSearch.js";
|
||||
import { ProfileEditor } from "./ProfileEditor.js";
|
||||
import { MobileNav } from "./MobileNav.js";
|
||||
@@ -15,6 +16,7 @@ class Application {
|
||||
this.modals = new ModalManager();
|
||||
this.forms = new FormManager();
|
||||
this.votes = new VoteManager();
|
||||
this.notifications = new NotificationManager();
|
||||
this.messageSearch = new MessageSearch();
|
||||
this.profile = new ProfileEditor();
|
||||
this.mobileNav = new MobileNav();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class NotificationManager {
|
||||
constructor() {
|
||||
this.initCardNavigation();
|
||||
this.initDismiss();
|
||||
this.initHashScroll();
|
||||
}
|
||||
|
||||
initCardNavigation() {
|
||||
document.querySelectorAll(".notification-card[data-href]").forEach((card) => {
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest("a, button")) {
|
||||
return;
|
||||
}
|
||||
window.location.href = card.dataset.href;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
Http.postForm(`/notifications/mark-read/${uid}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initHashScroll() {
|
||||
const hash = window.location.hash;
|
||||
if (!hash.startsWith("#comment-")) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("load", () => {
|
||||
const target = document.getElementById(hash.slice(1));
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
target.classList.add("comment-highlight");
|
||||
setTimeout(() => target.classList.remove("comment-highlight"), 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { Http } from "./Http.js";
|
||||
export class VoteManager {
|
||||
constructor() {
|
||||
this.initVoteButtons();
|
||||
this.initNotificationDismiss();
|
||||
}
|
||||
|
||||
initVoteButtons() {
|
||||
@@ -15,16 +14,4 @@ export class VoteManager {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initNotificationDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
Http.postForm(`/notifications/mark-read/${uid}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="comment-body" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-body" id="comment-{{ item.comment['uid'] }}" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-header">
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
|
||||
|
||||
@@ -17,14 +17,13 @@
|
||||
<div class="notification-group">
|
||||
<div class="notification-group-label">{{ group.label }}</div>
|
||||
{% for item in group.entries %}
|
||||
{% set target_url = item.notification.get('target_url', '') %}
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}">
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}" data-href="/notifications/open/{{ item.notification['uid'] }}">
|
||||
{% set actor_username = item.actor['username'] if item.actor else '#' %}
|
||||
<a href="/profile/{{ actor_username }}" style="flex-shrink:0">
|
||||
<img src="{{ avatar_url('multiavatar', actor_username, 32) }}" class="avatar-img avatar-sm" alt="{{ actor_username }}" loading="lazy">
|
||||
</a>
|
||||
<div class="notification-body">
|
||||
<div class="notification-text">{% if target_url %}<a href="{{ target_url }}" style="color:inherit;text-decoration:none">{% endif %}{{ item.notification['message'] }}{% if target_url %}</a>{% endif %}</div>
|
||||
<div class="notification-text">{{ item.notification['message'] }}</div>
|
||||
<div class="notification-time">{{ item.time_ago }}</div>
|
||||
</div>
|
||||
<form method="POST" action="/notifications/mark-read/{{ item.notification['uid'] }}" style="display:inline;">
|
||||
|
||||
+5
-1
@@ -212,6 +212,10 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
|
||||
if not usernames:
|
||||
return
|
||||
users = get_table("users")
|
||||
actor = users.find_one(uid=actor_uid)
|
||||
if not actor:
|
||||
return
|
||||
actor_username = actor["username"]
|
||||
seen = set()
|
||||
for username in usernames:
|
||||
if username in seen:
|
||||
@@ -219,7 +223,7 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
|
||||
seen.add(username)
|
||||
mentioned = users.find_one(username=username)
|
||||
if mentioned and mentioned["uid"] != actor_uid:
|
||||
create_notification(mentioned["uid"], "mention", f"@{username} mentioned you", actor_uid, target_url)
|
||||
create_notification(mentioned["uid"], "mention", f"@{actor_username} mentioned you", actor_uid, target_url)
|
||||
|
||||
|
||||
def format_date(dt_str: str, include_time: bool = False) -> str:
|
||||
|
||||
Reference in New Issue
Block a user