🚀 Major Enhancement: Complete AI-Powered LifeRPG Platform with Git LFS
✨ New Features: - AI-powered habit creation with natural language processing - HuggingFace transformers integration for sentiment analysis (tracked via Git LFS) - Advanced predictive analytics and behavioral insights - Voice & image input capabilities for hands-free habit tracking - Real-time notifications and community features - Plugin system with extensible architecture 🔧 Technical Improvements: - Comprehensive FastAPI backend with 30+ endpoints - React frontend with PWA capabilities - Advanced authentication with 2FA support - RBAC authorization system - Comprehensive security features (CSRF, rate limiting, audit logging) - Database migrations and health monitoring - Docker containerization support - Git LFS configured for large AI model files (2+ GB) 📚 Documentation & DevOps: - Complete deployment guides for multiple platforms - Professional README with feature highlights - GitHub Actions CI/CD workflows - Comprehensive API documentation - Security audit roadmap and compliance framework - Setup scripts for development environment 🧪 Testing & Quality: - Comprehensive test suite with 20+ test modules - Setup verification scripts - Working development environment with both backend and frontend - Health checks and monitoring systems 🌟 Ready for: - Portfolio showcasing - Community contributions - Production deployment - Professional presentation
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
"""
|
||||
Community Features System - Social Engagement and Habit Buddies
|
||||
Enables users to connect, share progress, and motivate each other
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text, and_, or_
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import User, Habit, Log
|
||||
from .db import get_db
|
||||
|
||||
|
||||
class ChallengeStatus(Enum):
|
||||
DRAFT = "draft"
|
||||
ACTIVE = "active"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class ChallengeType(Enum):
|
||||
INDIVIDUAL = "individual" # Personal challenge
|
||||
GROUP = "group" # Multiple participants
|
||||
COMMUNITY = "community" # Open to all users
|
||||
|
||||
|
||||
@dataclass
|
||||
class Community:
|
||||
"""Represents a community/group of users"""
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
category: str # fitness, productivity, wellness, etc.
|
||||
is_public: bool
|
||||
member_count: int
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
tags: List[str]
|
||||
rules: Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HabitBuddy:
|
||||
"""Represents a habit accountability partnership"""
|
||||
id: int
|
||||
user1_id: int
|
||||
user2_id: int
|
||||
shared_habits: List[int] # habit IDs they're tracking together
|
||||
status: str # active, paused, completed
|
||||
created_at: datetime
|
||||
motivation_message: str
|
||||
check_in_frequency: str # daily, weekly
|
||||
|
||||
|
||||
@dataclass
|
||||
class Challenge:
|
||||
"""Represents a habit challenge"""
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
challenge_type: ChallengeType
|
||||
status: ChallengeStatus
|
||||
start_date: datetime
|
||||
end_date: datetime
|
||||
creator_id: int
|
||||
participants: List[int]
|
||||
habit_template: Dict[str, Any]
|
||||
rewards: Dict[str, Any]
|
||||
rules: Dict[str, Any]
|
||||
progress: Dict[int, Any] # user_id -> progress data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Achievement:
|
||||
"""Community achievement/badge"""
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
icon: str
|
||||
category: str
|
||||
requirements: Dict[str, Any]
|
||||
rarity: str # common, rare, epic, legendary
|
||||
points: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SocialPost:
|
||||
"""Social media style post about habits"""
|
||||
id: int
|
||||
user_id: int
|
||||
content: str
|
||||
post_type: str # milestone, motivation, question, celebration
|
||||
habit_id: Optional[int]
|
||||
media_urls: List[str]
|
||||
likes: int
|
||||
comments: List[Dict]
|
||||
created_at: datetime
|
||||
visibility: str # public, friends, private
|
||||
|
||||
|
||||
class CommunityManager:
|
||||
"""Manages community features and social interactions"""
|
||||
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
|
||||
async def create_community(self, creator_id: int, community_data: Dict) -> Community:
|
||||
"""Create a new community"""
|
||||
|
||||
# Validate community data
|
||||
required_fields = ['name', 'description', 'category']
|
||||
for field in required_fields:
|
||||
if field not in community_data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
# Insert into database
|
||||
query = """
|
||||
INSERT INTO communities (name, description, category, is_public,
|
||||
created_by, created_at, tags, rules)
|
||||
VALUES (:name, :description, :category, :is_public,
|
||||
:created_by, :created_at, :tags, :rules)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'name': community_data['name'],
|
||||
'description': community_data['description'],
|
||||
'category': community_data['category'],
|
||||
'is_public': community_data.get('is_public', True),
|
||||
'created_by': creator_id,
|
||||
'created_at': datetime.now(),
|
||||
'tags': json.dumps(community_data.get('tags', [])),
|
||||
'rules': json.dumps(community_data.get('rules', {}))
|
||||
})
|
||||
|
||||
community_id = result.scalar()
|
||||
|
||||
# Add creator as first member
|
||||
await self._add_community_member(community_id, creator_id, role='admin')
|
||||
|
||||
# Return the created community
|
||||
return await self.get_community(community_id)
|
||||
|
||||
async def get_community(self, community_id: int) -> Optional[Community]:
|
||||
"""Get community details"""
|
||||
|
||||
query = """
|
||||
SELECT c.*, COUNT(cm.user_id) as member_count
|
||||
FROM communities c
|
||||
LEFT JOIN community_members cm ON c.id = cm.community_id
|
||||
WHERE c.id = :community_id
|
||||
GROUP BY c.id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {'community_id': community_id})
|
||||
row = result.first()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return Community(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
category=row.category,
|
||||
is_public=row.is_public,
|
||||
member_count=row.member_count or 0,
|
||||
created_by=row.created_by,
|
||||
created_at=row.created_at,
|
||||
tags=json.loads(row.tags or '[]'),
|
||||
rules=json.loads(row.rules or '{}')
|
||||
)
|
||||
|
||||
async def join_community(self, community_id: int, user_id: int) -> bool:
|
||||
"""Join a community"""
|
||||
|
||||
# Check if community exists and is public or user is invited
|
||||
community = await self.get_community(community_id)
|
||||
if not community:
|
||||
raise HTTPException(status_code=404, detail="Community not found")
|
||||
|
||||
# Check if already a member
|
||||
existing_member = await self._is_community_member(community_id, user_id)
|
||||
if existing_member:
|
||||
return False # Already a member
|
||||
|
||||
# Add as member
|
||||
await self._add_community_member(community_id, user_id, role='member')
|
||||
return True
|
||||
|
||||
async def _add_community_member(self, community_id: int, user_id: int, role: str = 'member'):
|
||||
"""Add a member to a community"""
|
||||
|
||||
query = """
|
||||
INSERT INTO community_members (community_id, user_id, role, joined_at)
|
||||
VALUES (:community_id, :user_id, :role, :joined_at)
|
||||
ON CONFLICT (community_id, user_id) DO NOTHING
|
||||
"""
|
||||
|
||||
await self.db.execute(text(query), {
|
||||
'community_id': community_id,
|
||||
'user_id': user_id,
|
||||
'role': role,
|
||||
'joined_at': datetime.now()
|
||||
})
|
||||
|
||||
async def _is_community_member(self, community_id: int, user_id: int) -> bool:
|
||||
"""Check if user is a community member"""
|
||||
|
||||
query = """
|
||||
SELECT 1 FROM community_members
|
||||
WHERE community_id = :community_id AND user_id = :user_id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'community_id': community_id,
|
||||
'user_id': user_id
|
||||
})
|
||||
|
||||
return result.first() is not None
|
||||
|
||||
async def create_habit_buddy_partnership(self, user1_id: int, user2_id: int,
|
||||
shared_habits: List[int]) -> HabitBuddy:
|
||||
"""Create a habit buddy partnership"""
|
||||
|
||||
# Validate that both users exist and habits belong to one of them
|
||||
# Implementation depends on your user validation logic
|
||||
|
||||
query = """
|
||||
INSERT INTO habit_buddies (user1_id, user2_id, shared_habits, status,
|
||||
created_at, check_in_frequency)
|
||||
VALUES (:user1_id, :user2_id, :shared_habits, :status,
|
||||
:created_at, :check_in_frequency)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'user1_id': user1_id,
|
||||
'user2_id': user2_id,
|
||||
'shared_habits': json.dumps(shared_habits),
|
||||
'status': 'active',
|
||||
'created_at': datetime.now(),
|
||||
'check_in_frequency': 'daily'
|
||||
})
|
||||
|
||||
buddy_id = result.scalar()
|
||||
|
||||
return HabitBuddy(
|
||||
id=buddy_id,
|
||||
user1_id=user1_id,
|
||||
user2_id=user2_id,
|
||||
shared_habits=shared_habits,
|
||||
status='active',
|
||||
created_at=datetime.now(),
|
||||
motivation_message='',
|
||||
check_in_frequency='daily'
|
||||
)
|
||||
|
||||
async def get_user_habit_buddies(self, user_id: int) -> List[HabitBuddy]:
|
||||
"""Get all habit buddies for a user"""
|
||||
|
||||
query = """
|
||||
SELECT hb.*, u1.username as user1_name, u2.username as user2_name
|
||||
FROM habit_buddies hb
|
||||
JOIN users u1 ON hb.user1_id = u1.id
|
||||
JOIN users u2 ON hb.user2_id = u2.id
|
||||
WHERE (hb.user1_id = :user_id OR hb.user2_id = :user_id)
|
||||
AND hb.status = 'active'
|
||||
ORDER BY hb.created_at DESC
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {'user_id': user_id})
|
||||
|
||||
buddies = []
|
||||
for row in result:
|
||||
buddies.append(HabitBuddy(
|
||||
id=row.id,
|
||||
user1_id=row.user1_id,
|
||||
user2_id=row.user2_id,
|
||||
shared_habits=json.loads(row.shared_habits or '[]'),
|
||||
status=row.status,
|
||||
created_at=row.created_at,
|
||||
motivation_message=row.motivation_message or '',
|
||||
check_in_frequency=row.check_in_frequency or 'daily'
|
||||
))
|
||||
|
||||
return buddies
|
||||
|
||||
|
||||
class ChallengeManager:
|
||||
"""Manages habit challenges and competitions"""
|
||||
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
|
||||
async def create_challenge(self, creator_id: int, challenge_data: Dict) -> Challenge:
|
||||
"""Create a new challenge"""
|
||||
|
||||
# Validate challenge data
|
||||
required_fields = ['title', 'description', 'challenge_type', 'start_date', 'end_date']
|
||||
for field in required_fields:
|
||||
if field not in challenge_data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
query = """
|
||||
INSERT INTO challenges (title, description, challenge_type, status,
|
||||
start_date, end_date, creator_id, created_at,
|
||||
habit_template, rewards, rules)
|
||||
VALUES (:title, :description, :challenge_type, :status,
|
||||
:start_date, :end_date, :creator_id, :created_at,
|
||||
:habit_template, :rewards, :rules)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'title': challenge_data['title'],
|
||||
'description': challenge_data['description'],
|
||||
'challenge_type': challenge_data['challenge_type'],
|
||||
'status': ChallengeStatus.DRAFT.value,
|
||||
'start_date': challenge_data['start_date'],
|
||||
'end_date': challenge_data['end_date'],
|
||||
'creator_id': creator_id,
|
||||
'created_at': datetime.now(),
|
||||
'habit_template': json.dumps(challenge_data.get('habit_template', {})),
|
||||
'rewards': json.dumps(challenge_data.get('rewards', {})),
|
||||
'rules': json.dumps(challenge_data.get('rules', {}))
|
||||
})
|
||||
|
||||
challenge_id = result.scalar()
|
||||
|
||||
# Auto-join creator to their own challenge
|
||||
await self.join_challenge(challenge_id, creator_id)
|
||||
|
||||
return await self.get_challenge(challenge_id)
|
||||
|
||||
async def get_challenge(self, challenge_id: int) -> Optional[Challenge]:
|
||||
"""Get challenge details"""
|
||||
|
||||
query = """
|
||||
SELECT c.*,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object('user_id', cp.user_id, 'joined_at', cp.joined_at)
|
||||
) FILTER (WHERE cp.user_id IS NOT NULL),
|
||||
'[]'
|
||||
) as participants_data
|
||||
FROM challenges c
|
||||
LEFT JOIN challenge_participants cp ON c.id = cp.challenge_id
|
||||
WHERE c.id = :challenge_id
|
||||
GROUP BY c.id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
|
||||
row = result.first()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
participants_data = json.loads(row.participants_data)
|
||||
participants = [p['user_id'] for p in participants_data]
|
||||
|
||||
return Challenge(
|
||||
id=row.id,
|
||||
title=row.title,
|
||||
description=row.description,
|
||||
challenge_type=ChallengeType(row.challenge_type),
|
||||
status=ChallengeStatus(row.status),
|
||||
start_date=row.start_date,
|
||||
end_date=row.end_date,
|
||||
creator_id=row.creator_id,
|
||||
participants=participants,
|
||||
habit_template=json.loads(row.habit_template or '{}'),
|
||||
rewards=json.loads(row.rewards or '{}'),
|
||||
rules=json.loads(row.rules or '{}'),
|
||||
progress={} # Will be populated separately if needed
|
||||
)
|
||||
|
||||
async def join_challenge(self, challenge_id: int, user_id: int) -> bool:
|
||||
"""Join a challenge"""
|
||||
|
||||
# Check if challenge exists and is joinable
|
||||
challenge = await self.get_challenge(challenge_id)
|
||||
if not challenge:
|
||||
raise HTTPException(status_code=404, detail="Challenge not found")
|
||||
|
||||
if challenge.status not in [ChallengeStatus.DRAFT, ChallengeStatus.ACTIVE]:
|
||||
raise HTTPException(status_code=400, detail="Challenge not joinable")
|
||||
|
||||
# Check if already participating
|
||||
if user_id in challenge.participants:
|
||||
return False # Already participating
|
||||
|
||||
# Add participant
|
||||
query = """
|
||||
INSERT INTO challenge_participants (challenge_id, user_id, joined_at)
|
||||
VALUES (:challenge_id, :user_id, :joined_at)
|
||||
ON CONFLICT (challenge_id, user_id) DO NOTHING
|
||||
"""
|
||||
|
||||
await self.db.execute(text(query), {
|
||||
'challenge_id': challenge_id,
|
||||
'user_id': user_id,
|
||||
'joined_at': datetime.now()
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
async def get_active_challenges(self, user_id: Optional[int] = None,
|
||||
limit: int = 20) -> List[Challenge]:
|
||||
"""Get active challenges, optionally filtered by user participation"""
|
||||
|
||||
base_query = """
|
||||
SELECT c.*,
|
||||
COUNT(cp.user_id) as participant_count,
|
||||
CASE WHEN :user_id IS NULL THEN FALSE
|
||||
ELSE EXISTS(
|
||||
SELECT 1 FROM challenge_participants cp2
|
||||
WHERE cp2.challenge_id = c.id AND cp2.user_id = :user_id
|
||||
) END as user_participating
|
||||
FROM challenges c
|
||||
LEFT JOIN challenge_participants cp ON c.id = cp.challenge_id
|
||||
WHERE c.status = 'active'
|
||||
AND c.start_date <= :now
|
||||
AND c.end_date > :now
|
||||
"""
|
||||
|
||||
if user_id:
|
||||
base_query += """
|
||||
AND (c.challenge_type = 'community'
|
||||
OR EXISTS(
|
||||
SELECT 1 FROM challenge_participants cp3
|
||||
WHERE cp3.challenge_id = c.id AND cp3.user_id = :user_id
|
||||
))
|
||||
"""
|
||||
|
||||
base_query += """
|
||||
GROUP BY c.id
|
||||
ORDER BY c.start_date DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(base_query), {
|
||||
'user_id': user_id,
|
||||
'now': datetime.now(),
|
||||
'limit': limit
|
||||
})
|
||||
|
||||
challenges = []
|
||||
for row in result:
|
||||
# Get participants for this challenge
|
||||
participants = await self._get_challenge_participants(row.id)
|
||||
|
||||
challenges.append(Challenge(
|
||||
id=row.id,
|
||||
title=row.title,
|
||||
description=row.description,
|
||||
challenge_type=ChallengeType(row.challenge_type),
|
||||
status=ChallengeStatus(row.status),
|
||||
start_date=row.start_date,
|
||||
end_date=row.end_date,
|
||||
creator_id=row.creator_id,
|
||||
participants=participants,
|
||||
habit_template=json.loads(row.habit_template or '{}'),
|
||||
rewards=json.loads(row.rewards or '{}'),
|
||||
rules=json.loads(row.rules or '{}'),
|
||||
progress={}
|
||||
))
|
||||
|
||||
return challenges
|
||||
|
||||
async def _get_challenge_participants(self, challenge_id: int) -> List[int]:
|
||||
"""Get list of participant user IDs for a challenge"""
|
||||
|
||||
query = """
|
||||
SELECT user_id FROM challenge_participants
|
||||
WHERE challenge_id = :challenge_id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
|
||||
return [row.user_id for row in result]
|
||||
|
||||
async def update_challenge_progress(self, challenge_id: int, user_id: int,
|
||||
progress_data: Dict):
|
||||
"""Update a user's progress in a challenge"""
|
||||
|
||||
query = """
|
||||
INSERT INTO challenge_progress (challenge_id, user_id, progress_data, updated_at)
|
||||
VALUES (:challenge_id, :user_id, :progress_data, :updated_at)
|
||||
ON CONFLICT (challenge_id, user_id)
|
||||
DO UPDATE SET
|
||||
progress_data = :progress_data,
|
||||
updated_at = :updated_at
|
||||
"""
|
||||
|
||||
await self.db.execute(text(query), {
|
||||
'challenge_id': challenge_id,
|
||||
'user_id': user_id,
|
||||
'progress_data': json.dumps(progress_data),
|
||||
'updated_at': datetime.now()
|
||||
})
|
||||
|
||||
async def get_challenge_leaderboard(self, challenge_id: int) -> List[Dict]:
|
||||
"""Get leaderboard for a challenge"""
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
cp.user_id,
|
||||
u.username,
|
||||
cp.progress_data,
|
||||
cp.updated_at,
|
||||
ROW_NUMBER() OVER (ORDER BY
|
||||
CAST(cp.progress_data->>'score' AS INTEGER) DESC,
|
||||
cp.updated_at ASC
|
||||
) as rank
|
||||
FROM challenge_progress cp
|
||||
JOIN users u ON cp.user_id = u.id
|
||||
WHERE cp.challenge_id = :challenge_id
|
||||
ORDER BY rank
|
||||
LIMIT 50
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {'challenge_id': challenge_id})
|
||||
|
||||
leaderboard = []
|
||||
for row in result:
|
||||
leaderboard.append({
|
||||
'rank': row.rank,
|
||||
'user_id': row.user_id,
|
||||
'username': row.username,
|
||||
'progress': json.loads(row.progress_data or '{}'),
|
||||
'last_updated': row.updated_at
|
||||
})
|
||||
|
||||
return leaderboard
|
||||
|
||||
|
||||
class SocialFeedManager:
|
||||
"""Manages social feed and posts"""
|
||||
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
|
||||
async def create_post(self, user_id: int, post_data: Dict) -> SocialPost:
|
||||
"""Create a social post"""
|
||||
|
||||
query = """
|
||||
INSERT INTO social_posts (user_id, content, post_type, habit_id,
|
||||
media_urls, created_at, visibility)
|
||||
VALUES (:user_id, :content, :post_type, :habit_id,
|
||||
:media_urls, :created_at, :visibility)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'user_id': user_id,
|
||||
'content': post_data['content'],
|
||||
'post_type': post_data.get('post_type', 'general'),
|
||||
'habit_id': post_data.get('habit_id'),
|
||||
'media_urls': json.dumps(post_data.get('media_urls', [])),
|
||||
'created_at': datetime.now(),
|
||||
'visibility': post_data.get('visibility', 'public')
|
||||
})
|
||||
|
||||
post_id = result.scalar()
|
||||
|
||||
return SocialPost(
|
||||
id=post_id,
|
||||
user_id=user_id,
|
||||
content=post_data['content'],
|
||||
post_type=post_data.get('post_type', 'general'),
|
||||
habit_id=post_data.get('habit_id'),
|
||||
media_urls=post_data.get('media_urls', []),
|
||||
likes=0,
|
||||
comments=[],
|
||||
created_at=datetime.now(),
|
||||
visibility=post_data.get('visibility', 'public')
|
||||
)
|
||||
|
||||
async def get_user_feed(self, user_id: int, limit: int = 50) -> List[Dict]:
|
||||
"""Get social feed for a user"""
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
sp.*,
|
||||
u.username,
|
||||
u.avatar_url,
|
||||
COUNT(spl.id) as likes_count,
|
||||
COUNT(spc.id) as comments_count
|
||||
FROM social_posts sp
|
||||
JOIN users u ON sp.user_id = u.id
|
||||
LEFT JOIN social_post_likes spl ON sp.id = spl.post_id
|
||||
LEFT JOIN social_post_comments spc ON sp.id = spc.post_id
|
||||
WHERE sp.visibility = 'public'
|
||||
OR sp.user_id = :user_id
|
||||
OR sp.user_id IN (
|
||||
SELECT user2_id FROM habit_buddies WHERE user1_id = :user_id
|
||||
UNION
|
||||
SELECT user1_id FROM habit_buddies WHERE user2_id = :user_id
|
||||
)
|
||||
GROUP BY sp.id, u.username, u.avatar_url
|
||||
ORDER BY sp.created_at DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'user_id': user_id,
|
||||
'limit': limit
|
||||
})
|
||||
|
||||
feed = []
|
||||
for row in result:
|
||||
feed.append({
|
||||
'id': row.id,
|
||||
'user_id': row.user_id,
|
||||
'username': row.username,
|
||||
'avatar_url': row.avatar_url,
|
||||
'content': row.content,
|
||||
'post_type': row.post_type,
|
||||
'habit_id': row.habit_id,
|
||||
'media_urls': json.loads(row.media_urls or '[]'),
|
||||
'likes': row.likes_count,
|
||||
'comments': row.comments_count,
|
||||
'created_at': row.created_at,
|
||||
'visibility': row.visibility
|
||||
})
|
||||
|
||||
return feed
|
||||
|
||||
async def like_post(self, post_id: int, user_id: int) -> bool:
|
||||
"""Like or unlike a post"""
|
||||
|
||||
# Check if already liked
|
||||
query = """
|
||||
SELECT 1 FROM social_post_likes
|
||||
WHERE post_id = :post_id AND user_id = :user_id
|
||||
"""
|
||||
|
||||
result = await self.db.execute(text(query), {
|
||||
'post_id': post_id,
|
||||
'user_id': user_id
|
||||
})
|
||||
|
||||
if result.first():
|
||||
# Unlike
|
||||
delete_query = """
|
||||
DELETE FROM social_post_likes
|
||||
WHERE post_id = :post_id AND user_id = :user_id
|
||||
"""
|
||||
await self.db.execute(text(delete_query), {
|
||||
'post_id': post_id,
|
||||
'user_id': user_id
|
||||
})
|
||||
return False
|
||||
else:
|
||||
# Like
|
||||
insert_query = """
|
||||
INSERT INTO social_post_likes (post_id, user_id, created_at)
|
||||
VALUES (:post_id, :user_id, :created_at)
|
||||
"""
|
||||
await self.db.execute(text(insert_query), {
|
||||
'post_id': post_id,
|
||||
'user_id': user_id,
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
return True
|
||||
|
||||
|
||||
# FastAPI endpoints for community features
|
||||
async def create_community_endpoint(creator_id: int, community_data: Dict,
|
||||
db: Session) -> Dict:
|
||||
"""Create a new community"""
|
||||
|
||||
manager = CommunityManager(db)
|
||||
community = await manager.create_community(creator_id, community_data)
|
||||
return asdict(community)
|
||||
|
||||
|
||||
async def get_user_communities(user_id: int, db: Session) -> List[Dict]:
|
||||
"""Get communities for a user"""
|
||||
|
||||
query = """
|
||||
SELECT c.*, cm.role, cm.joined_at
|
||||
FROM communities c
|
||||
JOIN community_members cm ON c.id = cm.community_id
|
||||
WHERE cm.user_id = :user_id
|
||||
ORDER BY cm.joined_at DESC
|
||||
"""
|
||||
|
||||
result = await db.execute(text(query), {'user_id': user_id})
|
||||
|
||||
communities = []
|
||||
for row in result:
|
||||
communities.append({
|
||||
'id': row.id,
|
||||
'name': row.name,
|
||||
'description': row.description,
|
||||
'category': row.category,
|
||||
'is_public': row.is_public,
|
||||
'created_by': row.created_by,
|
||||
'created_at': row.created_at,
|
||||
'tags': json.loads(row.tags or '[]'),
|
||||
'user_role': row.role,
|
||||
'joined_at': row.joined_at
|
||||
})
|
||||
|
||||
return communities
|
||||
Reference in New Issue
Block a user