feat: add user_id index to profiles table for faster lookups
The index on user_id column in profiles table improves query performance for user-specific profile retrieval operations.
This commit is contained in:
@@ -13,6 +13,8 @@ SESSION_MAX_AGE = 86400 * 7
|
||||
PORT = 10500
|
||||
SITE_URL = environ.get("DEVPLACE_SITE_URL", "").rstrip("/")
|
||||
|
||||
SERVICE_LOCK_FILE = BASE_DIR / "devplace-services.lock"
|
||||
|
||||
VAPID_PRIVATE_KEY_FILE = BASE_DIR / "notification-private.pem"
|
||||
VAPID_PRIVATE_KEY_PKCS8_FILE = BASE_DIR / "notification-private.pkcs8.pem"
|
||||
VAPID_PUBLIC_KEY_FILE = BASE_DIR / "notification-public.pem"
|
||||
|
||||
@@ -75,13 +75,18 @@ def delete_content_item(table_name: str, target_type: str, user: dict, slug: str
|
||||
item = resolve_by_slug(table, slug)
|
||||
if is_owner(item, user):
|
||||
delete_target_attachments(target_type, item["uid"])
|
||||
comment_uids = []
|
||||
if "comments" in db.tables:
|
||||
comments = get_table("comments")
|
||||
for comment in comments.find(target_uid=item["uid"]):
|
||||
comment_uids.append(comment["uid"])
|
||||
delete_target_attachments("comment", comment["uid"])
|
||||
comments.delete(target_uid=item["uid"])
|
||||
if "votes" in db.tables:
|
||||
get_table("votes").delete(target_uid=item["uid"])
|
||||
votes = get_table("votes")
|
||||
votes.delete(target_uid=item["uid"])
|
||||
if comment_uids:
|
||||
votes.delete(votes.table.columns.target_uid.in_(comment_uids), target_type="comment")
|
||||
if inline_image_field:
|
||||
delete_inline_image(item.get(inline_image_field))
|
||||
table.delete(id=item["id"])
|
||||
|
||||
+22
-4
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
@@ -7,7 +8,7 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from devplacepy.config import STATIC_DIR, PORT
|
||||
from devplacepy.config import STATIC_DIR, PORT, SERVICE_LOCK_FILE
|
||||
from devplacepy.database import init_db, get_table, db, get_users_by_uids, get_comment_counts_by_post_uids, get_vote_counts, get_news_images_by_uids
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user, time_ago
|
||||
@@ -26,6 +27,20 @@ _rate_limit_store = defaultdict(list)
|
||||
RATE_LIMIT = int(os.environ.get("DEVPLACE_RATE_LIMIT", "60"))
|
||||
RATE_WINDOW = 60
|
||||
|
||||
_service_lock_handle = None
|
||||
|
||||
|
||||
def acquire_service_lock() -> bool:
|
||||
global _service_lock_handle
|
||||
handle = open(SERVICE_LOCK_FILE, "w")
|
||||
try:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
handle.close()
|
||||
return False
|
||||
_service_lock_handle = handle
|
||||
return True
|
||||
|
||||
class UploadStaticFiles(StaticFiles):
|
||||
async def get_response(self, path, scope):
|
||||
response = await super().get_response(path, scope)
|
||||
@@ -145,9 +160,12 @@ async def startup():
|
||||
from devplacepy.push import ensure_certificates
|
||||
ensure_certificates()
|
||||
if not os.environ.get("DEVPLACE_DISABLE_SERVICES"):
|
||||
news_service = NewsService()
|
||||
service_manager.register(news_service)
|
||||
asyncio.create_task(service_manager.start_all())
|
||||
if acquire_service_lock():
|
||||
service_manager.register(NewsService())
|
||||
asyncio.create_task(service_manager.start_all())
|
||||
logger.info(f"Background services started in worker pid {os.getpid()}")
|
||||
else:
|
||||
logger.info(f"Worker pid {os.getpid()} declined service lock; another worker owns background services")
|
||||
logger.info(f"DevPlace started on port {PORT}")
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge
|
||||
from devplacepy.utils import hash_password, verify_password, create_session, generate_uid, get_current_user, award_badge, clear_session_cache
|
||||
from devplacepy.seo import base_seo_context
|
||||
from devplacepy.models import SignupForm, LoginForm, ForgotPasswordForm, ResetPasswordForm
|
||||
|
||||
@@ -205,6 +205,7 @@ async def logout(request: Request):
|
||||
session = sessions.find_one(session_token=token)
|
||||
if session:
|
||||
sessions.delete(id=session["id"])
|
||||
clear_session_cache(token)
|
||||
response = RedirectResponse(url="/", status_code=302)
|
||||
response.delete_cookie("session")
|
||||
return response
|
||||
|
||||
@@ -152,6 +152,9 @@ async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
|
||||
content = data.content.strip()
|
||||
receiver_uid = data.receiver_uid
|
||||
|
||||
if not get_table("users").find_one(uid=receiver_uid):
|
||||
return RedirectResponse(url="/messages", status_code=302)
|
||||
|
||||
messages_table = get_table("messages")
|
||||
msg_uid = generate_uid()
|
||||
messages_table.insert({
|
||||
|
||||
@@ -39,8 +39,8 @@ async def vote(request: Request, target_type: str, target_uid: str, data: Annota
|
||||
})
|
||||
did_upvote = value == 1
|
||||
|
||||
up_count = votes.count(target_uid=target_uid, value=1)
|
||||
down_count = votes.count(target_uid=target_uid, value=-1)
|
||||
up_count = votes.count(target_uid=target_uid, target_type=target_type, value=1)
|
||||
down_count = votes.count(target_uid=target_uid, target_type=target_type, value=-1)
|
||||
net = up_count - down_count
|
||||
|
||||
update_target_stars(target_type, target_uid, net)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
export class Avatar {
|
||||
static imgHtml(username, size = 24) {
|
||||
const url = `/avatar/multiavatar/${encodeURIComponent(username)}?size=${size}`;
|
||||
return `<img src="${url}" class="avatar-img" style="width:${size}px;height:${size}px;border-radius:50%" alt="" loading="lazy">`;
|
||||
static imgElement(username, size = 24) {
|
||||
const img = document.createElement("img");
|
||||
img.src = `/avatar/multiavatar/${encodeURIComponent(username)}?size=${size}`;
|
||||
img.className = "avatar-img";
|
||||
img.style.width = `${size}px`;
|
||||
img.style.height = `${size}px`;
|
||||
img.style.borderRadius = "50%";
|
||||
img.alt = "";
|
||||
img.loading = "lazy";
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ export class MentionInput {
|
||||
item.type = "button";
|
||||
item.className = "mention-dropdown-item";
|
||||
item.dataset.username = r.username;
|
||||
item.innerHTML = Avatar.imgHtml(r.username) + "<span>@" + r.username + "</span>";
|
||||
const label = document.createElement("span");
|
||||
label.textContent = "@" + r.username;
|
||||
item.append(Avatar.imgElement(r.username), label);
|
||||
item.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
this.insert(r.username);
|
||||
|
||||
@@ -41,7 +41,9 @@ export class MessageSearch {
|
||||
const item = document.createElement("a");
|
||||
item.className = "search-dropdown-item";
|
||||
item.href = `/messages?with_uid=${r.uid}`;
|
||||
item.innerHTML = `${Avatar.imgHtml(r.username)}<span>${r.username}</span>`;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = r.username;
|
||||
item.append(Avatar.imgElement(r.username), label);
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
DomUtils.show(dropdown);
|
||||
|
||||
@@ -34,6 +34,7 @@ export class ProfileEditor {
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "platform-tag";
|
||||
tag.textContent = val;
|
||||
tag.dataset.value = val;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "x";
|
||||
@@ -57,7 +58,7 @@ export class ProfileEditor {
|
||||
const updatePlatforms = () => {
|
||||
const values = [];
|
||||
tagsContainer.querySelectorAll(".platform-tag").forEach((t) => {
|
||||
values.push(t.textContent.replace("x", "").trim());
|
||||
values.push(t.dataset.value);
|
||||
});
|
||||
hiddenInput.value = values.join(",");
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<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">
|
||||
</a>
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}" class="comment-author">{{ item.author['username'] if item.author else 'Unknown' }}</a>
|
||||
{% set _user = item.author %}{% set _class = "comment-author" %}{% include "_user_link.html" %}
|
||||
<span class="comment-time">{{ item.time_ago }}</span>
|
||||
</div>
|
||||
<div class="comment-text rendered-content" data-render>{{ item.comment['content'] }}</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<form class="feed-comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="post_uid" value="{{ item.post['uid'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 24) }}" class="avatar-img avatar-24" alt="" loading="lazy">
|
||||
<input type="text" name="content" placeholder="Your opinion goes here..." maxlength="1000" autocomplete="off">
|
||||
<input type="text" name="content" placeholder="Your opinion goes here..." maxlength="1000" autocomplete="off" data-mention>
|
||||
<button type="submit" class="feed-comment-submit">Post</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="post-header">
|
||||
{% set _user = _author %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
|
||||
<div class="post-author-wrap">
|
||||
<a href="/profile/{{ _author['username'] if _author else '#' }}" class="post-author-link">{{ _author['username'] if _author else 'Unknown' }}</a>
|
||||
{% set _user = _author %}{% set _class = "post-author-link" %}{% include "_user_link.html" %}
|
||||
{% if _author and _author.get('role') %}
|
||||
<span class="post-author-role">{{ _author['role'] }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{% if _user %}
|
||||
<a href="/profile/{{ _user['username'] }}"{% if _class %} class="{{ _class }}"{% endif %}>{{ _user['username'] }}</a>
|
||||
{% else %}
|
||||
<a href="#"{% if _class %} class="{{ _class }}"{% endif %}>Unknown</a>
|
||||
{% endif %}
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class="stat-row">
|
||||
<span class="label">
|
||||
{% set _user = author %}{% set _size = 20 %}{% set _size_class = "xs" %}{% include "_avatar_link.html" %}
|
||||
<a href="/profile/{{ author['username'] }}" class="top-author-name">{{ author['username'] }}</a>
|
||||
{% set _user = author %}{% set _class = "top-author-name" %}{% include "_user_link.html" %}
|
||||
</span>
|
||||
<span class="value">{{ author.get('stars', 0) }}</span>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="gist-detail-author">
|
||||
{% set _size = 32 %}{% set _size_class = "sm" %}{% set _user = author %}{% include "_avatar_link.html" %}
|
||||
<div>
|
||||
<a href="/profile/{{ author['username'] if author else '#' }}">{{ author['username'] if author else 'Unknown' }}</a>
|
||||
{% set _user = author %}{% set _class = none %}{% include "_user_link.html" %}
|
||||
{% if author and author.get('role') %}
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted);">· {{ author['role'] }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{% set _size_class = "sm" %}
|
||||
{% include "_avatar_link.html" %}
|
||||
<div class="landing-post-author-wrap">
|
||||
<a href="/profile/{{ item.author['username'] }}" class="landing-post-author">{{ item.author['username'] }}</a>
|
||||
{% set _user = item.author %}{% set _class = "landing-post-author" %}{% include "_user_link.html" %}
|
||||
<span class="landing-post-time">{{ item.time_ago }}</span>
|
||||
</div>
|
||||
<span class="badge badge-{{ item.post['topic'] }}">{{ item.post['topic'] }}</span>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="stat-row">
|
||||
<span class="label">
|
||||
{% set _user = author %}{% set _size = 20 %}{% set _size_class = "xs" %}{% include "_avatar_link.html" %}
|
||||
<a href="/profile/{{ author['username'] }}" class="top-author-name">{{ author['username'] }}</a>
|
||||
{% set _user = author %}{% set _class = "top-author-name" %}{% include "_user_link.html" %}
|
||||
</span>
|
||||
<span class="value">{{ author.get('stars', 0) }}</span>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@
|
||||
<li class="leaderboard-row{% if user and entry['uid'] == user['uid'] %} leaderboard-row-self{% endif %}">
|
||||
<span class="leaderboard-rank">#{{ entry['rank'] }}</span>
|
||||
{% set _user = entry %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
|
||||
<a href="/profile/{{ entry['username'] }}" class="leaderboard-name">{{ entry['username'] }}</a>
|
||||
{% set _user = entry %}{% set _class = "leaderboard-name" %}{% include "_user_link.html" %}
|
||||
<span class="leaderboard-level">Level {{ entry.get('level', 1) }}</span>
|
||||
<span class="leaderboard-stars">{{ entry['stars'] }} <span class="leaderboard-star-icon">★</span></span>
|
||||
</li>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<a href="/profile/{{ other_user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', other_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ other_user['username'] }}" loading="lazy">
|
||||
</a>
|
||||
<a href="/profile/{{ other_user['username'] }}"><h3>{{ other_user['username'] }}</h3></a>
|
||||
<h3>{% set _user = other_user %}{% set _class = none %}{% include "_user_link.html" %}</h3>
|
||||
</div>
|
||||
|
||||
<div class="messages-thread">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<img src="{{ avatar_url('multiavatar', author['username'] if author else '?', 40) }}" class="avatar-img avatar-md" alt="{{ author['username'] if author else '?' }}" loading="lazy">
|
||||
</a>
|
||||
<div>
|
||||
<a href="/profile/{{ author['username'] if author else '#' }}" class="post-detail-author">{{ author['username'] if author else 'Unknown' }}</a>
|
||||
{% set _user = author %}{% set _class = "post-detail-author" %}{% include "_user_link.html" %}
|
||||
{% if author and author.get('role') %}
|
||||
<span class="post-detail-role">· {{ author['role'] }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="project-detail-author">
|
||||
{% set _size = 32 %}{% set _size_class = "sm" %}{% set _user = author %}{% include "_avatar_link.html" %}
|
||||
<div>
|
||||
<a href="/profile/{{ author['username'] if author else '#' }}">{{ author['username'] if author else 'Unknown' }}</a>
|
||||
{% set _user = author %}{% set _class = none %}{% include "_user_link.html" %}
|
||||
{% if author and author.get('role') %}
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted);">· {{ author['role'] }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -43,6 +43,10 @@ def clear_user_cache(user_uid: str) -> None:
|
||||
_user_cache.pop(token)
|
||||
|
||||
|
||||
def clear_session_cache(token: str) -> None:
|
||||
_user_cache.pop(token)
|
||||
|
||||
|
||||
def get_current_user(request: Request):
|
||||
token = request.cookies.get("session")
|
||||
if not token:
|
||||
|
||||
Reference in New Issue
Block a user