update
This commit is contained in:
0
Backend/src/reviews/__init__.py
Normal file
0
Backend/src/reviews/__init__.py
Normal file
0
Backend/src/reviews/models/__init__.py
Normal file
0
Backend/src/reviews/models/__init__.py
Normal file
14
Backend/src/reviews/models/favorite.py
Normal file
14
Backend/src/reviews/models/favorite.py
Normal 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')
|
||||
29
Backend/src/reviews/models/review.py
Normal file
29
Backend/src/reviews/models/review.py
Normal 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'),
|
||||
)
|
||||
0
Backend/src/reviews/routes/__init__.py
Normal file
0
Backend/src/reviews/routes/__init__.py
Normal file
86
Backend/src/reviews/routes/favorite_routes.py
Normal file
86
Backend/src/reviews/routes/favorite_routes.py
Normal 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))
|
||||
271
Backend/src/reviews/routes/review_routes.py
Normal file
271
Backend/src/reviews/routes/review_routes.py
Normal 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))
|
||||
0
Backend/src/reviews/schemas/__init__.py
Normal file
0
Backend/src/reviews/schemas/__init__.py
Normal file
23
Backend/src/reviews/schemas/review.py
Normal file
23
Backend/src/reviews/schemas/review.py
Normal 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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
Backend/src/reviews/services/__init__.py
Normal file
0
Backend/src/reviews/services/__init__.py
Normal file
Reference in New Issue
Block a user