276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
import os
|
|
from typing import List, Optional, Dict, Any
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
|
|
def get_db_connection():
|
|
"""Get database connection"""
|
|
return psycopg2.connect(
|
|
host=os.getenv("DB_HOST", "localhost"),
|
|
port=int(os.getenv("DB_PORT", "5432")),
|
|
database=os.getenv("DB_NAME", "recipes_db"),
|
|
user=os.getenv("DB_USER", "recipes_user"),
|
|
password=os.getenv("DB_PASSWORD", "recipes_password"),
|
|
)
|
|
|
|
|
|
def create_grocery_list(owner_id: int, name: str, items: List[str] = None) -> Dict[str, Any]:
|
|
"""Create a new grocery list"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
items = items or []
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO grocery_lists (owner_id, name, items)
|
|
VALUES (%s, %s, %s)
|
|
RETURNING id, name, items, owner_id, created_at, updated_at
|
|
""",
|
|
(owner_id, name, items)
|
|
)
|
|
grocery_list = cur.fetchone()
|
|
conn.commit()
|
|
return dict(grocery_list)
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def get_user_grocery_lists(user_id: int) -> List[Dict[str, Any]]:
|
|
"""Get all grocery lists owned by or shared with a user"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
SELECT DISTINCT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at,
|
|
u.display_name as owner_display_name,
|
|
CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit,
|
|
CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner
|
|
FROM grocery_lists gl
|
|
LEFT JOIN users u ON gl.owner_id = u.id
|
|
LEFT JOIN grocery_list_shares gls ON gl.id = gls.list_id AND gls.shared_with_user_id = %s
|
|
WHERE gl.owner_id = %s OR gls.shared_with_user_id = %s
|
|
ORDER BY gl.updated_at DESC
|
|
""",
|
|
(user_id, user_id, user_id, user_id, user_id)
|
|
)
|
|
lists = cur.fetchall()
|
|
return [dict(row) for row in lists]
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def get_grocery_list_by_id(list_id: int, user_id: int) -> Optional[Dict[str, Any]]:
|
|
"""Get a specific grocery list if user has access"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
SELECT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at,
|
|
u.display_name as owner_display_name,
|
|
CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit,
|
|
CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner
|
|
FROM grocery_lists gl
|
|
LEFT JOIN users u ON gl.owner_id = u.id
|
|
LEFT JOIN grocery_list_shares gls ON gl.id = gls.list_id AND gls.shared_with_user_id = %s
|
|
WHERE gl.id = %s AND (gl.owner_id = %s OR gls.shared_with_user_id = %s)
|
|
""",
|
|
(user_id, user_id, user_id, list_id, user_id, user_id)
|
|
)
|
|
grocery_list = cur.fetchone()
|
|
return dict(grocery_list) if grocery_list else None
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def update_grocery_list(list_id: int, name: str = None, items: List[str] = None) -> Optional[Dict[str, Any]]:
|
|
"""Update a grocery list"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
updates = []
|
|
params = []
|
|
|
|
if name is not None:
|
|
updates.append("name = %s")
|
|
params.append(name)
|
|
|
|
if items is not None:
|
|
updates.append("items = %s")
|
|
params.append(items)
|
|
|
|
if not updates:
|
|
return None
|
|
|
|
updates.append("updated_at = CURRENT_TIMESTAMP")
|
|
params.append(list_id)
|
|
|
|
query = f"UPDATE grocery_lists SET {', '.join(updates)} WHERE id = %s RETURNING id, name, items, owner_id, created_at, updated_at"
|
|
|
|
cur.execute(query, params)
|
|
grocery_list = cur.fetchone()
|
|
conn.commit()
|
|
return dict(grocery_list) if grocery_list else None
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def delete_grocery_list(list_id: int) -> bool:
|
|
"""Delete a grocery list"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor()
|
|
try:
|
|
cur.execute("DELETE FROM grocery_lists WHERE id = %s", (list_id,))
|
|
deleted = cur.rowcount > 0
|
|
conn.commit()
|
|
return deleted
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def share_grocery_list(list_id: int, shared_with_user_id: int, can_edit: bool = False) -> Dict[str, Any]:
|
|
"""Share a grocery list with another user or update existing share permissions"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO grocery_list_shares (list_id, shared_with_user_id, can_edit)
|
|
VALUES (%s, %s, %s)
|
|
ON CONFLICT (list_id, shared_with_user_id)
|
|
DO UPDATE SET can_edit = EXCLUDED.can_edit, shared_at = CURRENT_TIMESTAMP
|
|
RETURNING id, list_id, shared_with_user_id, can_edit, shared_at
|
|
""",
|
|
(list_id, shared_with_user_id, can_edit)
|
|
)
|
|
share = cur.fetchone()
|
|
conn.commit()
|
|
return dict(share)
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def update_share_permission(list_id: int, shared_with_user_id: int, can_edit: bool) -> Optional[Dict[str, Any]]:
|
|
"""Update edit permission for an existing share"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
UPDATE grocery_list_shares
|
|
SET can_edit = %s
|
|
WHERE list_id = %s AND shared_with_user_id = %s
|
|
RETURNING id, list_id, shared_with_user_id, can_edit, shared_at
|
|
""",
|
|
(can_edit, list_id, shared_with_user_id)
|
|
)
|
|
share = cur.fetchone()
|
|
conn.commit()
|
|
return dict(share) if share else None
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def unshare_grocery_list(list_id: int, user_id: int) -> bool:
|
|
"""Remove sharing access for a user"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor()
|
|
try:
|
|
cur.execute(
|
|
"DELETE FROM grocery_list_shares WHERE list_id = %s AND shared_with_user_id = %s",
|
|
(list_id, user_id)
|
|
)
|
|
deleted = cur.rowcount > 0
|
|
conn.commit()
|
|
return deleted
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def get_grocery_list_shares(list_id: int) -> List[Dict[str, Any]]:
|
|
"""Get all users a grocery list is shared with"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
SELECT gls.id, gls.list_id, gls.shared_with_user_id, gls.can_edit, gls.shared_at,
|
|
u.username, u.display_name, u.email
|
|
FROM grocery_list_shares gls
|
|
JOIN users u ON gls.shared_with_user_id = u.id
|
|
WHERE gls.list_id = %s
|
|
ORDER BY gls.shared_at DESC
|
|
""",
|
|
(list_id,)
|
|
)
|
|
shares = cur.fetchall()
|
|
return [dict(row) for row in shares]
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def search_users(query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
"""Search users by username or display_name for autocomplete"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
cur.execute(
|
|
"""
|
|
SELECT id, username, display_name, email
|
|
FROM users
|
|
WHERE username ILIKE %s OR display_name ILIKE %s
|
|
ORDER BY username
|
|
LIMIT %s
|
|
""",
|
|
(f"%{query}%", f"%{query}%", limit)
|
|
)
|
|
users = cur.fetchall()
|
|
return [dict(row) for row in users]
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
def toggle_grocery_list_pin(list_id: int, user_id: int) -> Optional[Dict[str, Any]]:
|
|
"""Toggle pin status for a grocery list (owner only)"""
|
|
conn = get_db_connection()
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
try:
|
|
# Check if user is owner
|
|
cur.execute(
|
|
"SELECT id, is_pinned FROM grocery_lists WHERE id = %s AND owner_id = %s",
|
|
(list_id, user_id)
|
|
)
|
|
result = cur.fetchone()
|
|
if not result:
|
|
return None
|
|
|
|
# Toggle pin status
|
|
new_pin_status = not result["is_pinned"]
|
|
cur.execute(
|
|
"""
|
|
UPDATE grocery_lists
|
|
SET is_pinned = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = %s
|
|
RETURNING id, name, items, owner_id, is_pinned, created_at, updated_at
|
|
""",
|
|
(new_pin_status, list_id)
|
|
)
|
|
updated = cur.fetchone()
|
|
conn.commit()
|
|
return dict(updated) if updated else None
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|