feat: add backup CLI commands, AI correction/modifier services, and timezone-aware date display
DevPlace CI / test (push) Failing after 25m18s

- Add `devplace backups` CLI subcommands (list, run, prune, clear) with job enqueueing and orphan cleanup
- Introduce `BACKUPS_DIR` and `BACKUP_STAGING_DIR` config paths for backup storage
- Implement `schedule_correction` and `schedule_modification` calls in content creation, comment creation, and comment editing flows
- Add `DEFAULT_CORRECTION_PROMPT` and `DEFAULT_MODIFIER_PROMPT` config constants for AI content processing
- Document timezone-aware date display using `local_dt`/`dt_ago` Jinja globals with client-side `Intl` localization
- Update README with AI content correction/modifier support in direct messages via `@ai` inline instructions
- Add `track_action(user["uid"], "vote")` call on upvote in `apply_vote`
This commit is contained in:
2026-06-16 03:32:19 +00:00
parent e59bc2d34e
commit 15bd4ad87c
115 changed files with 5839 additions and 187 deletions
+64 -3
View File
@@ -34,6 +34,8 @@ from devplacepy.utils import (
clear_user_cache,
not_found,
generate_uid,
track_action,
build_achievements,
)
from devplacepy.responses import respond, action_result
from devplacepy.schemas import ProfileOut
@@ -45,8 +47,11 @@ from devplacepy.seo import (
profile_page_schema,
)
from devplacepy.services.audit import record as audit
from devplacepy.services.correction import schedule_correction
from devplacepy.services.ai_modifier import schedule_modification
from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
from devplacepy.routers.profile.usage import _ai_quota
from devplacepy.routers.profile.usage import _ai_quota, _correction_usage, _modifier_usage
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -139,7 +144,10 @@ async def profile_page(
posts_table = get_table("posts")
raw_posts = list(
posts_table.find(
user_uid=profile_user["uid"], deleted_at=None, order_by=["-created_at"]
user_uid=profile_user["uid"],
deleted_at=None,
order_by=["-created_at"],
_limit=10,
)
)
post_uids = [p["uid"] for p in raw_posts]
@@ -162,6 +170,9 @@ async def profile_page(
item["poll"] = polls_map.get(uid)
badges = list(get_table("badges").find(user_uid=profile_user["uid"]))
achievements = build_achievements({b["badge_name"] for b in badges})
badge_total = sum(group["total"] for group in achievements)
badge_earned = sum(group["earned"] for group in achievements)
projects = list(
get_table("projects").find(user_uid=profile_user["uid"], deleted_at=None)
)
@@ -172,7 +183,7 @@ async def profile_page(
gists = []
for g in gists_raw:
gists.append({"gist": g, "time_ago": time_ago(g["created_at"])})
posts_count = len(posts) or get_table("posts").count(
posts_count = get_table("posts").count(
user_uid=profile_user["uid"], deleted_at=None
)
@@ -234,6 +245,28 @@ async def profile_page(
viewer_is_admin = bool(current_user and current_user.get("role") == "Admin")
can_view_api_key = bool(current_user and (is_owner or viewer_is_admin))
api_key = profile_user.get("api_key", "") if can_view_api_key else ""
ai_correction_enabled = (
bool(profile_user.get("ai_correction_enabled")) if is_owner else False
)
ai_correction_sync = (
bool(profile_user.get("ai_correction_sync")) if is_owner else False
)
ai_correction_prompt = (
(profile_user.get("ai_correction_prompt") or DEFAULT_CORRECTION_PROMPT)
if is_owner
else None
)
ai_modifier_enabled = (
bool(profile_user.get("ai_modifier_enabled")) if is_owner else False
)
ai_modifier_sync = (
bool(profile_user.get("ai_modifier_sync")) if is_owner else False
)
ai_modifier_prompt = (
(profile_user.get("ai_modifier_prompt") or DEFAULT_MODIFIER_PROMPT)
if is_owner
else None
)
profile_is_admin = profile_user.get("role") == "Admin"
ai_quota = (
_ai_quota(
@@ -242,6 +275,16 @@ async def profile_page(
if (is_owner or viewer_is_admin)
else None
)
correction_usage = (
_correction_usage(profile_user["uid"], include_cost=viewer_is_admin)
if (is_owner or viewer_is_admin)
else None
)
modifier_usage = (
_modifier_usage(profile_user["uid"], include_cost=viewer_is_admin)
if (is_owner or viewer_is_admin)
else None
)
can_manage_customization = is_owner or viewer_is_admin
customization_prefs = (
get_customization_prefs("user", profile_user["uid"])
@@ -291,6 +334,9 @@ async def profile_page(
"profile_user": profile_user,
"posts": posts,
"badges": badges,
"achievements": achievements,
"badge_total": badge_total,
"badge_earned": badge_earned,
"projects": projects,
"gists": gists,
"current_tab": tab,
@@ -302,11 +348,19 @@ async def profile_page(
"media_pagination": media_pagination,
"can_view_api_key": can_view_api_key,
"api_key": api_key,
"ai_correction_enabled": ai_correction_enabled,
"ai_correction_sync": ai_correction_sync,
"ai_correction_prompt": ai_correction_prompt,
"ai_modifier_enabled": ai_modifier_enabled,
"ai_modifier_sync": ai_modifier_sync,
"ai_modifier_prompt": ai_modifier_prompt,
"can_manage_customization": can_manage_customization,
"cust_disable_global": customization_prefs["disable_global"],
"cust_disable_pagetype": customization_prefs["disable_pagetype"],
"notification_prefs": notification_prefs,
"ai_quota": ai_quota,
"correction_usage": correction_usage,
"modifier_usage": modifier_usage,
"activities": activities,
"rank": rank,
"heatmap": heatmap,
@@ -336,6 +390,8 @@ async def update_profile(request: Request, data: Annotated[ProfileForm, Form()])
["uid"],
)
clear_user_cache(user["uid"])
schedule_correction(user, "users", user["uid"], request)
schedule_modification(user, "users", user["uid"], request)
logger.info(f"Profile updated for {user['username']}")
audit.record(
@@ -349,6 +405,11 @@ async def update_profile(request: Request, data: Annotated[ProfileForm, Form()])
summary=f"{user['username']} updated their profile",
links=[audit.target("user", user["uid"], user["username"])],
)
if any(
value.strip()
for value in (data.bio, data.location, data.git_link, data.website)
):
track_action(user["uid"], "profile")
return action_result(request, f"/profile/{user['username']}")