feat(community): KiwiCommunityStore + pseudonym helpers in per-user store
This commit is contained in:
parent
b1ed369ea6
commit
81107ed238
3 changed files with 159 additions and 0 deletions
|
|
@ -1011,3 +1011,29 @@ class Store:
|
|||
(domain, category, page, result_count),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_current_pseudonym(self, directus_user_id: str) -> str | None:
|
||||
"""Return the current community pseudonym for this user, or None if not set."""
|
||||
cur = self.conn.execute(
|
||||
"SELECT pseudonym FROM community_pseudonyms "
|
||||
"WHERE directus_user_id = ? AND is_current = 1 LIMIT 1",
|
||||
(directus_user_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row["pseudonym"] if row else None
|
||||
|
||||
def set_pseudonym(self, directus_user_id: str, pseudonym: str) -> None:
|
||||
"""Set the current community pseudonym for this user.
|
||||
|
||||
Marks any previous pseudonym as non-current (retains history for attribution).
|
||||
"""
|
||||
self.conn.execute(
|
||||
"UPDATE community_pseudonyms SET is_current = 0 WHERE directus_user_id = ?",
|
||||
(directus_user_id,),
|
||||
)
|
||||
self.conn.execute(
|
||||
"INSERT INTO community_pseudonyms (pseudonym, directus_user_id, is_current) "
|
||||
"VALUES (?, ?, 1)",
|
||||
(pseudonym, directus_user_id),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
|
|
|||
90
app/services/community/community_store.py
Normal file
90
app/services/community/community_store.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# app/services/community/community_store.py
|
||||
# MIT License
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from circuitforge_core.community import CommunityPost, SharedStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KiwiCommunityStore(SharedStore):
|
||||
"""Kiwi-specific community store: adds kiwi-domain query methods on top of SharedStore."""
|
||||
|
||||
def list_meal_plans(
|
||||
self,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
dietary_tags: list[str] | None = None,
|
||||
allergen_exclude: list[str] | None = None,
|
||||
) -> list[CommunityPost]:
|
||||
return self.list_posts(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
post_type="plan",
|
||||
dietary_tags=dietary_tags,
|
||||
allergen_exclude=allergen_exclude,
|
||||
source_product="kiwi",
|
||||
)
|
||||
|
||||
def list_outcomes(
|
||||
self,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
post_type: str | None = None,
|
||||
) -> list[CommunityPost]:
|
||||
if post_type in ("recipe_success", "recipe_blooper"):
|
||||
return self.list_posts(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
post_type=post_type,
|
||||
source_product="kiwi",
|
||||
)
|
||||
success = self.list_posts(
|
||||
limit=limit,
|
||||
offset=0,
|
||||
post_type="recipe_success",
|
||||
source_product="kiwi",
|
||||
)
|
||||
bloopers = self.list_posts(
|
||||
limit=limit,
|
||||
offset=0,
|
||||
post_type="recipe_blooper",
|
||||
source_product="kiwi",
|
||||
)
|
||||
merged = sorted(success + bloopers, key=lambda p: p.published, reverse=True)
|
||||
return merged[:limit]
|
||||
|
||||
|
||||
def get_or_create_pseudonym(
|
||||
store,
|
||||
directus_user_id: str,
|
||||
requested_name: str | None,
|
||||
) -> str:
|
||||
"""Return the user's current pseudonym, creating it if it doesn't exist.
|
||||
|
||||
If the user has an existing pseudonym, return it (ignore requested_name).
|
||||
If not, create using requested_name (must be provided for first-time setup).
|
||||
|
||||
Raises ValueError if no existing pseudonym and requested_name is None or blank.
|
||||
"""
|
||||
existing = store.get_current_pseudonym(directus_user_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
if not requested_name or not requested_name.strip():
|
||||
raise ValueError(
|
||||
"A pseudonym is required for first publish. "
|
||||
"Pass requested_name with the user's chosen display name."
|
||||
)
|
||||
|
||||
name = requested_name.strip()
|
||||
if "@" in name:
|
||||
raise ValueError(
|
||||
"Pseudonym must not contain '@' — use a display name, not an email address."
|
||||
)
|
||||
|
||||
store.set_pseudonym(directus_user_id, name)
|
||||
return name
|
||||
43
tests/services/community/test_community_store.py
Normal file
43
tests/services/community/test_community_store.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# tests/services/community/test_community_store.py
|
||||
# MIT License
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.services.community.community_store import KiwiCommunityStore, get_or_create_pseudonym
|
||||
|
||||
|
||||
def test_get_or_create_pseudonym_new_user():
|
||||
"""First-time publish: creates a new pseudonym in per-user SQLite."""
|
||||
mock_store = MagicMock()
|
||||
mock_store.get_current_pseudonym.return_value = None
|
||||
result = get_or_create_pseudonym(
|
||||
store=mock_store,
|
||||
directus_user_id="user-123",
|
||||
requested_name="PastaWitch",
|
||||
)
|
||||
mock_store.set_pseudonym.assert_called_once_with("user-123", "PastaWitch")
|
||||
assert result == "PastaWitch"
|
||||
|
||||
|
||||
def test_get_or_create_pseudonym_existing():
|
||||
"""If user already has a pseudonym, return it without creating a new one."""
|
||||
mock_store = MagicMock()
|
||||
mock_store.get_current_pseudonym.return_value = "PastaWitch"
|
||||
result = get_or_create_pseudonym(
|
||||
store=mock_store,
|
||||
directus_user_id="user-123",
|
||||
requested_name=None,
|
||||
)
|
||||
mock_store.set_pseudonym.assert_not_called()
|
||||
assert result == "PastaWitch"
|
||||
|
||||
|
||||
def test_kiwi_community_store_list_meal_plans():
|
||||
"""KiwiCommunityStore.list_meal_plans filters by post_type='plan'."""
|
||||
mock_db = MagicMock()
|
||||
store = KiwiCommunityStore(mock_db)
|
||||
with patch.object(store, "list_posts", return_value=[]) as mock_list:
|
||||
result = store.list_meal_plans(limit=10)
|
||||
mock_list.assert_called_once()
|
||||
call_kwargs = mock_list.call_args.kwargs
|
||||
assert call_kwargs.get("post_type") == "plan"
|
||||
Loading…
Reference in a new issue