This commit is contained in:
Iliyan Angelov
2025-11-30 22:43:09 +02:00
parent 24b40450dd
commit 39fcfff811
1610 changed files with 5442 additions and 1383 deletions

View File

View File

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ...shared.config.database import Base
class Favorite(Base):
__tablename__ = 'favorites'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
user = relationship('User', back_populates='favorites')
room = relationship('Room', back_populates='favorites')

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, Text, Enum, ForeignKey, DateTime, Index
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ...shared.config.database import Base
class ReviewStatus(str, enum.Enum):
pending = 'pending'
approved = 'approved'
rejected = 'rejected'
class Review(Base):
__tablename__ = 'reviews'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True)
rating = Column(Integer, nullable=False)
comment = Column(Text, nullable=False)
status = Column(Enum(ReviewStatus), nullable=False, default=ReviewStatus.pending)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
user = relationship('User', back_populates='reviews')
room = relationship('Room', back_populates='reviews')
# Index for review status and room queries
__table_args__ = (
Index('idx_review_status', 'status'),
Index('idx_review_room_status', 'room_id', 'status'),
)

View File

View File

@@ -0,0 +1,86 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func
from ...shared.config.database import get_db
from ...security.middleware.auth import get_current_user
from ...auth.models.user import User
from ...auth.models.role import Role
from ..models.favorite import Favorite
from ...rooms.models.room import Room
from ...rooms.models.room_type import RoomType
from ..models.review import Review, ReviewStatus
router = APIRouter(prefix='/favorites', tags=['favorites'])
@router.get('/')
async def get_favorites(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot have favorites')
try:
favorites = db.query(Favorite).filter(Favorite.user_id == current_user.id).order_by(Favorite.created_at.desc()).all()
result = []
for favorite in favorites:
if not favorite.room:
continue
room = favorite.room
review_stats = db.query(func.avg(Review.rating).label('average_rating'), func.count(Review.id).label('total_reviews')).filter(Review.room_id == room.id, Review.status == ReviewStatus.approved).first()
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if hasattr(room.status, 'value') else room.status, 'price': float(room.price) if room.price else 0.0, 'featured': room.featured, 'description': room.description, 'amenities': room.amenities, 'images': room.images or [], 'average_rating': round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None, 'total_reviews': review_stats.total_reviews or 0 if review_stats else 0}
if room.room_type:
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities}
favorite_dict = {'id': favorite.id, 'user_id': favorite.user_id, 'room_id': favorite.room_id, 'room': room_dict, 'created_at': favorite.created_at.isoformat() if favorite.created_at else None}
result.append(favorite_dict)
return {'status': 'success', 'data': {'favorites': result, 'total': len(result)}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post('/{room_id}')
async def add_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot add favorites')
try:
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail='Room not found')
existing = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
if existing:
raise HTTPException(status_code=400, detail='Room already in favorites list')
favorite = Favorite(user_id=current_user.id, room_id=room_id)
db.add(favorite)
db.commit()
db.refresh(favorite)
return {'status': 'success', 'message': 'Added to favorites list', 'data': {'favorite': favorite}}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete('/{room_id}')
async def remove_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot remove favorites')
try:
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
if not favorite:
raise HTTPException(status_code=404, detail='Room not found in favorites list')
db.delete(favorite)
db.commit()
return {'status': 'success', 'message': 'Removed from favorites list'}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get('/check/{room_id}')
async def check_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
return {'status': 'success', 'data': {'isFavorited': False}}
try:
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
return {'status': 'success', 'data': {'isFavorited': favorite is not None}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,271 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from ...shared.utils.response_helpers import success_response, error_response
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func
from typing import Optional
from ...shared.config.database import get_db
from ...shared.config.logging_config import get_logger
from ...security.middleware.auth import get_current_user, authorize_roles
from ...auth.models.user import User
from ..models.review import Review, ReviewStatus
from ...rooms.models.room import Room
from ..schemas.review import CreateReviewRequest
logger = get_logger(__name__)
router = APIRouter(prefix='/reviews', tags=['reviews'])
@router.get('/room/{room_id}')
async def get_room_reviews(
room_id: int,
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
try:
# Calculate average rating and total reviews for approved reviews
stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
Review.room_id == room_id,
Review.status == ReviewStatus.approved
).first()
average_rating = round(float(stats.average_rating or 0), 1) if stats and stats.average_rating else 0
total_reviews = stats.total_reviews or 0 if stats else 0
query = db.query(Review).options(
joinedload(Review.user)
).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved)
total = query.count()
offset = (page - 1) * limit
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
result = []
for review in reviews:
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
if review.user:
review_dict['user'] = {
'id': review.user.id,
'full_name': review.user.full_name,
'name': review.user.full_name, # Add name alias for compatibility
'email': review.user.email
}
result.append(review_dict)
return {
'status': 'success',
'data': {
'reviews': result,
'average_rating': average_rating,
'total_reviews': total_reviews,
'pagination': {
'total': total,
'page': page,
'limit': limit,
'totalPages': (total + limit - 1) // limit
}
}
}
except Exception as e:
db.rollback()
logger.error(f'Error fetching room reviews: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
try:
query = db.query(Review).options(
joinedload(Review.user),
joinedload(Review.room).joinedload(Room.room_type)
)
if status_filter:
try:
query = query.filter(Review.status == ReviewStatus(status_filter))
except ValueError:
pass
total = query.count()
offset = (page - 1) * limit
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
result = []
for review in reviews:
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
if review.user:
review_dict['user'] = {
'id': review.user.id,
'full_name': review.user.full_name,
'name': review.user.full_name, # Add name alias for frontend compatibility
'email': review.user.email,
'phone': review.user.phone
}
if review.room:
room_dict = {
'id': review.room.id,
'room_number': review.room.room_number
}
# Include room type if available
if review.room.room_type:
room_dict['room_type'] = {
'id': review.room.room_type.id,
'name': review.room.room_type.name
}
review_dict['room'] = room_dict
result.append(review_dict)
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
except Exception as e:
db.rollback()
logger.error(f'Error fetching all reviews: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.post('/')
async def create_review(review_data: CreateReviewRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
try:
room_id = review_data.room_id
rating = review_data.rating
# Ensure comment is a string (not None) since the model requires it
comment = review_data.comment if review_data.comment else ''
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(
status_code=404,
detail=error_response(message='Room not found')
)
existing = db.query(Review).filter(Review.user_id == current_user.id, Review.room_id == room_id).first()
if existing:
raise HTTPException(
status_code=400,
detail=error_response(message='You have already reviewed this room')
)
review = Review(
user_id=current_user.id,
room_id=room_id,
rating=rating,
comment=comment or '', # Ensure comment is a string
status=ReviewStatus.pending
)
db.add(review)
db.commit()
db.refresh(review)
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review submitted successfully and is pending approval'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error creating review: {str(e)}', exc_info=True)
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while creating the review')
)
@router.patch('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
async def approve_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
review.status = ReviewStatus.approved
db.commit()
db.refresh(review)
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review approved successfully'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error approving review: {str(e)}', exc_info=True)
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while approving the review')
)
@router.patch('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
async def reject_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
review.status = ReviewStatus.rejected
db.commit()
db.refresh(review)
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review rejected successfully'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error rejecting review: {str(e)}', exc_info=True)
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while rejecting the review')
)
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
async def delete_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail='Review not found')
db.delete(review)
db.commit()
return {'status': 'success', 'message': 'Review deleted successfully'}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error deleting review: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

View File

View File

@@ -0,0 +1,23 @@
"""
Pydantic schemas for review-related requests and responses.
"""
from pydantic import BaseModel, Field
from typing import Optional
class CreateReviewRequest(BaseModel):
"""Schema for creating a review."""
room_id: int = Field(..., gt=0, description="Room ID")
rating: int = Field(..., ge=1, le=5, description="Rating from 1 to 5")
comment: str = Field(..., min_length=1, max_length=2000, description="Review comment")
model_config = {
"json_schema_extra": {
"example": {
"room_id": 1,
"rating": 5,
"comment": "Great room, excellent service!"
}
}
}

View File