initial commit - LeDiscord plateforme des copains
This commit is contained in:
3
backend/api/routers/__init__.py
Normal file
3
backend/api/routers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import auth, users, events, albums, posts, vlogs, stats, admin, notifications, settings, information, tickets
|
||||
|
||||
__all__ = ["auth", "users", "events", "albums", "posts", "vlogs", "stats", "admin", "notifications", "settings", "information", "tickets"]
|
||||
444
backend/api/routers/admin.py
Normal file
444
backend/api/routers/admin.py
Normal file
@@ -0,0 +1,444 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import List
|
||||
import os
|
||||
from pathlib import Path
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.user import User
|
||||
from models.album import Album, Media
|
||||
from models.vlog import Vlog
|
||||
from models.event import Event
|
||||
from models.post import Post
|
||||
from utils.security import get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_directory_size(path):
|
||||
"""Calculate total size of a directory."""
|
||||
total = 0
|
||||
try:
|
||||
for entry in os.scandir(path):
|
||||
if entry.is_file():
|
||||
total += entry.stat().st_size
|
||||
elif entry.is_dir():
|
||||
total += get_directory_size(entry.path)
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
return total
|
||||
|
||||
def format_bytes(bytes):
|
||||
"""Format bytes to human readable string."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes < 1024.0:
|
||||
return f"{bytes:.2f} {unit}"
|
||||
bytes /= 1024.0
|
||||
return f"{bytes:.2f} PB"
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_admin_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Get admin dashboard information."""
|
||||
# User statistics
|
||||
total_users = db.query(User).count()
|
||||
active_users = db.query(User).filter(User.is_active == True).count()
|
||||
admin_users = db.query(User).filter(User.is_admin == True).count()
|
||||
|
||||
# Content statistics
|
||||
total_events = db.query(Event).count()
|
||||
total_posts = db.query(Post).count()
|
||||
total_vlogs = db.query(Vlog).count()
|
||||
total_media = db.query(Media).count()
|
||||
|
||||
# Storage statistics
|
||||
upload_path = Path(settings.UPLOAD_PATH)
|
||||
total_storage = 0
|
||||
storage_breakdown = {}
|
||||
|
||||
if upload_path.exists():
|
||||
# Calculate storage by category
|
||||
categories = ['avatars', 'albums', 'vlogs', 'posts']
|
||||
for category in categories:
|
||||
category_path = upload_path / category
|
||||
if category_path.exists():
|
||||
size = get_directory_size(category_path)
|
||||
storage_breakdown[category] = {
|
||||
"bytes": size,
|
||||
"formatted": format_bytes(size)
|
||||
}
|
||||
total_storage += size
|
||||
|
||||
# Database storage
|
||||
db_size = db.query(func.sum(Media.file_size)).scalar() or 0
|
||||
|
||||
return {
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"active": active_users,
|
||||
"admins": admin_users
|
||||
},
|
||||
"content": {
|
||||
"events": total_events,
|
||||
"posts": total_posts,
|
||||
"vlogs": total_vlogs,
|
||||
"media_files": total_media
|
||||
},
|
||||
"storage": {
|
||||
"total_bytes": total_storage,
|
||||
"total_formatted": format_bytes(total_storage),
|
||||
"breakdown": storage_breakdown,
|
||||
"database_tracked": format_bytes(db_size)
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/users")
|
||||
async def get_all_users_admin(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Get all users with admin details."""
|
||||
users = db.query(User).all()
|
||||
|
||||
user_list = []
|
||||
for user in users:
|
||||
# Calculate user storage
|
||||
user_media = db.query(func.sum(Media.file_size)).join(Album).filter(
|
||||
Album.creator_id == user.id
|
||||
).scalar() or 0
|
||||
|
||||
user_vlogs = db.query(func.sum(Vlog.id)).filter(
|
||||
Vlog.author_id == user.id
|
||||
).scalar() or 0
|
||||
|
||||
user_list.append({
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"is_active": user.is_active,
|
||||
"is_admin": user.is_admin,
|
||||
"created_at": user.created_at,
|
||||
"attendance_rate": user.attendance_rate,
|
||||
"storage_used": format_bytes(user_media),
|
||||
"content_count": {
|
||||
"posts": db.query(Post).filter(Post.author_id == user.id).count(),
|
||||
"vlogs": user_vlogs,
|
||||
"albums": db.query(Album).filter(Album.creator_id == user.id).count()
|
||||
}
|
||||
})
|
||||
|
||||
return user_list
|
||||
|
||||
@router.put("/users/{user_id}/toggle-active")
|
||||
async def toggle_user_active(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Toggle user active status."""
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot deactivate your own account"
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
user.is_active = not user.is_active
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"User {'activated' if user.is_active else 'deactivated'} successfully",
|
||||
"is_active": user.is_active
|
||||
}
|
||||
|
||||
@router.put("/users/{user_id}/toggle-admin")
|
||||
async def toggle_user_admin(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Toggle user admin status."""
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot change your own admin status"
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
user.is_admin = not user.is_admin
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"User admin status {'granted' if user.is_admin else 'revoked'} successfully",
|
||||
"is_admin": user.is_admin
|
||||
}
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Delete a user and all their content."""
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete your own account"
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Delete user's files
|
||||
user_dirs = [
|
||||
Path(settings.UPLOAD_PATH) / "avatars" / str(user_id),
|
||||
Path(settings.UPLOAD_PATH) / "vlogs" / str(user_id)
|
||||
]
|
||||
|
||||
for dir_path in user_dirs:
|
||||
if dir_path.exists():
|
||||
import shutil
|
||||
shutil.rmtree(dir_path)
|
||||
|
||||
# Delete user (cascade will handle related records)
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
@router.get("/storage/cleanup")
|
||||
async def cleanup_storage(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Find and optionally clean up orphaned files."""
|
||||
upload_path = Path(settings.UPLOAD_PATH)
|
||||
orphaned_files = []
|
||||
total_orphaned_size = 0
|
||||
|
||||
try:
|
||||
# Check for orphaned media files
|
||||
if (upload_path / "albums").exists():
|
||||
for album_dir in (upload_path / "albums").iterdir():
|
||||
if album_dir.is_dir():
|
||||
try:
|
||||
album_id = int(album_dir.name)
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
|
||||
if not album:
|
||||
# Entire album directory is orphaned
|
||||
size = get_directory_size(album_dir)
|
||||
orphaned_files.append({
|
||||
"path": str(album_dir),
|
||||
"type": "album_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_orphaned_size += size
|
||||
else:
|
||||
# Check individual files
|
||||
for file_path in album_dir.glob("*"):
|
||||
if file_path.is_file():
|
||||
relative_path = f"/albums/{album_id}/{file_path.name}"
|
||||
media = db.query(Media).filter(
|
||||
(Media.file_path == relative_path) |
|
||||
(Media.thumbnail_path == relative_path)
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
size = file_path.stat().st_size
|
||||
orphaned_files.append({
|
||||
"path": str(file_path),
|
||||
"type": "media_file",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_orphaned_size += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing album directory {album_dir}: {e}")
|
||||
continue
|
||||
|
||||
# Check for orphaned avatar files
|
||||
if (upload_path / "avatars").exists():
|
||||
for user_dir in (upload_path / "avatars").iterdir():
|
||||
if user_dir.is_dir():
|
||||
try:
|
||||
user_id = int(user_dir.name)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
size = get_directory_size(user_dir)
|
||||
orphaned_files.append({
|
||||
"path": str(user_dir),
|
||||
"type": "user_avatar_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_orphaned_size += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing avatar directory {user_dir}: {e}")
|
||||
continue
|
||||
|
||||
# Check for orphaned vlog files
|
||||
if (upload_path / "vlogs").exists():
|
||||
for user_dir in (upload_path / "vlogs").iterdir():
|
||||
if user_dir.is_dir():
|
||||
try:
|
||||
user_id = int(user_dir.name)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
size = get_directory_size(user_dir)
|
||||
orphaned_files.append({
|
||||
"path": str(user_dir),
|
||||
"type": "user_vlog_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_orphaned_size += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing vlog directory {user_dir}: {e}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"orphaned_files": orphaned_files,
|
||||
"total_orphaned": len(orphaned_files),
|
||||
"total_orphaned_size": format_bytes(total_orphaned_size),
|
||||
"message": f"Trouvé {len(orphaned_files)} fichier(s) orphelin(s) pour un total de {format_bytes(total_orphaned_size)}. Utilisez DELETE pour les supprimer."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup scan: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error during cleanup scan: {str(e)}"
|
||||
)
|
||||
|
||||
@router.delete("/storage/cleanup")
|
||||
async def delete_orphaned_files(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Delete orphaned files."""
|
||||
upload_path = Path(settings.UPLOAD_PATH)
|
||||
deleted_files = []
|
||||
total_freed_space = 0
|
||||
|
||||
try:
|
||||
# Check for orphaned media files
|
||||
if (upload_path / "albums").exists():
|
||||
for album_dir in (upload_path / "albums").iterdir():
|
||||
if album_dir.is_dir():
|
||||
try:
|
||||
album_id = int(album_dir.name)
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
|
||||
if not album:
|
||||
# Entire album directory is orphaned
|
||||
size = get_directory_size(album_dir)
|
||||
import shutil
|
||||
shutil.rmtree(album_dir)
|
||||
deleted_files.append({
|
||||
"path": str(album_dir),
|
||||
"type": "album_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_freed_space += size
|
||||
else:
|
||||
# Check individual files
|
||||
for file_path in album_dir.glob("*"):
|
||||
if file_path.is_file():
|
||||
relative_path = f"/albums/{album_id}/{file_path.name}"
|
||||
media = db.query(Media).filter(
|
||||
(Media.file_path == relative_path) |
|
||||
(Media.thumbnail_path == relative_path)
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
size = file_path.stat().st_size
|
||||
file_path.unlink() # Delete the file
|
||||
deleted_files.append({
|
||||
"path": str(file_path),
|
||||
"type": "media_file",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_freed_space += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing album directory {album_dir}: {e}")
|
||||
continue
|
||||
|
||||
# Check for orphaned avatar files
|
||||
if (upload_path / "avatars").exists():
|
||||
for user_dir in (upload_path / "avatars").iterdir():
|
||||
if user_dir.is_dir():
|
||||
try:
|
||||
user_id = int(user_dir.name)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
# User directory is orphaned
|
||||
size = get_directory_size(user_dir)
|
||||
import shutil
|
||||
shutil.rmtree(user_dir)
|
||||
deleted_files.append({
|
||||
"path": str(user_dir),
|
||||
"type": "user_avatar_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_freed_space += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing avatar directory {user_dir}: {e}")
|
||||
continue
|
||||
|
||||
# Check for orphaned vlog files
|
||||
if (upload_path / "vlogs").exists():
|
||||
for user_dir in (upload_path / "vlogs").iterdir():
|
||||
if user_dir.is_dir():
|
||||
try:
|
||||
user_id = int(user_dir.name)
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
# User directory is orphaned
|
||||
size = get_directory_size(user_dir)
|
||||
import shutil
|
||||
shutil.rmtree(user_dir)
|
||||
deleted_files.append({
|
||||
"path": str(user_dir),
|
||||
"type": "user_vlog_directory",
|
||||
"size": format_bytes(size)
|
||||
})
|
||||
total_freed_space += size
|
||||
except (ValueError, OSError) as e:
|
||||
print(f"Error processing vlog directory {user_dir}: {e}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"message": "Cleanup completed successfully",
|
||||
"deleted_files": deleted_files,
|
||||
"total_deleted": len(deleted_files),
|
||||
"total_freed_space": format_bytes(total_freed_space),
|
||||
"details": f"Supprimé {len(deleted_files)} fichier(s) pour libérer {format_bytes(total_freed_space)}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error during cleanup: {str(e)}"
|
||||
)
|
||||
464
backend/api/routers/albums.py
Normal file
464
backend/api/routers/albums.py
Normal file
@@ -0,0 +1,464 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import magic
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.album import Album, Media, MediaType, MediaLike
|
||||
from models.user import User
|
||||
from schemas.album import AlbumCreate, AlbumUpdate, AlbumResponse
|
||||
from utils.security import get_current_active_user
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=AlbumResponse)
|
||||
async def create_album(
|
||||
album_data: AlbumCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new album."""
|
||||
album = Album(
|
||||
**album_data.dict(),
|
||||
creator_id=current_user.id
|
||||
)
|
||||
db.add(album)
|
||||
db.commit()
|
||||
db.refresh(album)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.get("/", response_model=List[AlbumResponse])
|
||||
async def get_albums(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
event_id: Optional[int] = None
|
||||
):
|
||||
"""Get all albums, optionally filtered by event."""
|
||||
query = db.query(Album)
|
||||
if event_id:
|
||||
query = query.filter(Album.event_id == event_id)
|
||||
albums = query.order_by(Album.created_at.desc()).all()
|
||||
return [format_album_response(album, db, current_user.id) for album in albums]
|
||||
|
||||
@router.get("/{album_id}", response_model=AlbumResponse)
|
||||
async def get_album(
|
||||
album_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.put("/{album_id}", response_model=AlbumResponse)
|
||||
async def update_album(
|
||||
album_id: int,
|
||||
album_update: AlbumUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this album"
|
||||
)
|
||||
|
||||
for field, value in album_update.dict(exclude_unset=True).items():
|
||||
setattr(album, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(album)
|
||||
return format_album_response(album, db, current_user.id)
|
||||
|
||||
@router.delete("/{album_id}")
|
||||
async def delete_album(
|
||||
album_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this album"
|
||||
)
|
||||
|
||||
# Delete media files
|
||||
for media in album.media:
|
||||
try:
|
||||
if media.file_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.file_path)
|
||||
if media.thumbnail_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(album)
|
||||
db.commit()
|
||||
return {"message": "Album deleted successfully"}
|
||||
|
||||
@router.post("/{album_id}/media")
|
||||
async def upload_media(
|
||||
album_id: int,
|
||||
files: List[UploadFile] = File(...),
|
||||
captions: Optional[List[str]] = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Upload media files to an album."""
|
||||
album = db.query(Album).filter(Album.id == album_id).first()
|
||||
if not album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Album not found"
|
||||
)
|
||||
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to add media to this album"
|
||||
)
|
||||
|
||||
# Check max media per album limit
|
||||
from utils.settings_service import SettingsService
|
||||
max_media_per_album = SettingsService.get_setting(db, "max_media_per_album", 50)
|
||||
current_media_count = len(album.media)
|
||||
|
||||
if current_media_count + len(files) > max_media_per_album:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Album would exceed maximum of {max_media_per_album} media files. Current: {current_media_count}, trying to add: {len(files)}"
|
||||
)
|
||||
|
||||
# Create album directory
|
||||
album_dir = Path(settings.UPLOAD_PATH) / "albums" / str(album_id)
|
||||
album_dir.mkdir(parents=True, exist_ok=True)
|
||||
thumbnails_dir = album_dir / "thumbnails"
|
||||
thumbnails_dir.mkdir(exist_ok=True)
|
||||
|
||||
uploaded_media = []
|
||||
|
||||
for i, file in enumerate(files):
|
||||
# Check file size
|
||||
contents = await file.read()
|
||||
file_size = len(contents)
|
||||
|
||||
# Get dynamic upload limits
|
||||
max_size = SettingsService.get_max_upload_size(db, file.content_type or "unknown")
|
||||
if file_size > max_size:
|
||||
max_size_mb = max_size // (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File {file.filename} exceeds maximum size of {max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Check file type
|
||||
mime = magic.from_buffer(contents, mime=True)
|
||||
if not SettingsService.is_file_type_allowed(db, mime):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File {file.filename} has unsupported type: {mime}"
|
||||
)
|
||||
|
||||
# Determine media type
|
||||
if mime.startswith('image/'):
|
||||
media_type = MediaType.IMAGE
|
||||
elif mime.startswith('video/'):
|
||||
media_type = MediaType.VIDEO
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File {file.filename} has unsupported type: {mime}"
|
||||
)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = file.filename.split(".")[-1]
|
||||
filename = f"{uuid.uuid4()}.{file_extension}"
|
||||
file_path = album_dir / filename
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Process media
|
||||
thumbnail_path = None
|
||||
width = height = duration = None
|
||||
|
||||
if media_type == MediaType.IMAGE:
|
||||
# Get image dimensions and create thumbnail
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
width, height = img.size
|
||||
|
||||
# Create thumbnail with better quality
|
||||
thumbnail = img.copy()
|
||||
thumbnail.thumbnail((800, 800)) # Larger thumbnail for better quality
|
||||
thumbnail_filename = f"thumb_{filename}"
|
||||
thumbnail_path_full = thumbnails_dir / thumbnail_filename
|
||||
thumbnail.save(thumbnail_path_full, quality=95, optimize=True) # High quality
|
||||
thumbnail_path = f"/albums/{album_id}/thumbnails/{thumbnail_filename}"
|
||||
except Exception as e:
|
||||
print(f"Error processing image: {e}")
|
||||
|
||||
# Create media record
|
||||
media = Media(
|
||||
album_id=album_id,
|
||||
file_path=f"/albums/{album_id}/{filename}",
|
||||
thumbnail_path=thumbnail_path,
|
||||
media_type=media_type,
|
||||
caption=captions[i] if captions and i < len(captions) else None,
|
||||
file_size=file_size,
|
||||
width=width,
|
||||
height=height,
|
||||
duration=duration
|
||||
)
|
||||
db.add(media)
|
||||
uploaded_media.append(media)
|
||||
|
||||
# Set first image as album cover if not set
|
||||
if not album.cover_image and uploaded_media:
|
||||
first_image = next((m for m in uploaded_media if m.media_type == MediaType.IMAGE), None)
|
||||
if first_image:
|
||||
album.cover_image = first_image.thumbnail_path or first_image.file_path
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {
|
||||
"message": f"Successfully uploaded {len(uploaded_media)} files",
|
||||
"media_count": len(uploaded_media)
|
||||
}
|
||||
|
||||
@router.delete("/{album_id}/media/{media_id}")
|
||||
async def delete_media(
|
||||
album_id: int,
|
||||
media_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a media file from an album."""
|
||||
media = db.query(Media).filter(
|
||||
Media.id == media_id,
|
||||
Media.album_id == album_id
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Media not found"
|
||||
)
|
||||
|
||||
album = media.album
|
||||
if album.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this media"
|
||||
)
|
||||
|
||||
# Delete files
|
||||
try:
|
||||
if media.file_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.file_path)
|
||||
if media.thumbnail_path:
|
||||
os.remove(settings.UPLOAD_PATH + media.thumbnail_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {"message": "Media deleted successfully"}
|
||||
|
||||
@router.post("/{album_id}/media/{media_id}/like")
|
||||
async def toggle_media_like(
|
||||
album_id: int,
|
||||
media_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Toggle like on a media file."""
|
||||
media = db.query(Media).filter(
|
||||
Media.id == media_id,
|
||||
Media.album_id == album_id
|
||||
).first()
|
||||
|
||||
if not media:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Media not found"
|
||||
)
|
||||
|
||||
existing_like = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media_id,
|
||||
MediaLike.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# Unlike
|
||||
db.delete(existing_like)
|
||||
media.likes_count = max(0, media.likes_count - 1)
|
||||
message = "Like removed"
|
||||
else:
|
||||
# Like
|
||||
like = MediaLike(media_id=media_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
media.likes_count += 1
|
||||
message = "Media liked"
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update event cover image if this album is linked to an event
|
||||
album = media.album
|
||||
if album.event_id:
|
||||
update_event_cover_from_album(album.event_id, db)
|
||||
|
||||
return {"message": message, "likes_count": media.likes_count}
|
||||
|
||||
def format_album_response(album: Album, db: Session, current_user_id: int) -> dict:
|
||||
"""Format album response with additional information."""
|
||||
# Get top media (most liked)
|
||||
top_media = db.query(Media).filter(
|
||||
Media.album_id == album.id
|
||||
).order_by(Media.likes_count.desc()).limit(3).all()
|
||||
|
||||
# Format media with likes information
|
||||
formatted_media = []
|
||||
for media in album.media:
|
||||
# Check if current user liked this media
|
||||
is_liked = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media.id,
|
||||
MediaLike.user_id == current_user_id
|
||||
).first() is not None
|
||||
|
||||
# Format likes
|
||||
likes = []
|
||||
for like in media.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
formatted_media.append({
|
||||
"id": media.id,
|
||||
"file_path": media.file_path,
|
||||
"thumbnail_path": media.thumbnail_path,
|
||||
"media_type": media.media_type,
|
||||
"caption": media.caption,
|
||||
"file_size": media.file_size,
|
||||
"width": media.width,
|
||||
"height": media.height,
|
||||
"duration": media.duration,
|
||||
"likes_count": media.likes_count,
|
||||
"is_liked": is_liked,
|
||||
"likes": likes,
|
||||
"created_at": media.created_at
|
||||
})
|
||||
|
||||
# Format top media
|
||||
formatted_top_media = []
|
||||
for media in top_media:
|
||||
is_liked = db.query(MediaLike).filter(
|
||||
MediaLike.media_id == media.id,
|
||||
MediaLike.user_id == current_user_id
|
||||
).first() is not None
|
||||
|
||||
likes = []
|
||||
for like in media.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
formatted_top_media.append({
|
||||
"id": media.id,
|
||||
"file_path": media.file_path,
|
||||
"thumbnail_path": media.thumbnail_path,
|
||||
"media_type": media.media_type,
|
||||
"caption": media.caption,
|
||||
"file_size": media.file_size,
|
||||
"width": media.width,
|
||||
"height": media.height,
|
||||
"duration": media.duration,
|
||||
"likes_count": media.likes_count,
|
||||
"is_liked": is_liked,
|
||||
"likes": likes,
|
||||
"created_at": media.created_at
|
||||
})
|
||||
|
||||
return {
|
||||
"id": album.id,
|
||||
"title": album.title,
|
||||
"description": album.description,
|
||||
"creator_id": album.creator_id,
|
||||
"event_id": album.event_id,
|
||||
"cover_image": album.cover_image,
|
||||
"created_at": album.created_at,
|
||||
"updated_at": album.updated_at,
|
||||
"creator_name": album.creator.full_name,
|
||||
"media_count": len(album.media),
|
||||
"media": formatted_media,
|
||||
"top_media": formatted_top_media,
|
||||
"event_title": album.event.title if album.event else None
|
||||
}
|
||||
|
||||
def update_event_cover_from_album(event_id: int, db: Session):
|
||||
"""Update event cover image from the most liked media in linked albums."""
|
||||
from models.event import Event
|
||||
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
return
|
||||
|
||||
# Get the most liked media from all albums linked to this event
|
||||
most_liked_media = db.query(Media).join(Album).filter(
|
||||
Album.event_id == event_id,
|
||||
Media.media_type == MediaType.IMAGE
|
||||
).order_by(Media.likes_count.desc()).first()
|
||||
|
||||
if most_liked_media:
|
||||
event.cover_image = most_liked_media.thumbnail_path or most_liked_media.file_path
|
||||
db.commit()
|
||||
97
backend/api/routers/auth.py
Normal file
97
backend/api/routers/auth.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timedelta
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.user import User
|
||||
from schemas.user import UserCreate, UserResponse, Token
|
||||
from utils.security import verify_password, get_password_hash, create_access_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register", response_model=Token)
|
||||
async def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
||||
"""Register a new user."""
|
||||
# Check if registration is enabled
|
||||
from utils.settings_service import SettingsService
|
||||
if not SettingsService.get_setting(db, "enable_registration", True):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Registration is currently disabled"
|
||||
)
|
||||
|
||||
# Check if max users limit is reached
|
||||
max_users = SettingsService.get_setting(db, "max_users", 50)
|
||||
current_users_count = db.query(User).count()
|
||||
if current_users_count >= max_users:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Maximum number of users reached"
|
||||
)
|
||||
|
||||
# Check if email already exists
|
||||
if db.query(User).filter(User.email == user_data.email).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered"
|
||||
)
|
||||
|
||||
# Check if username already exists
|
||||
if db.query(User).filter(User.username == user_data.username).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already taken"
|
||||
)
|
||||
|
||||
# Create new user
|
||||
user = User(
|
||||
email=user_data.email,
|
||||
username=user_data.username,
|
||||
full_name=user_data.full_name,
|
||||
hashed_password=get_password_hash(user_data.password)
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# Create access token
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id), "email": user.email}
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
"""Login with email and password."""
|
||||
# Find user by email (username field is used for email in OAuth2PasswordRequestForm)
|
||||
user = db.query(User).filter(User.email == form_data.username).first()
|
||||
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Inactive user"
|
||||
)
|
||||
|
||||
# Create access token
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id), "email": user.email}
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user
|
||||
}
|
||||
257
backend/api/routers/events.py
Normal file
257
backend/api/routers/events.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from config.database import get_db
|
||||
from models.event import Event, EventParticipation, ParticipationStatus
|
||||
from models.user import User
|
||||
from models.notification import Notification, NotificationType
|
||||
from schemas.event import EventCreate, EventUpdate, EventResponse, ParticipationUpdate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.email import send_event_notification
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=EventResponse)
|
||||
async def create_event(
|
||||
event_data: EventCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new event."""
|
||||
event = Event(
|
||||
**event_data.dict(),
|
||||
creator_id=current_user.id
|
||||
)
|
||||
db.add(event)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
|
||||
# Create participations for all active users
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
for user in users:
|
||||
participation = EventParticipation(
|
||||
event_id=event.id,
|
||||
user_id=user.id,
|
||||
status=ParticipationStatus.PENDING
|
||||
)
|
||||
db.add(participation)
|
||||
|
||||
# Create notification
|
||||
if user.id != current_user.id:
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type=NotificationType.EVENT_INVITATION,
|
||||
title=f"Nouvel événement: {event.title}",
|
||||
message=f"{current_user.full_name} a créé un nouvel événement",
|
||||
link=f"/events/{event.id}"
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# Send email notification (async task would be better)
|
||||
try:
|
||||
send_event_notification(user.email, event)
|
||||
except:
|
||||
pass # Don't fail if email sending fails
|
||||
|
||||
db.commit()
|
||||
return format_event_response(event, db)
|
||||
|
||||
@router.get("/", response_model=List[EventResponse])
|
||||
async def get_events(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
upcoming: bool = None
|
||||
):
|
||||
"""Get all events, optionally filtered by upcoming status."""
|
||||
query = db.query(Event)
|
||||
|
||||
if upcoming is True:
|
||||
# Only upcoming events
|
||||
query = query.filter(Event.date >= datetime.utcnow())
|
||||
elif upcoming is False:
|
||||
# Only past events
|
||||
query = query.filter(Event.date < datetime.utcnow())
|
||||
# If upcoming is None, return all events
|
||||
|
||||
events = query.order_by(Event.date.desc()).all()
|
||||
return [format_event_response(event, db) for event in events]
|
||||
|
||||
@router.get("/upcoming", response_model=List[EventResponse])
|
||||
async def get_upcoming_events(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get only upcoming events."""
|
||||
events = db.query(Event).filter(
|
||||
Event.date >= datetime.utcnow()
|
||||
).order_by(Event.date).all()
|
||||
return [format_event_response(event, db) for event in events]
|
||||
|
||||
@router.get("/past", response_model=List[EventResponse])
|
||||
async def get_past_events(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get only past events."""
|
||||
events = db.query(Event).filter(
|
||||
Event.date < datetime.utcnow()
|
||||
).order_by(Event.date.desc()).all()
|
||||
return [format_event_response(event, db) for event in events]
|
||||
|
||||
@router.get("/{event_id}", response_model=EventResponse)
|
||||
async def get_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific event."""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
return format_event_response(event, db)
|
||||
|
||||
@router.put("/{event_id}", response_model=EventResponse)
|
||||
async def update_event(
|
||||
event_id: int,
|
||||
event_update: EventUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update an event."""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
if event.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this event"
|
||||
)
|
||||
|
||||
for field, value in event_update.dict(exclude_unset=True).items():
|
||||
setattr(event, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return format_event_response(event, db)
|
||||
|
||||
@router.delete("/{event_id}")
|
||||
async def delete_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete an event."""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
if event.creator_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this event"
|
||||
)
|
||||
|
||||
db.delete(event)
|
||||
db.commit()
|
||||
return {"message": "Event deleted successfully"}
|
||||
|
||||
@router.put("/{event_id}/participation", response_model=EventResponse)
|
||||
async def update_participation(
|
||||
event_id: int,
|
||||
participation_update: ParticipationUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update user participation status for an event."""
|
||||
participation = db.query(EventParticipation).filter(
|
||||
EventParticipation.event_id == event_id,
|
||||
EventParticipation.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not participation:
|
||||
# Create new participation if it doesn't exist
|
||||
participation = EventParticipation(
|
||||
event_id=event_id,
|
||||
user_id=current_user.id,
|
||||
status=participation_update.status
|
||||
)
|
||||
db.add(participation)
|
||||
else:
|
||||
participation.status = participation_update.status
|
||||
participation.response_date = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update user attendance rate
|
||||
update_user_attendance_rate(current_user, db)
|
||||
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
return format_event_response(event, db)
|
||||
|
||||
def format_event_response(event: Event, db: Session) -> dict:
|
||||
"""Format event response with participation counts."""
|
||||
participations = []
|
||||
present_count = absent_count = maybe_count = pending_count = 0
|
||||
|
||||
for p in event.participations:
|
||||
user = db.query(User).filter(User.id == p.user_id).first()
|
||||
participations.append({
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"status": p.status,
|
||||
"response_date": p.response_date
|
||||
})
|
||||
|
||||
if p.status == ParticipationStatus.PRESENT:
|
||||
present_count += 1
|
||||
elif p.status == ParticipationStatus.ABSENT:
|
||||
absent_count += 1
|
||||
elif p.status == ParticipationStatus.MAYBE:
|
||||
maybe_count += 1
|
||||
else:
|
||||
pending_count += 1
|
||||
|
||||
return {
|
||||
"id": event.id,
|
||||
"title": event.title,
|
||||
"description": event.description,
|
||||
"location": event.location,
|
||||
"date": event.date,
|
||||
"end_date": event.end_date,
|
||||
"creator_id": event.creator_id,
|
||||
"cover_image": event.cover_image,
|
||||
"created_at": event.created_at,
|
||||
"updated_at": event.updated_at,
|
||||
"creator_name": event.creator.full_name,
|
||||
"participations": participations,
|
||||
"present_count": present_count,
|
||||
"absent_count": absent_count,
|
||||
"maybe_count": maybe_count,
|
||||
"pending_count": pending_count
|
||||
}
|
||||
|
||||
def update_user_attendance_rate(user: User, db: Session):
|
||||
"""Update user attendance rate based on past events."""
|
||||
past_participations = db.query(EventParticipation).join(Event).filter(
|
||||
EventParticipation.user_id == user.id,
|
||||
Event.date < datetime.utcnow(),
|
||||
EventParticipation.status.in_([ParticipationStatus.PRESENT, ParticipationStatus.ABSENT])
|
||||
).all()
|
||||
|
||||
if past_participations:
|
||||
present_count = sum(1 for p in past_participations if p.status == ParticipationStatus.PRESENT)
|
||||
user.attendance_rate = (present_count / len(past_participations)) * 100
|
||||
db.commit()
|
||||
143
backend/api/routers/information.py
Normal file
143
backend/api/routers/information.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from config.database import get_db
|
||||
from models.information import Information
|
||||
from models.user import User
|
||||
from schemas.information import InformationCreate, InformationUpdate, InformationResponse
|
||||
from utils.security import get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[InformationResponse])
|
||||
async def get_informations(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
category: str = None,
|
||||
published_only: bool = True
|
||||
):
|
||||
"""Get all informations, optionally filtered by category and published status."""
|
||||
query = db.query(Information)
|
||||
|
||||
if published_only:
|
||||
query = query.filter(Information.is_published == True)
|
||||
|
||||
if category:
|
||||
query = query.filter(Information.category == category)
|
||||
|
||||
informations = query.order_by(Information.priority.desc(), Information.created_at.desc()).all()
|
||||
return informations
|
||||
|
||||
@router.get("/public", response_model=List[InformationResponse])
|
||||
async def get_public_informations(
|
||||
db: Session = Depends(get_db),
|
||||
category: str = None
|
||||
):
|
||||
"""Get public informations without authentication."""
|
||||
query = db.query(Information).filter(Information.is_published == True)
|
||||
|
||||
if category:
|
||||
query = query.filter(Information.category == category)
|
||||
|
||||
informations = query.order_by(Information.priority.desc(), Information.created_at.desc()).all()
|
||||
return informations
|
||||
|
||||
@router.get("/{information_id}", response_model=InformationResponse)
|
||||
async def get_information(
|
||||
information_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific information."""
|
||||
information = db.query(Information).filter(Information.id == information_id).first()
|
||||
if not information:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Information not found"
|
||||
)
|
||||
|
||||
if not information.is_published and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Information not found"
|
||||
)
|
||||
|
||||
return information
|
||||
|
||||
@router.post("/", response_model=InformationResponse)
|
||||
async def create_information(
|
||||
information_data: InformationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Create a new information (admin only)."""
|
||||
information = Information(**information_data.dict())
|
||||
db.add(information)
|
||||
db.commit()
|
||||
db.refresh(information)
|
||||
return information
|
||||
|
||||
@router.put("/{information_id}", response_model=InformationResponse)
|
||||
async def update_information(
|
||||
information_id: int,
|
||||
information_update: InformationUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Update an information (admin only)."""
|
||||
information = db.query(Information).filter(Information.id == information_id).first()
|
||||
if not information:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Information not found"
|
||||
)
|
||||
|
||||
for field, value in information_update.dict(exclude_unset=True).items():
|
||||
setattr(information, field, value)
|
||||
|
||||
information.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(information)
|
||||
return information
|
||||
|
||||
@router.delete("/{information_id}")
|
||||
async def delete_information(
|
||||
information_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Delete an information (admin only)."""
|
||||
information = db.query(Information).filter(Information.id == information_id).first()
|
||||
if not information:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Information not found"
|
||||
)
|
||||
|
||||
db.delete(information)
|
||||
db.commit()
|
||||
return {"message": "Information deleted successfully"}
|
||||
|
||||
@router.put("/{information_id}/toggle-publish")
|
||||
async def toggle_information_publish(
|
||||
information_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Toggle information published status (admin only)."""
|
||||
information = db.query(Information).filter(Information.id == information_id).first()
|
||||
if not information:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Information not found"
|
||||
)
|
||||
|
||||
information.is_published = not information.is_published
|
||||
information.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Information {'published' if information.is_published else 'unpublished'} successfully",
|
||||
"is_published": information.is_published
|
||||
}
|
||||
79
backend/api/routers/notifications.py
Normal file
79
backend/api/routers/notifications.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from config.database import get_db
|
||||
from models.notification import Notification
|
||||
from models.user import User
|
||||
from schemas.notification import NotificationResponse
|
||||
from utils.security import get_current_active_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[NotificationResponse])
|
||||
async def get_user_notifications(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
):
|
||||
"""Get user notifications."""
|
||||
notifications = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id
|
||||
).order_by(Notification.created_at.desc()).limit(limit).offset(offset).all()
|
||||
|
||||
return notifications
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
async def mark_notification_read(
|
||||
notification_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Mark a notification as read."""
|
||||
notification = db.query(Notification).filter(
|
||||
Notification.id == notification_id,
|
||||
Notification.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Notification not found"
|
||||
)
|
||||
|
||||
notification.is_read = True
|
||||
notification.read_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"message": "Notification marked as read"}
|
||||
|
||||
@router.put("/read-all")
|
||||
async def mark_all_notifications_read(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Mark all user notifications as read."""
|
||||
db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).update({
|
||||
"is_read": True,
|
||||
"read_at": datetime.utcnow()
|
||||
})
|
||||
db.commit()
|
||||
|
||||
return {"message": "All notifications marked as read"}
|
||||
|
||||
@router.get("/unread-count")
|
||||
async def get_unread_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get count of unread notifications."""
|
||||
count = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).count()
|
||||
|
||||
return {"unread_count": count}
|
||||
347
backend/api/routers/posts.py
Normal file
347
backend/api/routers/posts.py
Normal file
@@ -0,0 +1,347 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.post import Post, PostMention, PostLike, PostComment
|
||||
from models.user import User
|
||||
from models.notification import Notification, NotificationType
|
||||
from utils.notification_service import NotificationService
|
||||
from schemas.post import PostCreate, PostUpdate, PostResponse, PostCommentCreate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=PostResponse)
|
||||
async def create_post(
|
||||
post_data: PostCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new post."""
|
||||
post = Post(
|
||||
author_id=current_user.id,
|
||||
content=post_data.content,
|
||||
image_url=post_data.image_url
|
||||
)
|
||||
db.add(post)
|
||||
db.flush() # Get the post ID before creating mentions
|
||||
|
||||
# Create mentions
|
||||
for user_id in post_data.mentioned_user_ids:
|
||||
mentioned_user = db.query(User).filter(User.id == user_id).first()
|
||||
if mentioned_user:
|
||||
mention = PostMention(
|
||||
post_id=post.id,
|
||||
mentioned_user_id=user_id
|
||||
)
|
||||
db.add(mention)
|
||||
|
||||
# Create notification for mentioned user
|
||||
NotificationService.create_mention_notification(
|
||||
db=db,
|
||||
mentioned_user_id=user_id,
|
||||
author=current_user,
|
||||
content_type="post",
|
||||
content_id=post.id
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
return format_post_response(post, db)
|
||||
|
||||
@router.get("/", response_model=List[PostResponse])
|
||||
async def get_posts(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
):
|
||||
"""Get all posts."""
|
||||
posts = db.query(Post).order_by(Post.created_at.desc()).limit(limit).offset(offset).all()
|
||||
return [format_post_response(post, db) for post in posts]
|
||||
|
||||
@router.get("/{post_id}", response_model=PostResponse)
|
||||
async def get_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
return format_post_response(post, db)
|
||||
|
||||
@router.put("/{post_id}", response_model=PostResponse)
|
||||
async def update_post(
|
||||
post_id: int,
|
||||
post_update: PostUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update a post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
if post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this post"
|
||||
)
|
||||
|
||||
for field, value in post_update.dict(exclude_unset=True).items():
|
||||
setattr(post, field, value)
|
||||
|
||||
post.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
return format_post_response(post, db)
|
||||
|
||||
@router.delete("/{post_id}")
|
||||
async def delete_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
if post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this post"
|
||||
)
|
||||
|
||||
db.delete(post)
|
||||
db.commit()
|
||||
return {"message": "Post deleted successfully"}
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_post_image(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Upload an image for a post."""
|
||||
# Validate file type
|
||||
if not SettingsService.is_file_type_allowed(db, file.content_type or "unknown"):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
|
||||
["image/jpeg", "image/png", "image/gif", "image/webp"])
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
# Check file size
|
||||
contents = await file.read()
|
||||
file_size = len(contents)
|
||||
max_size = SettingsService.get_max_upload_size(db, file.content_type or "image/jpeg")
|
||||
if file_size > max_size:
|
||||
max_size_mb = max_size // (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File too large. Maximum size: {max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Create posts directory
|
||||
posts_dir = Path(settings.UPLOAD_PATH) / "posts" / str(current_user.id)
|
||||
posts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = file.filename.split(".")[-1]
|
||||
filename = f"{uuid.uuid4()}.{file_extension}"
|
||||
file_path = posts_dir / filename
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Resize image if it's REALLY too large (4K support)
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
if img.size[0] > 3840 or img.size[1] > 2160: # 4K threshold
|
||||
img.thumbnail((3840, 2160), Image.Resampling.LANCZOS)
|
||||
img.save(file_path, quality=95, optimize=True) # High quality
|
||||
except Exception as e:
|
||||
print(f"Error processing image: {e}")
|
||||
|
||||
# Return the image URL
|
||||
image_url = f"/posts/{current_user.id}/{filename}"
|
||||
|
||||
return {"image_url": image_url}
|
||||
|
||||
@router.post("/{post_id}/like")
|
||||
async def toggle_post_like(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Toggle like on a post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
# Check if user already liked the post
|
||||
existing_like = db.query(PostLike).filter(
|
||||
PostLike.post_id == post_id,
|
||||
PostLike.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# Unlike
|
||||
db.delete(existing_like)
|
||||
post.likes_count = max(0, post.likes_count - 1)
|
||||
message = "Like removed"
|
||||
is_liked = False
|
||||
else:
|
||||
# Like
|
||||
like = PostLike(post_id=post_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
post.likes_count += 1
|
||||
message = "Post liked"
|
||||
is_liked = True
|
||||
|
||||
db.commit()
|
||||
return {"message": message, "is_liked": is_liked, "likes_count": post.likes_count}
|
||||
|
||||
@router.post("/{post_id}/comment")
|
||||
async def add_post_comment(
|
||||
post_id: int,
|
||||
comment_data: PostCommentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Add a comment to a post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
comment = PostComment(
|
||||
post_id=post_id,
|
||||
author_id=current_user.id,
|
||||
content=comment_data.content
|
||||
)
|
||||
db.add(comment)
|
||||
db.commit()
|
||||
db.refresh(comment)
|
||||
|
||||
return {
|
||||
"message": "Comment added successfully",
|
||||
"comment": {
|
||||
"id": comment.id,
|
||||
"content": comment.content,
|
||||
"author_id": comment.author_id,
|
||||
"author_name": current_user.full_name,
|
||||
"author_avatar": current_user.avatar_url,
|
||||
"created_at": comment.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@router.delete("/{post_id}/comment/{comment_id}")
|
||||
async def delete_post_comment(
|
||||
post_id: int,
|
||||
comment_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a comment from a post."""
|
||||
comment = db.query(PostComment).filter(
|
||||
PostComment.id == comment_id,
|
||||
PostComment.post_id == post_id
|
||||
).first()
|
||||
|
||||
if not comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Comment not found"
|
||||
)
|
||||
|
||||
if comment.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this comment"
|
||||
)
|
||||
|
||||
db.delete(comment)
|
||||
db.commit()
|
||||
return {"message": "Comment deleted successfully"}
|
||||
|
||||
def format_post_response(post: Post, db: Session) -> dict:
|
||||
"""Format post response with author and mentions information."""
|
||||
mentioned_users = []
|
||||
for mention in post.mentions:
|
||||
user = mention.mentioned_user
|
||||
mentioned_users.append({
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url
|
||||
})
|
||||
|
||||
# Get likes and comments
|
||||
likes = []
|
||||
for like in post.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
comments = []
|
||||
for comment in post.comments:
|
||||
user = db.query(User).filter(User.id == comment.author_id).first()
|
||||
if user:
|
||||
comments.append({
|
||||
"id": comment.id,
|
||||
"content": comment.content,
|
||||
"author_id": user.id,
|
||||
"author_name": user.full_name,
|
||||
"author_avatar": user.avatar_url,
|
||||
"created_at": comment.created_at
|
||||
})
|
||||
|
||||
return {
|
||||
"id": post.id,
|
||||
"author_id": post.author_id,
|
||||
"content": post.content,
|
||||
"image_url": post.image_url,
|
||||
"likes_count": post.likes_count,
|
||||
"comments_count": post.comments_count,
|
||||
"created_at": post.created_at,
|
||||
"updated_at": post.updated_at,
|
||||
"author_name": post.author.full_name,
|
||||
"author_avatar": post.author.avatar_url,
|
||||
"mentioned_users": mentioned_users,
|
||||
"likes": likes,
|
||||
"comments": comments
|
||||
}
|
||||
287
backend/api/routers/settings.py
Normal file
287
backend/api/routers/settings.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
from config.database import get_db
|
||||
from models.settings import SystemSettings
|
||||
from models.user import User
|
||||
from schemas.settings import (
|
||||
SystemSettingCreate,
|
||||
SystemSettingUpdate,
|
||||
SystemSettingResponse,
|
||||
SettingsCategoryResponse,
|
||||
UploadLimitsResponse
|
||||
)
|
||||
from utils.security import get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[SystemSettingResponse])
|
||||
async def get_all_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Get all system settings (admin only)."""
|
||||
settings = db.query(SystemSettings).order_by(SystemSettings.category, SystemSettings.key).all()
|
||||
return settings
|
||||
|
||||
@router.get("/category/{category}", response_model=SettingsCategoryResponse)
|
||||
async def get_settings_by_category(
|
||||
category: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Get settings by category (admin only)."""
|
||||
settings = db.query(SystemSettings).filter(SystemSettings.category == category).all()
|
||||
return SettingsCategoryResponse(category=category, settings=settings)
|
||||
|
||||
@router.get("/upload-limits", response_model=UploadLimitsResponse)
|
||||
async def get_upload_limits(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Get current upload limits configuration."""
|
||||
settings = db.query(SystemSettings).filter(
|
||||
SystemSettings.category == "uploads"
|
||||
).all()
|
||||
|
||||
# Convertir en dictionnaire pour faciliter l'accès
|
||||
settings_dict = {s.key: s.value for s in settings}
|
||||
|
||||
# Debug: afficher les valeurs récupérées
|
||||
print(f"DEBUG - Upload limits from DB: {settings_dict}")
|
||||
|
||||
return UploadLimitsResponse(
|
||||
max_album_size_mb=int(settings_dict.get("max_album_size_mb", "100")),
|
||||
max_vlog_size_mb=int(settings_dict.get("max_vlog_size_mb", "500")),
|
||||
max_image_size_mb=int(settings_dict.get("max_image_size_mb", "10")),
|
||||
max_video_size_mb=int(settings_dict.get("max_video_size_mb", "100")),
|
||||
max_media_per_album=int(settings_dict.get("max_media_per_album", "50")),
|
||||
allowed_image_types=settings_dict.get("allowed_image_types", "image/jpeg,image/png,image/gif,image/webp").split(","),
|
||||
allowed_video_types=settings_dict.get("allowed_video_types", "video/mp4,video/mpeg,video/quicktime,video/webm").split(",")
|
||||
)
|
||||
|
||||
@router.post("/", response_model=SystemSettingResponse)
|
||||
async def create_setting(
|
||||
setting_data: SystemSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Create a new system setting (admin only)."""
|
||||
# Vérifier si la clé existe déjà
|
||||
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data.key).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Setting key already exists"
|
||||
)
|
||||
|
||||
setting = SystemSettings(**setting_data.dict())
|
||||
db.add(setting)
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
return setting
|
||||
|
||||
@router.put("/{setting_key}", response_model=SystemSettingResponse)
|
||||
async def update_setting(
|
||||
setting_key: str,
|
||||
setting_update: SystemSettingUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Update a system setting (admin only)."""
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == setting_key).first()
|
||||
if not setting:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Setting not found"
|
||||
)
|
||||
|
||||
if not setting.is_editable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This setting cannot be modified"
|
||||
)
|
||||
|
||||
# Validation des valeurs selon le type de paramètre
|
||||
if setting_key.startswith("max_") and setting_key.endswith("_mb"):
|
||||
try:
|
||||
size_mb = int(setting_update.value)
|
||||
if size_mb <= 0 or size_mb > 10000: # Max 10GB
|
||||
raise ValueError("Size must be between 1 and 10000 MB")
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid size value: {str(e)}"
|
||||
)
|
||||
|
||||
setting.value = setting_update.value
|
||||
setting.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
return setting
|
||||
|
||||
@router.delete("/{setting_key}")
|
||||
async def delete_setting(
|
||||
setting_key: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Delete a system setting (admin only)."""
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == setting_key).first()
|
||||
if not setting:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Setting not found"
|
||||
)
|
||||
|
||||
if not setting.is_editable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This setting cannot be deleted"
|
||||
)
|
||||
|
||||
db.delete(setting)
|
||||
db.commit()
|
||||
return {"message": "Setting deleted successfully"}
|
||||
|
||||
@router.get("/test-upload-limits")
|
||||
async def test_upload_limits(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Test endpoint to verify upload limits are working correctly."""
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
# Test image upload limit
|
||||
image_limit = SettingsService.get_max_upload_size(db, "image/jpeg")
|
||||
image_limit_mb = image_limit // (1024 * 1024)
|
||||
|
||||
# Test video upload limit
|
||||
video_limit = SettingsService.get_max_upload_size(db, "video/mp4")
|
||||
video_limit_mb = video_limit // (1024 * 1024)
|
||||
|
||||
# Test file type validation
|
||||
image_allowed = SettingsService.is_file_type_allowed(db, "image/jpeg")
|
||||
video_allowed = SettingsService.is_file_type_allowed(db, "video/mp4")
|
||||
invalid_allowed = SettingsService.is_file_type_allowed(db, "application/pdf")
|
||||
|
||||
return {
|
||||
"image_upload_limit": {
|
||||
"bytes": image_limit,
|
||||
"mb": image_limit_mb,
|
||||
"content_type": "image/jpeg"
|
||||
},
|
||||
"video_upload_limit": {
|
||||
"bytes": video_limit,
|
||||
"mb": video_limit_mb,
|
||||
"content_type": "video/mp4"
|
||||
},
|
||||
"file_type_validation": {
|
||||
"image_jpeg_allowed": image_allowed,
|
||||
"video_mp4_allowed": video_allowed,
|
||||
"pdf_not_allowed": not invalid_allowed
|
||||
},
|
||||
"message": "Upload limits test completed"
|
||||
}
|
||||
|
||||
@router.get("/public/registration-status")
|
||||
async def get_public_registration_status(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get registration status without authentication (public endpoint)."""
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
try:
|
||||
enable_registration = SettingsService.get_setting(db, "enable_registration", True)
|
||||
max_users = SettingsService.get_setting(db, "max_users", 50)
|
||||
current_users_count = db.query(User).count()
|
||||
|
||||
return {
|
||||
"registration_enabled": enable_registration,
|
||||
"max_users": max_users,
|
||||
"current_users_count": current_users_count,
|
||||
"can_register": enable_registration and current_users_count < max_users
|
||||
}
|
||||
except Exception as e:
|
||||
# En cas d'erreur, on retourne des valeurs par défaut sécurisées
|
||||
return {
|
||||
"registration_enabled": False,
|
||||
"max_users": 50,
|
||||
"current_users_count": 0,
|
||||
"can_register": False
|
||||
}
|
||||
|
||||
@router.post("/initialize-defaults")
|
||||
async def initialize_default_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Initialize default system settings (admin only)."""
|
||||
default_settings = [
|
||||
{
|
||||
"key": "max_album_size_mb",
|
||||
"value": "100",
|
||||
"description": "Taille maximale des albums en MB",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "max_vlog_size_mb",
|
||||
"value": "500",
|
||||
"description": "Taille maximale des vlogs en MB",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "max_image_size_mb",
|
||||
"value": "10",
|
||||
"description": "Taille maximale des images en MB",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "max_video_size_mb",
|
||||
"value": "100",
|
||||
"description": "Taille maximale des vidéos en MB",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "max_media_per_album",
|
||||
"value": "50",
|
||||
"description": "Nombre maximum de médias par album",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "allowed_image_types",
|
||||
"value": "image/jpeg,image/png,image/gif,image/webp",
|
||||
"description": "Types d'images autorisés (séparés par des virgules)",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "allowed_video_types",
|
||||
"value": "video/mp4,video/mpeg,video/quicktime,video/webm",
|
||||
"description": "Types de vidéos autorisés (séparés par des virgules)",
|
||||
"category": "uploads"
|
||||
},
|
||||
{
|
||||
"key": "max_users",
|
||||
"value": "50",
|
||||
"description": "Nombre maximum d'utilisateurs",
|
||||
"category": "general"
|
||||
},
|
||||
{
|
||||
"key": "enable_registration",
|
||||
"value": "true",
|
||||
"description": "Autoriser les nouvelles inscriptions",
|
||||
"category": "general"
|
||||
}
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for setting_data in default_settings:
|
||||
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data["key"]).first()
|
||||
if not existing:
|
||||
setting = SystemSettings(**setting_data)
|
||||
db.add(setting)
|
||||
created_count += 1
|
||||
|
||||
db.commit()
|
||||
return {"message": f"{created_count} default settings created"}
|
||||
314
backend/api/routers/stats.py
Normal file
314
backend/api/routers/stats.py
Normal file
@@ -0,0 +1,314 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime, timedelta
|
||||
from config.database import get_db
|
||||
from models.user import User
|
||||
from models.event import Event, EventParticipation, ParticipationStatus
|
||||
from models.album import Album, Media
|
||||
from models.post import Post
|
||||
from models.vlog import Vlog
|
||||
from utils.security import get_current_active_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/overview")
|
||||
async def get_overview_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get general overview statistics."""
|
||||
total_users = db.query(User).filter(User.is_active == True).count()
|
||||
total_events = db.query(Event).count()
|
||||
total_albums = db.query(Album).count()
|
||||
total_posts = db.query(Post).count()
|
||||
total_vlogs = db.query(Vlog).count()
|
||||
total_media = db.query(Media).count()
|
||||
|
||||
# Recent activity (last 30 days)
|
||||
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||
recent_events = db.query(Event).filter(Event.created_at >= thirty_days_ago).count()
|
||||
recent_posts = db.query(Post).filter(Post.created_at >= thirty_days_ago).count()
|
||||
recent_vlogs = db.query(Vlog).filter(Vlog.created_at >= thirty_days_ago).count()
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"total_events": total_events,
|
||||
"total_albums": total_albums,
|
||||
"total_posts": total_posts,
|
||||
"total_vlogs": total_vlogs,
|
||||
"total_media": total_media,
|
||||
"recent_events": recent_events,
|
||||
"recent_posts": recent_posts,
|
||||
"recent_vlogs": recent_vlogs
|
||||
}
|
||||
|
||||
@router.get("/attendance")
|
||||
async def get_attendance_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get attendance statistics for all users."""
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
|
||||
attendance_stats = []
|
||||
for user in users:
|
||||
# Get past events participation
|
||||
past_participations = db.query(EventParticipation).join(Event).filter(
|
||||
EventParticipation.user_id == user.id,
|
||||
Event.date < datetime.utcnow()
|
||||
).all()
|
||||
|
||||
total_past_events = len(past_participations)
|
||||
present_count = sum(1 for p in past_participations if p.status == ParticipationStatus.PRESENT)
|
||||
absent_count = sum(1 for p in past_participations if p.status == ParticipationStatus.ABSENT)
|
||||
|
||||
# Get upcoming events participation
|
||||
upcoming_participations = db.query(EventParticipation).join(Event).filter(
|
||||
EventParticipation.user_id == user.id,
|
||||
Event.date >= datetime.utcnow()
|
||||
).all()
|
||||
|
||||
upcoming_present = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.PRESENT)
|
||||
upcoming_maybe = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.MAYBE)
|
||||
upcoming_pending = sum(1 for p in upcoming_participations if p.status == ParticipationStatus.PENDING)
|
||||
|
||||
attendance_stats.append({
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"attendance_rate": user.attendance_rate,
|
||||
"total_past_events": total_past_events,
|
||||
"present_count": present_count,
|
||||
"absent_count": absent_count,
|
||||
"upcoming_present": upcoming_present,
|
||||
"upcoming_maybe": upcoming_maybe,
|
||||
"upcoming_pending": upcoming_pending
|
||||
})
|
||||
|
||||
# Sort by attendance rate
|
||||
attendance_stats.sort(key=lambda x: x["attendance_rate"], reverse=True)
|
||||
|
||||
return {
|
||||
"attendance_stats": attendance_stats,
|
||||
"best_attendee": attendance_stats[0] if attendance_stats else None,
|
||||
"worst_attendee": attendance_stats[-1] if attendance_stats else None
|
||||
}
|
||||
|
||||
@router.get("/fun")
|
||||
async def get_fun_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get fun statistics about the group."""
|
||||
# Most active poster
|
||||
post_counts = db.query(
|
||||
User.id,
|
||||
User.username,
|
||||
User.full_name,
|
||||
func.count(Post.id).label("post_count")
|
||||
).join(Post).group_by(User.id).order_by(func.count(Post.id).desc()).first()
|
||||
|
||||
# Most mentioned user
|
||||
mention_counts = db.query(
|
||||
User.id,
|
||||
User.username,
|
||||
User.full_name,
|
||||
func.count().label("mention_count")
|
||||
).join(Post.mentions).group_by(User.id).order_by(func.count().desc()).first()
|
||||
|
||||
# Biggest vlogger
|
||||
vlog_counts = db.query(
|
||||
User.id,
|
||||
User.username,
|
||||
User.full_name,
|
||||
func.count(Vlog.id).label("vlog_count")
|
||||
).join(Vlog).group_by(User.id).order_by(func.count(Vlog.id).desc()).first()
|
||||
|
||||
# Photo addict (most albums created)
|
||||
album_counts = db.query(
|
||||
User.id,
|
||||
User.username,
|
||||
User.full_name,
|
||||
func.count(Album.id).label("album_count")
|
||||
).join(Album).group_by(User.id).order_by(func.count(Album.id).desc()).first()
|
||||
|
||||
# Event organizer (most events created)
|
||||
event_counts = db.query(
|
||||
User.id,
|
||||
User.username,
|
||||
User.full_name,
|
||||
func.count(Event.id).label("event_count")
|
||||
).join(Event, Event.creator_id == User.id).group_by(User.id).order_by(func.count(Event.id).desc()).first()
|
||||
|
||||
# Most viewed vlog
|
||||
most_viewed_vlog = db.query(Vlog).order_by(Vlog.views_count.desc()).first()
|
||||
|
||||
# Longest event streak (consecutive events attended)
|
||||
# This would require more complex logic to calculate
|
||||
|
||||
return {
|
||||
"most_active_poster": {
|
||||
"user_id": post_counts[0] if post_counts else None,
|
||||
"username": post_counts[1] if post_counts else None,
|
||||
"full_name": post_counts[2] if post_counts else None,
|
||||
"post_count": post_counts[3] if post_counts else 0
|
||||
} if post_counts else None,
|
||||
"most_mentioned": {
|
||||
"user_id": mention_counts[0] if mention_counts else None,
|
||||
"username": mention_counts[1] if mention_counts else None,
|
||||
"full_name": mention_counts[2] if mention_counts else None,
|
||||
"mention_count": mention_counts[3] if mention_counts else 0
|
||||
} if mention_counts else None,
|
||||
"biggest_vlogger": {
|
||||
"user_id": vlog_counts[0] if vlog_counts else None,
|
||||
"username": vlog_counts[1] if vlog_counts else None,
|
||||
"full_name": vlog_counts[2] if vlog_counts else None,
|
||||
"vlog_count": vlog_counts[3] if vlog_counts else 0
|
||||
} if vlog_counts else None,
|
||||
"photo_addict": {
|
||||
"user_id": album_counts[0] if album_counts else None,
|
||||
"username": album_counts[1] if album_counts else None,
|
||||
"full_name": album_counts[2] if album_counts else None,
|
||||
"album_count": album_counts[3] if album_counts else 0
|
||||
} if album_counts else None,
|
||||
"event_organizer": {
|
||||
"user_id": event_counts[0] if event_counts else None,
|
||||
"username": event_counts[1] if event_counts else None,
|
||||
"full_name": event_counts[2] if event_counts else None,
|
||||
"event_count": event_counts[3] if event_counts else 0
|
||||
} if event_counts else None,
|
||||
"most_viewed_vlog": {
|
||||
"id": most_viewed_vlog.id,
|
||||
"title": most_viewed_vlog.title,
|
||||
"author_name": most_viewed_vlog.author.full_name,
|
||||
"views_count": most_viewed_vlog.views_count
|
||||
} if most_viewed_vlog else None
|
||||
}
|
||||
|
||||
@router.get("/user/{user_id}")
|
||||
async def get_user_stats(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get statistics for a specific user."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return {"error": "User not found"}
|
||||
|
||||
posts_count = db.query(Post).filter(Post.author_id == user_id).count()
|
||||
vlogs_count = db.query(Vlog).filter(Vlog.author_id == user_id).count()
|
||||
albums_count = db.query(Album).filter(Album.creator_id == user_id).count()
|
||||
events_created = db.query(Event).filter(Event.creator_id == user_id).count()
|
||||
|
||||
# Get participation stats
|
||||
participations = db.query(EventParticipation).filter(EventParticipation.user_id == user_id).all()
|
||||
present_count = sum(1 for p in participations if p.status == ParticipationStatus.PRESENT)
|
||||
absent_count = sum(1 for p in participations if p.status == ParticipationStatus.ABSENT)
|
||||
maybe_count = sum(1 for p in participations if p.status == ParticipationStatus.MAYBE)
|
||||
|
||||
# Total views on vlogs
|
||||
total_vlog_views = db.query(func.sum(Vlog.views_count)).filter(Vlog.author_id == user_id).scalar() or 0
|
||||
|
||||
return {
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"attendance_rate": user.attendance_rate,
|
||||
"member_since": user.created_at
|
||||
},
|
||||
"content_stats": {
|
||||
"posts_count": posts_count,
|
||||
"vlogs_count": vlogs_count,
|
||||
"albums_count": albums_count,
|
||||
"events_created": events_created,
|
||||
"total_vlog_views": total_vlog_views
|
||||
},
|
||||
"participation_stats": {
|
||||
"total_events": len(participations),
|
||||
"present_count": present_count,
|
||||
"absent_count": absent_count,
|
||||
"maybe_count": maybe_count
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/activity/user/{user_id}")
|
||||
async def get_user_activity(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get recent activity for a specific user."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return {"error": "User not found"}
|
||||
|
||||
# Get recent posts
|
||||
recent_posts = db.query(Post).filter(
|
||||
Post.author_id == user_id
|
||||
).order_by(Post.created_at.desc()).limit(5).all()
|
||||
|
||||
# Get recent vlogs
|
||||
recent_vlogs = db.query(Vlog).filter(
|
||||
Vlog.author_id == user_id
|
||||
).order_by(Vlog.created_at.desc()).limit(5).all()
|
||||
|
||||
# Get recent albums
|
||||
recent_albums = db.query(Album).filter(
|
||||
Album.creator_id == user_id
|
||||
).order_by(Album.created_at.desc()).limit(5).all()
|
||||
|
||||
# Get recent events created
|
||||
recent_events = db.query(Event).filter(
|
||||
Event.creator_id == user_id
|
||||
).order_by(Event.created_at.desc()).limit(5).all()
|
||||
|
||||
# Combine and sort by date
|
||||
activities = []
|
||||
|
||||
for post in recent_posts:
|
||||
activities.append({
|
||||
"id": post.id,
|
||||
"type": "post",
|
||||
"description": f"A publié : {post.content[:50]}...",
|
||||
"created_at": post.created_at,
|
||||
"link": f"/posts/{post.id}"
|
||||
})
|
||||
|
||||
for vlog in recent_vlogs:
|
||||
activities.append({
|
||||
"id": vlog.id,
|
||||
"type": "vlog",
|
||||
"description": f"A publié un vlog : {vlog.title}",
|
||||
"created_at": vlog.created_at,
|
||||
"link": f"/vlogs/{vlog.id}"
|
||||
})
|
||||
|
||||
for album in recent_albums:
|
||||
activities.append({
|
||||
"id": album.id,
|
||||
"type": "album",
|
||||
"description": f"A créé un album : {album.title}",
|
||||
"created_at": album.created_at,
|
||||
"link": f"/albums/{album.id}"
|
||||
})
|
||||
|
||||
for event in recent_events:
|
||||
activities.append({
|
||||
"id": event.id,
|
||||
"type": "event",
|
||||
"description": f"A créé un événement : {event.title}",
|
||||
"created_at": event.created_at,
|
||||
"link": f"/events/{event.id}"
|
||||
})
|
||||
|
||||
# Sort by creation date (most recent first)
|
||||
activities.sort(key=lambda x: x["created_at"], reverse=True)
|
||||
|
||||
return {
|
||||
"activity": activities[:10] # Return top 10 most recent activities
|
||||
}
|
||||
295
backend/api/routers/tickets.py
Normal file
295
backend/api/routers/tickets.py
Normal file
@@ -0,0 +1,295 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.ticket import Ticket, TicketType, TicketStatus, TicketPriority
|
||||
from models.user import User
|
||||
from schemas.ticket import TicketCreate, TicketUpdate, TicketResponse, TicketAdminUpdate
|
||||
from utils.security import get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[TicketResponse])
|
||||
async def get_user_tickets(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get current user's tickets."""
|
||||
tickets = db.query(Ticket).filter(Ticket.user_id == current_user.id).order_by(Ticket.created_at.desc()).all()
|
||||
return [format_ticket_response(ticket, db) for ticket in tickets]
|
||||
|
||||
@router.get("/admin", response_model=List[TicketResponse])
|
||||
async def get_all_tickets_admin(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
status_filter: Optional[TicketStatus] = None,
|
||||
type_filter: Optional[TicketType] = None,
|
||||
priority_filter: Optional[TicketPriority] = None
|
||||
):
|
||||
"""Get all tickets (admin only)."""
|
||||
query = db.query(Ticket)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Ticket.status == status_filter)
|
||||
if type_filter:
|
||||
query = query.filter(Ticket.ticket_type == type_filter)
|
||||
if priority_filter:
|
||||
query = query.filter(Ticket.priority == priority_filter)
|
||||
|
||||
tickets = query.order_by(Ticket.created_at.desc()).all()
|
||||
return [format_ticket_response(ticket, db) for ticket in tickets]
|
||||
|
||||
@router.get("/{ticket_id}", response_model=TicketResponse)
|
||||
async def get_ticket(
|
||||
ticket_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific ticket."""
|
||||
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ticket not found"
|
||||
)
|
||||
|
||||
# Users can only see their own tickets, admins can see all
|
||||
if ticket.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to view this ticket"
|
||||
)
|
||||
|
||||
return format_ticket_response(ticket, db)
|
||||
|
||||
@router.post("/", response_model=TicketResponse)
|
||||
async def create_ticket(
|
||||
title: str = Form(...),
|
||||
description: str = Form(...),
|
||||
ticket_type: str = Form("other"), # Changed from TicketType to str
|
||||
priority: str = Form("medium"), # Changed from TicketPriority to str
|
||||
screenshot: Optional[UploadFile] = File(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new ticket."""
|
||||
print(f"DEBUG - Received ticket data: title='{title}', description='{description}', ticket_type='{ticket_type}', priority='{priority}'")
|
||||
|
||||
# Validate and convert ticket_type
|
||||
try:
|
||||
ticket_type_enum = TicketType(ticket_type)
|
||||
print(f"DEBUG - Ticket type converted successfully: {ticket_type_enum}")
|
||||
except ValueError as e:
|
||||
print(f"DEBUG - Error converting ticket_type '{ticket_type}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid ticket type: {ticket_type}. Valid types: {[t.value for t in TicketType]}"
|
||||
)
|
||||
|
||||
# Validate and convert priority
|
||||
try:
|
||||
priority_enum = TicketPriority(priority)
|
||||
print(f"DEBUG - Priority converted successfully: {priority_enum}")
|
||||
except ValueError as e:
|
||||
print(f"DEBUG - Error converting priority '{priority}': {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid priority: {priority}. Valid priorities: {[p.value for p in TicketPriority]}"
|
||||
)
|
||||
|
||||
print("DEBUG - Starting ticket creation...")
|
||||
|
||||
# Create tickets directory
|
||||
tickets_dir = Path(settings.UPLOAD_PATH) / "tickets" / str(current_user.id)
|
||||
tickets_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"DEBUG - Created tickets directory: {tickets_dir}")
|
||||
|
||||
screenshot_path = None
|
||||
if screenshot:
|
||||
print(f"DEBUG - Processing screenshot: {screenshot.filename}, content_type: {screenshot.content_type}")
|
||||
# Validate file type
|
||||
if not screenshot.content_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Screenshot must be an image file"
|
||||
)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = screenshot.filename.split(".")[-1]
|
||||
filename = f"ticket_{uuid.uuid4()}.{file_extension}"
|
||||
file_path = tickets_dir / filename
|
||||
|
||||
# Save and optimize image
|
||||
contents = await screenshot.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Optimize image
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
if img.size[0] > 1920 or img.size[1] > 1080:
|
||||
img.thumbnail((1920, 1080), Image.Resampling.LANCZOS)
|
||||
img.save(file_path, quality=85, optimize=True)
|
||||
except Exception as e:
|
||||
print(f"Error optimizing screenshot: {e}")
|
||||
|
||||
screenshot_path = f"/tickets/{current_user.id}/{filename}"
|
||||
print(f"DEBUG - Screenshot saved: {screenshot_path}")
|
||||
|
||||
print("DEBUG - Creating ticket object...")
|
||||
ticket = Ticket(
|
||||
title=title,
|
||||
description=description,
|
||||
ticket_type=ticket_type_enum,
|
||||
priority=priority_enum,
|
||||
user_id=current_user.id,
|
||||
screenshot_path=screenshot_path
|
||||
)
|
||||
|
||||
print("DEBUG - Adding ticket to database...")
|
||||
db.add(ticket)
|
||||
db.commit()
|
||||
db.refresh(ticket)
|
||||
print(f"DEBUG - Ticket created successfully with ID: {ticket.id}")
|
||||
|
||||
return format_ticket_response(ticket, db)
|
||||
|
||||
@router.put("/{ticket_id}", response_model=TicketResponse)
|
||||
async def update_ticket(
|
||||
ticket_id: int,
|
||||
ticket_update: TicketUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update a ticket."""
|
||||
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ticket not found"
|
||||
)
|
||||
|
||||
# Users can only update their own tickets, admins can update all
|
||||
if ticket.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this ticket"
|
||||
)
|
||||
|
||||
# Regular users can only update title, description, and type
|
||||
if not current_user.is_admin:
|
||||
update_data = ticket_update.dict(exclude_unset=True)
|
||||
allowed_fields = ['title', 'description', 'ticket_type']
|
||||
update_data = {k: v for k, v in update_data.items() if k in allowed_fields}
|
||||
else:
|
||||
update_data = ticket_update.dict(exclude_unset=True)
|
||||
|
||||
# Update resolved_at if status is resolved
|
||||
if 'status' in update_data and update_data['status'] == TicketStatus.RESOLVED:
|
||||
update_data['resolved_at'] = datetime.utcnow()
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(ticket, field, value)
|
||||
|
||||
ticket.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(ticket)
|
||||
|
||||
return format_ticket_response(ticket, db)
|
||||
|
||||
@router.put("/{ticket_id}/admin", response_model=TicketResponse)
|
||||
async def update_ticket_admin(
|
||||
ticket_id: int,
|
||||
ticket_update: TicketAdminUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""Update a ticket as admin."""
|
||||
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ticket not found"
|
||||
)
|
||||
|
||||
update_data = ticket_update.dict(exclude_unset=True)
|
||||
|
||||
# Update resolved_at if status is resolved
|
||||
if 'status' in update_data and update_data['status'] == TicketStatus.RESOLVED:
|
||||
update_data['resolved_at'] = datetime.utcnow()
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(ticket, field, value)
|
||||
|
||||
ticket.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(ticket)
|
||||
|
||||
return format_ticket_response(ticket, db)
|
||||
|
||||
@router.delete("/{ticket_id}")
|
||||
async def delete_ticket(
|
||||
ticket_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a ticket."""
|
||||
ticket = db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ticket not found"
|
||||
)
|
||||
|
||||
# Users can only delete their own tickets, admins can delete all
|
||||
if ticket.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this ticket"
|
||||
)
|
||||
|
||||
# Delete screenshot if exists
|
||||
if ticket.screenshot_path:
|
||||
try:
|
||||
screenshot_file = settings.UPLOAD_PATH + ticket.screenshot_path
|
||||
if os.path.exists(screenshot_file):
|
||||
os.remove(screenshot_file)
|
||||
except Exception as e:
|
||||
print(f"Error deleting screenshot: {e}")
|
||||
|
||||
db.delete(ticket)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Ticket deleted successfully"}
|
||||
|
||||
def format_ticket_response(ticket: Ticket, db: Session) -> dict:
|
||||
"""Format ticket response with user information."""
|
||||
user = db.query(User).filter(User.id == ticket.user_id).first()
|
||||
assigned_admin = None
|
||||
if ticket.assigned_to:
|
||||
assigned_admin = db.query(User).filter(User.id == ticket.assigned_to).first()
|
||||
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"title": ticket.title,
|
||||
"description": ticket.description,
|
||||
"ticket_type": ticket.ticket_type,
|
||||
"status": ticket.status,
|
||||
"priority": ticket.priority,
|
||||
"user_id": ticket.user_id,
|
||||
"assigned_to": ticket.assigned_to,
|
||||
"screenshot_path": ticket.screenshot_path,
|
||||
"admin_notes": ticket.admin_notes,
|
||||
"created_at": ticket.created_at,
|
||||
"updated_at": ticket.updated_at,
|
||||
"resolved_at": ticket.resolved_at,
|
||||
"user_name": user.full_name if user else "Unknown",
|
||||
"user_email": user.email if user else "Unknown",
|
||||
"assigned_admin_name": assigned_admin.full_name if assigned_admin else None
|
||||
}
|
||||
111
backend/api/routers/users.py
Normal file
111
backend/api/routers/users.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
import os
|
||||
import uuid
|
||||
from PIL import Image
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.user import User
|
||||
from schemas.user import UserResponse, UserUpdate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.settings_service import SettingsService
|
||||
from pathlib import Path
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get current user information."""
|
||||
return current_user
|
||||
|
||||
@router.get("/", response_model=List[UserResponse])
|
||||
async def get_all_users(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get all users."""
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
return users
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific user by ID."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
return user
|
||||
|
||||
@router.put("/me", response_model=UserResponse)
|
||||
async def update_current_user(
|
||||
user_update: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update current user information."""
|
||||
if user_update.full_name:
|
||||
current_user.full_name = user_update.full_name
|
||||
if user_update.bio is not None:
|
||||
current_user.bio = user_update.bio
|
||||
if user_update.avatar_url is not None:
|
||||
current_user.avatar_url = user_update.avatar_url
|
||||
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
@router.post("/me/avatar", response_model=UserResponse)
|
||||
async def upload_avatar(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Upload user avatar."""
|
||||
# Validate file type
|
||||
if not SettingsService.is_file_type_allowed(db, file.content_type or "unknown"):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
|
||||
["image/jpeg", "image/png", "image/gif", "image/webp"])
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
# Create user directory
|
||||
user_dir = Path(settings.UPLOAD_PATH) / "avatars" / str(current_user.id)
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
file_extension = file.filename.split(".")[-1]
|
||||
filename = f"{uuid.uuid4()}.{file_extension}"
|
||||
file_path = user_dir / filename
|
||||
|
||||
# Save and resize image
|
||||
contents = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Resize image to 800x800 with high quality
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
img.thumbnail((800, 800))
|
||||
img.save(file_path, quality=95, optimize=True)
|
||||
except Exception as e:
|
||||
os.remove(file_path)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error processing image"
|
||||
)
|
||||
|
||||
# Update user avatar URL
|
||||
current_user.avatar_url = f"/avatars/{current_user.id}/{filename}"
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
|
||||
return current_user
|
||||
374
backend/api/routers/vlogs.py
Normal file
374
backend/api/routers/vlogs.py
Normal file
@@ -0,0 +1,374 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.vlog import Vlog, VlogLike, VlogComment
|
||||
from models.user import User
|
||||
from schemas.vlog import VlogCreate, VlogUpdate, VlogResponse, VlogCommentCreate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.video_utils import generate_video_thumbnail, get_video_duration
|
||||
from utils.settings_service import SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/", response_model=VlogResponse)
|
||||
async def create_vlog(
|
||||
vlog_data: VlogCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new vlog."""
|
||||
vlog = Vlog(
|
||||
author_id=current_user.id,
|
||||
**vlog_data.dict()
|
||||
)
|
||||
db.add(vlog)
|
||||
db.commit()
|
||||
db.refresh(vlog)
|
||||
return format_vlog_response(vlog, db, current_user.id)
|
||||
|
||||
@router.get("/", response_model=List[VlogResponse])
|
||||
async def get_vlogs(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
limit: int = 20,
|
||||
offset: int = 0
|
||||
):
|
||||
"""Get all vlogs."""
|
||||
vlogs = db.query(Vlog).order_by(Vlog.created_at.desc()).limit(limit).offset(offset).all()
|
||||
return [format_vlog_response(vlog, db, current_user.id) for vlog in vlogs]
|
||||
|
||||
@router.get("/{vlog_id}", response_model=VlogResponse)
|
||||
async def get_vlog(
|
||||
vlog_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a specific vlog."""
|
||||
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
|
||||
if not vlog:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
# Increment view count
|
||||
vlog.views_count += 1
|
||||
db.commit()
|
||||
|
||||
return format_vlog_response(vlog, db, current_user.id)
|
||||
|
||||
@router.put("/{vlog_id}", response_model=VlogResponse)
|
||||
async def update_vlog(
|
||||
vlog_id: int,
|
||||
vlog_update: VlogUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update a vlog."""
|
||||
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
|
||||
if not vlog:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
if vlog.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this vlog"
|
||||
)
|
||||
|
||||
for field, value in vlog_update.dict(exclude_unset=True).items():
|
||||
setattr(vlog, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(vlog)
|
||||
return format_vlog_response(vlog, db, current_user.id)
|
||||
|
||||
@router.delete("/{vlog_id}")
|
||||
async def delete_vlog(
|
||||
vlog_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a vlog."""
|
||||
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
|
||||
if not vlog:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
if vlog.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this vlog"
|
||||
)
|
||||
|
||||
# Delete video files
|
||||
try:
|
||||
if vlog.video_url:
|
||||
os.remove(settings.UPLOAD_PATH + vlog.video_url)
|
||||
if vlog.thumbnail_url:
|
||||
os.remove(settings.UPLOAD_PATH + vlog.thumbnail_url)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(vlog)
|
||||
db.commit()
|
||||
return {"message": "Vlog deleted successfully"}
|
||||
|
||||
@router.post("/{vlog_id}/like")
|
||||
async def toggle_vlog_like(
|
||||
vlog_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Toggle like on a vlog."""
|
||||
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
|
||||
if not vlog:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
existing_like = db.query(VlogLike).filter(
|
||||
VlogLike.vlog_id == vlog_id,
|
||||
VlogLike.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# Unlike
|
||||
db.delete(existing_like)
|
||||
vlog.likes_count = max(0, vlog.likes_count - 1)
|
||||
message = "Like removed"
|
||||
else:
|
||||
# Like
|
||||
like = VlogLike(vlog_id=vlog_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
vlog.likes_count += 1
|
||||
message = "Vlog liked"
|
||||
|
||||
db.commit()
|
||||
return {"message": message, "likes_count": vlog.likes_count}
|
||||
|
||||
@router.post("/{vlog_id}/comment")
|
||||
async def add_vlog_comment(
|
||||
vlog_id: int,
|
||||
comment_data: VlogCommentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Add a comment to a vlog."""
|
||||
vlog = db.query(Vlog).filter(Vlog.id == vlog_id).first()
|
||||
if not vlog:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Vlog not found"
|
||||
)
|
||||
|
||||
comment = VlogComment(
|
||||
vlog_id=vlog_id,
|
||||
user_id=current_user.id,
|
||||
content=comment_data.content
|
||||
)
|
||||
db.add(comment)
|
||||
db.commit()
|
||||
db.refresh(comment)
|
||||
|
||||
return {
|
||||
"message": "Comment added successfully",
|
||||
"comment": {
|
||||
"id": comment.id,
|
||||
"content": comment.content,
|
||||
"user_id": comment.user_id,
|
||||
"username": current_user.username,
|
||||
"full_name": current_user.full_name,
|
||||
"avatar_url": current_user.avatar_url,
|
||||
"created_at": comment.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@router.delete("/{vlog_id}/comment/{comment_id}")
|
||||
async def delete_vlog_comment(
|
||||
vlog_id: int,
|
||||
comment_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete a comment from a vlog."""
|
||||
comment = db.query(VlogComment).filter(
|
||||
VlogComment.id == comment_id,
|
||||
VlogComment.vlog_id == vlog_id
|
||||
).first()
|
||||
|
||||
if not comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Comment not found"
|
||||
)
|
||||
|
||||
if comment.user_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this comment"
|
||||
)
|
||||
|
||||
db.delete(comment)
|
||||
db.commit()
|
||||
return {"message": "Comment deleted successfully"}
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_vlog_video(
|
||||
title: str = Form(...),
|
||||
description: str = Form(None),
|
||||
video: UploadFile = File(...),
|
||||
thumbnail: UploadFile = File(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Upload a vlog video."""
|
||||
# Validate video file
|
||||
if not SettingsService.is_file_type_allowed(db, video.content_type or "unknown"):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
|
||||
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid video type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
# Check file size
|
||||
video_content = await video.read()
|
||||
max_size = SettingsService.get_max_upload_size(db, video.content_type or "video/mp4")
|
||||
if len(video_content) > max_size:
|
||||
max_size_mb = max_size // (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"Video file too large. Maximum size: {max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Create vlog directory
|
||||
vlog_dir = Path(settings.UPLOAD_PATH) / "vlogs" / str(current_user.id)
|
||||
vlog_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save video
|
||||
video_extension = video.filename.split(".")[-1]
|
||||
video_filename = f"{uuid.uuid4()}.{video_extension}"
|
||||
video_path = vlog_dir / video_filename
|
||||
|
||||
with open(video_path, "wb") as f:
|
||||
f.write(video_content)
|
||||
|
||||
# Process thumbnail
|
||||
thumbnail_url = None
|
||||
if thumbnail:
|
||||
if thumbnail.content_type not in settings.ALLOWED_IMAGE_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid thumbnail type"
|
||||
)
|
||||
|
||||
thumbnail_content = await thumbnail.read()
|
||||
thumbnail_extension = thumbnail.filename.split(".")[-1]
|
||||
thumbnail_filename = f"thumb_{uuid.uuid4()}.{thumbnail_extension}"
|
||||
thumbnail_path = vlog_dir / thumbnail_filename
|
||||
|
||||
with open(thumbnail_path, "wb") as f:
|
||||
f.write(thumbnail_content)
|
||||
|
||||
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
|
||||
else:
|
||||
# Generate automatic thumbnail from video
|
||||
try:
|
||||
thumbnail_filename = f"auto_thumb_{uuid.uuid4()}.jpg"
|
||||
thumbnail_path = vlog_dir / thumbnail_filename
|
||||
|
||||
if generate_video_thumbnail(str(video_path), str(thumbnail_path)):
|
||||
thumbnail_url = f"/vlogs/{current_user.id}/{thumbnail_filename}"
|
||||
except Exception as e:
|
||||
print(f"Error generating automatic thumbnail: {e}")
|
||||
# Continue without thumbnail if generation fails
|
||||
|
||||
# Get video duration
|
||||
duration = None
|
||||
try:
|
||||
duration = int(get_video_duration(str(video_path)))
|
||||
except Exception as e:
|
||||
print(f"Error getting video duration: {e}")
|
||||
|
||||
# Create vlog record
|
||||
vlog = Vlog(
|
||||
author_id=current_user.id,
|
||||
title=title,
|
||||
description=description,
|
||||
video_url=f"/vlogs/{current_user.id}/{video_filename}",
|
||||
thumbnail_url=thumbnail_url,
|
||||
duration=duration
|
||||
)
|
||||
db.add(vlog)
|
||||
db.commit()
|
||||
db.refresh(vlog)
|
||||
|
||||
return format_vlog_response(vlog, db, current_user.id)
|
||||
|
||||
def format_vlog_response(vlog: Vlog, db: Session, current_user_id: int) -> dict:
|
||||
"""Format vlog response with author information, likes, and comments."""
|
||||
# Check if current user liked this vlog
|
||||
is_liked = db.query(VlogLike).filter(
|
||||
VlogLike.vlog_id == vlog.id,
|
||||
VlogLike.user_id == current_user_id
|
||||
).first() is not None
|
||||
|
||||
# Format likes
|
||||
likes = []
|
||||
for like in vlog.likes:
|
||||
user = db.query(User).filter(User.id == like.user_id).first()
|
||||
if user:
|
||||
likes.append({
|
||||
"id": like.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"created_at": like.created_at
|
||||
})
|
||||
|
||||
# Format comments
|
||||
comments = []
|
||||
for comment in vlog.comments:
|
||||
user = db.query(User).filter(User.id == comment.user_id).first()
|
||||
if user:
|
||||
comments.append({
|
||||
"id": comment.id,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"content": comment.content,
|
||||
"created_at": comment.created_at,
|
||||
"updated_at": comment.updated_at
|
||||
})
|
||||
|
||||
return {
|
||||
"id": vlog.id,
|
||||
"author_id": vlog.author_id,
|
||||
"title": vlog.title,
|
||||
"description": vlog.description,
|
||||
"video_url": vlog.video_url,
|
||||
"thumbnail_url": vlog.thumbnail_url,
|
||||
"duration": vlog.duration,
|
||||
"views_count": vlog.views_count,
|
||||
"likes_count": vlog.likes_count,
|
||||
"created_at": vlog.created_at,
|
||||
"updated_at": vlog.updated_at,
|
||||
"author_name": vlog.author.full_name,
|
||||
"author_avatar": vlog.author.avatar_url,
|
||||
"is_liked": is_liked,
|
||||
"likes": likes,
|
||||
"comments": comments
|
||||
}
|
||||
Reference in New Issue
Block a user