feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
This commit is contained in:
@@ -594,3 +594,77 @@ def test_text_search_clause_still_matches_text_fields(local_db):
|
||||
assert clause is not None
|
||||
rows = list(posts.find(clause, deleted_at=None))
|
||||
assert post_uid in {r["uid"] for r in rows}
|
||||
|
||||
|
||||
def _notification(user_uid, target_url, read=False):
|
||||
uid = generate_uid()
|
||||
get_table("notifications").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"user_uid": user_uid,
|
||||
"type": "comment",
|
||||
"message": "someone replied",
|
||||
"related_uid": generate_uid(),
|
||||
"target_url": target_url,
|
||||
"read": read,
|
||||
"created_at": _now_db_helpers(),
|
||||
}
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
def _is_read(uid):
|
||||
return bool(get_table("notifications").find_one(uid=uid)["read"])
|
||||
|
||||
|
||||
def test_mark_notifications_marks_exact_url(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
user = _user_db_helpers()
|
||||
uid = _notification(user, "/posts/abc-post")
|
||||
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
|
||||
assert _is_read(uid) is True
|
||||
|
||||
|
||||
def test_mark_notifications_matches_anchor_prefix(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
user = _user_db_helpers()
|
||||
uid = _notification(user, "/posts/abc-post#comment-5")
|
||||
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
|
||||
assert _is_read(uid) is True
|
||||
|
||||
|
||||
def test_mark_notifications_leaves_other_targets(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
user = _user_db_helpers()
|
||||
keep = _notification(user, "/posts/other-post")
|
||||
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0
|
||||
assert _is_read(keep) is False
|
||||
|
||||
|
||||
def test_mark_notifications_only_unread_counted(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
user = _user_db_helpers()
|
||||
_notification(user, "/posts/abc-post", read=True)
|
||||
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0
|
||||
|
||||
|
||||
def test_mark_notifications_empty_args(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
user = _user_db_helpers()
|
||||
assert mark_notifications_read_by_target("", "/posts/abc") == 0
|
||||
assert mark_notifications_read_by_target(user, "") == 0
|
||||
|
||||
|
||||
def test_mark_notifications_scoped_per_user(local_db):
|
||||
from devplacepy.database import mark_notifications_read_by_target
|
||||
|
||||
owner = _user_db_helpers()
|
||||
other = _user_db_helpers()
|
||||
other_uid = _notification(other, "/posts/abc-post")
|
||||
assert mark_notifications_read_by_target(owner, "/posts/abc-post") == 0
|
||||
assert _is_read(other_uid) is False
|
||||
|
||||
Reference in New Issue
Block a user