This commit is contained in:
Iliyan Angelov
2025-12-02 10:42:35 +02:00
parent 4b053ce703
commit 2d770dd27b
24 changed files with 1766 additions and 1402 deletions

View File

@@ -562,3 +562,122 @@ async def upload_blog_image(
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error uploading image: {str(e)}')
@router.get('/admin/tags', response_model=dict)
async def get_all_tags(
current_user: User = Depends(authorize_roles('admin')),
db: Session = Depends(get_db)
):
"""Get all unique tags from all blog posts (admin only)"""
try:
all_posts = db.query(BlogPost).all()
all_unique_tags = set()
tag_usage = {} # Track how many posts use each tag
for post in all_posts:
if post.tags:
try:
tags_list = json.loads(post.tags)
for tag in tags_list:
all_unique_tags.add(tag)
tag_usage[tag] = tag_usage.get(tag, 0) + 1
except:
pass
tags_with_usage = [
{'name': tag, 'usage_count': tag_usage.get(tag, 0)}
for tag in sorted(all_unique_tags)
]
return success_response({
'tags': tags_with_usage,
'total': len(tags_with_usage)
})
except Exception as e:
logger.error(f"Error in get_all_tags: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.put('/admin/tags/rename', response_model=dict)
async def rename_tag(
old_tag: str = Query(..., description='Old tag name'),
new_tag: str = Query(..., description='New tag name'),
current_user: User = Depends(authorize_roles('admin')),
db: Session = Depends(get_db)
):
"""Rename a tag across all blog posts (admin only)"""
try:
if not old_tag or not new_tag:
raise HTTPException(status_code=400, detail='Both old_tag and new_tag are required')
if old_tag == new_tag:
raise HTTPException(status_code=400, detail='Old and new tag names must be different')
# Get all posts that contain the old tag
all_posts = db.query(BlogPost).all()
updated_count = 0
for post in all_posts:
if post.tags:
try:
tags_list = json.loads(post.tags)
if old_tag in tags_list:
# Replace old tag with new tag
tags_list = [new_tag if tag == old_tag else tag for tag in tags_list]
post.tags = json.dumps(tags_list)
updated_count += 1
except:
pass
db.commit()
return success_response({
'old_tag': old_tag,
'new_tag': new_tag,
'updated_posts': updated_count
}, message=f'Tag renamed successfully. Updated {updated_count} post(s).')
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Error in rename_tag: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.delete('/admin/tags', response_model=dict)
async def delete_tag(
tag: str = Query(..., description='Tag name to delete'),
current_user: User = Depends(authorize_roles('admin')),
db: Session = Depends(get_db)
):
"""Delete a tag from all blog posts (admin only)"""
try:
if not tag:
raise HTTPException(status_code=400, detail='Tag name is required')
# Get all posts that contain the tag
all_posts = db.query(BlogPost).all()
updated_count = 0
for post in all_posts:
if post.tags:
try:
tags_list = json.loads(post.tags)
if tag in tags_list:
# Remove the tag
tags_list = [t for t in tags_list if t != tag]
post.tags = json.dumps(tags_list) if tags_list else None
updated_count += 1
except:
pass
db.commit()
return success_response({
'deleted_tag': tag,
'updated_posts': updated_count
}, message=f'Tag deleted successfully. Updated {updated_count} post(s).')
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Error in delete_tag: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -10,7 +10,7 @@ from ...security.middleware.auth import get_current_user, authorize_roles
from ...auth.models.user import User
from ..models.room import Room, RoomStatus
from ..models.room_type import RoomType
from ..schemas.room import CreateRoomRequest, UpdateRoomRequest, BulkDeleteRoomsRequest
from ..schemas.room import CreateRoomRequest, UpdateRoomRequest, BulkDeleteRoomsRequest, UpdateAmenityRequest
from ...shared.utils.response_helpers import success_response
from ...reviews.models.review import Review, ReviewStatus
from ...bookings.models.booking import Booking, BookingStatus
@@ -72,6 +72,112 @@ async def get_amenities(db: Session=Depends(get_db)):
logger.error(f'Error fetching amenities: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.put('/amenities/{old_name}', dependencies=[Depends(authorize_roles('admin'))])
async def update_amenity(old_name: str, request: UpdateAmenityRequest, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
"""Update/rename an amenity across all rooms and room types."""
try:
import json
updated_count = 0
# Update in room types
room_types = db.query(RoomType).all()
for rt in room_types:
if rt.amenities:
amenities_list = []
if isinstance(rt.amenities, list):
amenities_list = rt.amenities
elif isinstance(rt.amenities, str):
try:
amenities_list = json.loads(rt.amenities)
except:
amenities_list = [s.strip() for s in rt.amenities.split(',') if s.strip()]
if old_name in amenities_list:
amenities_list = [request.new_name if a == old_name else a for a in amenities_list]
rt.amenities = amenities_list
updated_count += 1
# Update in rooms
rooms = db.query(Room).all()
for room in rooms:
if room.amenities:
amenities_list = []
if isinstance(room.amenities, list):
amenities_list = room.amenities
elif isinstance(room.amenities, str):
try:
amenities_list = json.loads(room.amenities)
except:
amenities_list = [s.strip() for s in room.amenities.split(',') if s.strip()]
if old_name in amenities_list:
amenities_list = [request.new_name if a == old_name else a for a in amenities_list]
room.amenities = amenities_list
updated_count += 1
db.commit()
return success_response(
data={'updated_count': updated_count, 'old_name': old_name, 'new_name': request.new_name},
message=f'Amenity "{old_name}" updated to "{request.new_name}" in {updated_count} location(s)'
)
except Exception as e:
db.rollback()
logger.error(f'Error updating amenity: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.delete('/amenities/{amenity_name}', dependencies=[Depends(authorize_roles('admin'))])
async def delete_amenity(amenity_name: str, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
"""Remove an amenity from all rooms and room types."""
try:
import json
updated_count = 0
# Remove from room types
room_types = db.query(RoomType).all()
for rt in room_types:
if rt.amenities:
amenities_list = []
if isinstance(rt.amenities, list):
amenities_list = rt.amenities
elif isinstance(rt.amenities, str):
try:
amenities_list = json.loads(rt.amenities)
except:
amenities_list = [s.strip() for s in rt.amenities.split(',') if s.strip()]
if amenity_name in amenities_list:
amenities_list = [a for a in amenities_list if a != amenity_name]
rt.amenities = amenities_list if amenities_list else []
updated_count += 1
# Remove from rooms
rooms = db.query(Room).all()
for room in rooms:
if room.amenities:
amenities_list = []
if isinstance(room.amenities, list):
amenities_list = room.amenities
elif isinstance(room.amenities, str):
try:
amenities_list = json.loads(room.amenities)
except:
amenities_list = [s.strip() for s in room.amenities.split(',') if s.strip()]
if amenity_name in amenities_list:
amenities_list = [a for a in amenities_list if a != amenity_name]
room.amenities = amenities_list if amenities_list else []
updated_count += 1
db.commit()
return success_response(
data={'updated_count': updated_count, 'amenity_name': amenity_name},
message=f'Amenity "{amenity_name}" removed from {updated_count} location(s)'
)
except Exception as e:
db.rollback()
logger.error(f'Error deleting amenity: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get('/room-types')
async def get_room_types(db: Session=Depends(get_db)):
"""Get all room types for dropdowns and forms."""
@@ -457,7 +563,21 @@ async def upload_room_images(id: int, images: List[UploadFile]=File(...), curren
continue
await f.write(content)
image_urls.append(f'/uploads/rooms/{filename}')
# Handle existing_images - it might be a list, a JSON string, or None
existing_images = room.images or []
if isinstance(existing_images, str):
# If it's a string, try to parse it as JSON
import json
try:
existing_images = json.loads(existing_images)
except (json.JSONDecodeError, TypeError):
# If parsing fails, treat as empty list
existing_images = []
# Ensure it's a list
if not isinstance(existing_images, list):
existing_images = []
updated_images = existing_images + image_urls
room.images = updated_images
db.commit()
@@ -483,7 +603,21 @@ async def delete_room_images(id: int, image_url: str=Query(..., description='Ima
if not normalized_url.startswith('/'):
normalized_url = f'/{normalized_url}'
filename = Path(normalized_url).name
# Handle existing_images - it might be a list, a JSON string, or None
existing_images = room.images or []
if isinstance(existing_images, str):
# If it's a string, try to parse it as JSON
import json
try:
existing_images = json.loads(existing_images)
except (json.JSONDecodeError, TypeError):
# If parsing fails, treat as empty list
existing_images = []
# Ensure it's a list
if not isinstance(existing_images, list):
existing_images = []
updated_images = []
for img in existing_images:
stored_path = img if img.startswith('/') else f'/{img}'

View File

@@ -105,3 +105,8 @@ class BulkDeleteRoomsRequest(BaseModel):
raise ValueError('All room IDs must be positive integers')
return v
class UpdateAmenityRequest(BaseModel):
"""Schema for updating/renaming an amenity."""
new_name: str = Field(..., min_length=1, max_length=100, description="New name for the amenity")

View File

@@ -54,11 +54,14 @@ async def get_rooms_with_ratings(db: Session, rooms: List[Room], base_url: str)
result.append(room_dict)
return result
def get_predefined_amenities() -> List[str]:
return ['Free WiFi', 'WiFi', 'High-Speed Internet', 'WiFi in Room', 'Flat-Screen TV', 'TV', 'Cable TV', 'Satellite TV', 'Smart TV', 'Netflix', 'Streaming Services', 'DVD Player', 'Stereo System', 'Radio', 'iPod Dock', 'Air Conditioning', 'AC', 'Heating', 'Climate Control', 'Ceiling Fan', 'Air Purifier', 'Private Bathroom', 'Ensuite Bathroom', 'Bathtub', 'Jacuzzi Bathtub', 'Hot Tub', 'Shower', 'Rain Shower', 'Walk-in Shower', 'Bidet', 'Hair Dryer', 'Hairdryer', 'Bathrobes', 'Slippers', 'Toiletries', 'Premium Toiletries', 'Towels', 'Mini Bar', 'Minibar', 'Refrigerator', 'Fridge', 'Microwave', 'Coffee Maker', 'Electric Kettle', 'Tea Making Facilities', 'Coffee Machine', 'Nespresso Machine', 'Kitchenette', 'Dining Table', 'Room Service', 'Breakfast Included', 'Breakfast', 'Complimentary Water', 'Bottled Water', 'Desk', 'Writing Desk', 'Office Desk', 'Work Desk', 'Sofa', 'Sitting Area', 'Lounge Area', 'Dining Area', 'Separate Living Area', 'Wardrobe', 'Closet', 'Dresser', 'Mirror', 'Full-Length Mirror', 'Seating Area', 'King Size Bed', 'Queen Size Bed', 'Double Bed', 'Twin Beds', 'Single Bed', 'Extra Bedding', 'Pillow Menu', 'Premium Bedding', 'Blackout Curtains', 'Soundproofing', 'Safe', 'In-Room Safe', 'Safety Deposit Box', 'Smoke Detector', 'Fire Extinguisher', 'Security System', 'Key Card Access', 'Door Lock', 'Pepper Spray', 'USB Charging Ports', 'USB Ports', 'USB Outlets', 'Power Outlets', 'Charging Station', 'Laptop Safe', 'HDMI Port', 'Phone', 'Desk Phone', 'Wake-Up Service', 'Alarm Clock', 'Digital Clock', 'Balcony', 'Private Balcony', 'Terrace', 'Patio', 'City View', 'Ocean View', 'Sea View', 'Mountain View', 'Garden View', 'Pool View', 'Park View', 'Window', 'Large Windows', 'Floor-to-Ceiling Windows', '24-Hour Front Desk', '24 Hour Front Desk', '24/7 Front Desk', 'Concierge Service', 'Butler Service', 'Housekeeping', 'Daily Housekeeping', 'Turndown Service', 'Laundry Service', 'Dry Cleaning', 'Ironing Service', 'Luggage Storage', 'Bell Service', 'Valet Parking', 'Parking', 'Free Parking', 'Airport Shuttle', 'Shuttle Service', 'Car Rental', 'Taxi Service', 'Gym Access', 'Fitness Center', 'Fitness Room', 'Spa Access', 'Spa', 'Sauna', 'Steam Room', 'Hot Tub', 'Massage Service', 'Beauty Services', 'Swimming Pool', 'Pool', 'Indoor Pool', 'Outdoor Pool', 'Infinity Pool', 'Pool Access', 'Golf Course', 'Tennis Court', 'Beach Access', 'Water Sports', 'Business Center', 'Meeting Room', 'Conference Room', 'Fax Service', 'Photocopying', 'Printing Service', 'Secretarial Services', 'Wheelchair Accessible', 'Accessible Room', 'Elevator Access', 'Ramp Access', 'Accessible Bathroom', 'Lowered Sink', 'Grab Bars', 'Hearing Accessible', 'Visual Alarm', 'Family Room', 'Kids Welcome', 'Baby Crib', 'Extra Bed', 'Crib', 'Childcare Services', 'Pets Allowed', 'Pet Friendly', 'Smoking Room', 'Non-Smoking Room', 'No Smoking', 'Interconnecting Rooms', 'Adjoining Rooms', 'Suite', 'Separate Bedroom', 'Kitchen', 'Full Kitchen', 'Dishwasher', 'Oven', 'Stove', 'Washing Machine', 'Dryer', 'Iron', 'Ironing Board', 'Clothes Rack', 'Umbrella', 'Shoe Shine Service', 'Fireplace', 'Jacuzzi', 'Steam Shower', 'Spa Bath', 'Bidet Toilet', 'Smart Home System', 'Lighting Control', 'Curtain Control', 'Automated Systems', 'Personalized Service', 'VIP Treatment', 'Butler', 'Private Entrance', 'Private Elevator', 'Panic Button', 'Blu-ray Player', 'Gaming Console', 'PlayStation', 'Xbox', 'Sound System', 'Surround Sound', 'Music System', 'Library', 'Reading Room', 'Study Room', 'Private Pool', 'Private Garden', 'Yard', 'Courtyard', 'Outdoor Furniture', 'BBQ Facilities', 'Picnic Area']
async def get_amenities_list(db: Session) -> List[str]:
all_amenities = set(get_predefined_amenities())
"""
Get all unique amenities from the database only.
Aggregates amenities from room_types and rooms tables.
"""
all_amenities = set()
# Get amenities from room types
room_types = db.query(RoomType.amenities).all()
for rt in room_types:
if rt.amenities:
@@ -74,6 +77,8 @@ async def get_amenities_list(db: Session) -> List[str]:
all_amenities.update([s.strip() for s in rt.amenities.split(',') if s.strip()])
except:
all_amenities.update([s.strip() for s in rt.amenities.split(',') if s.strip()])
# Get amenities from rooms
rooms = db.query(Room.amenities).all()
for r in rooms:
if r.amenities:
@@ -89,4 +94,5 @@ async def get_amenities_list(db: Session) -> List[str]:
all_amenities.update([s.strip() for s in r.amenities.split(',') if s.strip()])
except:
all_amenities.update([s.strip() for s in r.amenities.split(',') if s.strip()])
return sorted(list(all_amenities))