Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion app/api/endpoints/user_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from app.api.security import require_role
from app.application.schemas.item_dto import ItemResponse
from app.application.schemas.rating_dto import RatingResponse
from app.application.schemas.user_dto import UserCreateDTO, UserUpdateDTO, UserResponse
from app.application.schemas.user_dto import (
UserCreateDTO, UserUpdateDTO, UserResponse,
UserGrowthDTO, UserEngagementDTO, UserStatsDTO
)
from app.application.services.rating_service import RatingService
from app.application.services.user_service import UserService
from app.domain.item import Item
Expand All @@ -15,6 +18,65 @@

router = APIRouter(prefix="/users", tags=["Users"])

# Admin Analytics Endpoints - place these before path parameter routes
@router.get("/growth", response_model=list[UserGrowthDTO])
def get_user_growth(
days: int = 30,
db: Session = Depends(get_db),
role: str = Depends(require_role(["admin"]))
):
"""Get user growth data for the specified number of days

Shows the number of new users that registered each day

Requires the "admin" role.

Args:
days (int, optional): Number of days to show growth data for. Defaults to 30.
"""

user_service = UserService(db)
return user_service.get_user_growth(days)

@router.get("/engagement", response_model=list[UserEngagementDTO])
def get_user_engagement(
limit: int = 10,
db: Session = Depends(get_db),
role: str = Depends(require_role(["admin"]))
):
"""
Retrieve a list of the most engaged users based on rating activity.

Args:
limit (int, optional): The maximum number of users to return. Defaults to 10.
db (Session): Database session dependency.
role (str): Role dependency, requires "admin" role to access.

Returns:
List[UserEngagementDTO]: A list of user engagement data transfer objects.
"""

user_service = UserService(db)
return user_service.get_user_engagement(limit)

@router.get("/stats", response_model=UserStatsDTO)
def get_user_stats(
db: Session = Depends(get_db),
role: str = Depends(require_role(["admin"]))
):
"""Get overall user statistics

Returns a dictionary with the following keys:

- total_users: The total number of users in the database
- total_items: The total number of items in the database
- total_ratings: The total number of ratings in the database

Requires the "admin" role.
"""
user_service = UserService(db)
return user_service.get_user_stats()

@router.post("", response_model=UserResponse, status_code=201)
def create_user(user_data: UserCreateDTO, db: Session = Depends(get_db), role: str = Depends(require_role(["admin"]))):
user_service = UserService(db)
Expand Down Expand Up @@ -122,3 +184,4 @@ def reset_user_password(
)

return {"message": "Password reset successfully"}

28 changes: 27 additions & 1 deletion app/application/schemas/user_dto.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl
from typing import Optional
from datetime import datetime
from datetime import datetime, date

class UserCreateDTO(BaseModel):
name: str = Field(..., max_length=100)
Expand All @@ -26,3 +26,29 @@ class UserResponse(BaseModel):
updated_at: datetime

model_config = ConfigDict(from_attributes=True)

# Admin Analytics DTOs
class UserGrowthDTO(BaseModel):
date: date
count: int

class Config:
orm_mode = True

class UserEngagementDTO(BaseModel):
user_id: int
username: str
ratings_count: int
last_activity: date

class Config:
orm_mode = True

class UserStatsDTO(BaseModel):
total_users: int
active_users: int
new_users_today: int
average_ratings_per_user: float

class Config:
orm_mode = True
15 changes: 14 additions & 1 deletion app/application/services/user_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List, Optional
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from app.api.security import verify_password
from app.domain.user import User
Expand Down Expand Up @@ -38,3 +39,15 @@ def update_user(self, user_id: int, user_data: UserUpdateDTO) -> Optional[User]:

def delete_user(self, user_id: int) -> bool:
return self.repository.delete(user_id)

def get_user_growth(self, days: int = 30) -> List[Dict[str, Any]]:
"""Get user growth data for the specified number of days"""
return self.repository.get_user_growth(days)

def get_user_engagement(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get most engaged users based on rating activity"""
return self.repository.get_user_engagement(limit)

def get_user_stats(self) -> Dict[str, Any]:
"""Get overall user statistics"""
return self.repository.get_user_stats()
114 changes: 112 additions & 2 deletions app/infrastructure/repositories/user_repository.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from datetime import datetime
from datetime import datetime, timedelta
from typing import List, Optional
from sqlalchemy import func, desc
from sqlalchemy.orm import Session
from app.api.security import hash_password
from app.domain.user import User
from app.application.schemas.user_dto import UserCreateDTO, UserUpdateDTO
from app.domain.rating import Rating
from app.application.schemas.user_dto import (
UserCreateDTO, UserUpdateDTO,
UserGrowthDTO, UserEngagementDTO, UserStatsDTO
)
from app.config import settings

class UserRepository:
Expand Down Expand Up @@ -58,3 +63,108 @@ def delete(self, user_id: int) -> bool:
self.db.delete(user)
self.db.commit()
return True

def get_user_growth(self, days: int = 30) -> List[UserGrowthDTO]:
"""
Get user growth data for the specified number of days
Shows the number of new users that registered each day
"""
# Calculate the start date (n days ago)
start_date = datetime.now() - timedelta(days=days)

# Query to get count of users registered per day
results = self.db.query(
func.date(User.created_at).label('date'),
func.count(User.id).label('count')
).filter(
User.created_at >= start_date
).group_by(
func.date(User.created_at)
).order_by(
'date'
).all()

# Create a dictionary with all dates in the range
all_dates = {}
current_date = start_date.date()
end_date = datetime.now().date()

while current_date <= end_date:
all_dates[current_date] = 0
current_date += timedelta(days=1)

# Fill in the actual counts
for date_count in results:
all_dates[date_count.date] = date_count.count

# Convert to the DTO format
return [
UserGrowthDTO(date=date, count=count)
for date, count in all_dates.items()
]

def get_user_engagement(self, limit: int = 10) -> List[UserEngagementDTO]:
"""
Get most engaged users based on rating activity
"""
results = self.db.query(
User.id.label('user_id'),
User.name.label('username'),
func.count(Rating.id).label('ratings_count'),
func.max(Rating.created_at).label('last_activity')
).join(
Rating, User.id == Rating.user_id
).group_by(
User.id, User.name
).order_by(
desc('ratings_count')
).limit(limit).all()

return [
UserEngagementDTO(
user_id=r.user_id,
username=r.username,
ratings_count=r.ratings_count,
last_activity=r.last_activity.date()
)
for r in results
]

def get_user_stats(self) -> UserStatsDTO:
"""
Get overall user statistics
"""
# Total users
total_users = self.db.query(func.count(User.id)).scalar() or 0

# Active users (users with at least one rating in last 30 days)
thirty_days_ago = datetime.now() - timedelta(days=30)
active_users = self.db.query(
func.count(func.distinct(Rating.user_id))
).filter(
Rating.created_at >= thirty_days_ago
).scalar() or 0

# New users today
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
new_users_today = self.db.query(
func.count(User.id)
).filter(
User.created_at >= today_start
).scalar() or 0

# Average ratings per user
ratings_per_user = self.db.query(
func.avg(func.count(Rating.id))
).join(
User, Rating.user_id == User.id
).group_by(
Rating.user_id
).scalar() or 0

return UserStatsDTO(
total_users=total_users,
active_users=active_users,
new_users_today=new_users_today,
average_ratings_per_user=round(float(ratings_per_user), 1)
)