#!/usr/bin/env python3 """ Migration Script: Fix Ratings and Reviews Count This script ensures all listings and users have correct rating/reviews_count: 1. Users: Recalculates rating and reviews_count from actual reviews 2. Short-stay listings: Recalculates rating and reviews_count from actual reviews 3. Non-short-stay listings: Will use owner's rating (handled at fetch time) Run with: python scripts/fix_ratings.py """ import asyncio import sys import os # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from motor.motor_asyncio import AsyncIOMotorClient from bson import ObjectId from datetime import datetime # Import app config to get correct MongoDB URL from app.config import settings async def get_database(): """Connect to MongoDB using app settings""" client = AsyncIOMotorClient( settings.MONGODB_URL, serverSelectionTimeoutMS=5000, ) # Test connection await client.admin.command("ping") print(f" šŸ“¦ Database: {settings.MONGODB_DATABASE}") return client[settings.MONGODB_DATABASE] async def recalculate_user_ratings(db): """ Recalculate rating and reviews_count for all users based on actual reviews in the reviews collection. """ print("\n" + "="*60) print("STEP 2: Recalculating User Ratings") print("="*60) # Get all users users_cursor = db.users.find({}) users = await users_cursor.to_list(length=None) print(f"Found {len(users)} users to process") updated_count = 0 for user in users: user_id = str(user["_id"]) # Find all reviews where this user is the target reviews_cursor = db.reviews.find({ "target_type": "user", "target_id": user_id }) reviews = await reviews_cursor.to_list(length=None) # Calculate new rating if reviews: total_rating = sum(r.get("rating", 0) for r in reviews) reviews_count = len(reviews) avg_rating = round(total_rating / reviews_count, 2) else: reviews_count = 0 avg_rating = 0.0 # Get current values current_rating = user.get("rating", 0.0) current_count = user.get("reviews_count", 0) # Update if different or missing if current_rating != avg_rating or current_count != reviews_count: await db.users.update_one( {"_id": user["_id"]}, {"$set": { "rating": avg_rating, "reviews_count": reviews_count }} ) updated_count += 1 print(f" āœ… User {user.get('firstName', 'Unknown')} {user.get('lastName', '')}: " f"{current_rating} → {avg_rating} ({reviews_count} reviews)") print(f"\nšŸ“Š Updated {updated_count} users") return updated_count async def recalculate_listing_ratings(db): """ Recalculate rating and reviews_count for all SHORT-STAY listings based on actual reviews in the reviews collection. Non-short-stay listings use the owner's rating (handled at fetch time). """ print("\n" + "="*60) print("STEP 3: Recalculating Short-Stay Listing Ratings") print("="*60) # Get all short-stay listings listings_cursor = db.listings.find({"listing_type": "short-stay"}) listings = await listings_cursor.to_list(length=None) print(f"Found {len(listings)} short-stay listings to process") updated_count = 0 for listing in listings: listing_id = str(listing["_id"]) # Find all reviews where this listing is the target reviews_cursor = db.reviews.find({ "target_type": "listing", "target_id": listing_id }) reviews = await reviews_cursor.to_list(length=None) # Calculate new rating if reviews: total_rating = sum(r.get("rating", 0) for r in reviews) reviews_count = len(reviews) avg_rating = round(total_rating / reviews_count, 2) else: reviews_count = 0 avg_rating = 0.0 # Get current values current_rating = listing.get("rating", 0.0) current_count = listing.get("reviews_count", 0) # Update if different or missing if current_rating != avg_rating or current_count != reviews_count: await db.listings.update_one( {"_id": listing["_id"]}, {"$set": { "rating": avg_rating, "reviews_count": reviews_count }} ) updated_count += 1 print(f" āœ… Listing '{listing.get('title', 'Untitled')[:40]}': " f"{current_rating} → {avg_rating} ({reviews_count} reviews)") print(f"\nšŸ“Š Updated {updated_count} short-stay listings") return updated_count async def ensure_fields_exist(db): """ Ensure all users and listings have the rating/reviews_count fields. This initializes them to 0 if missing. """ print("\n" + "="*60) print("STEP 1: Ensuring Fields Exist on All Documents") print("="*60) # Update users missing rating field users_result = await db.users.update_many( {"rating": {"$exists": False}}, {"$set": {"rating": 0.0}} ) print(f" āœ… Added 'rating' field to {users_result.modified_count} users") # Update users missing reviews_count field users_result2 = await db.users.update_many( {"reviews_count": {"$exists": False}}, {"$set": {"reviews_count": 0}} ) print(f" āœ… Added 'reviews_count' field to {users_result2.modified_count} users") # Update short-stay listings missing rating field listings_result = await db.listings.update_many( {"listing_type": "short-stay", "rating": {"$exists": False}}, {"$set": {"rating": 0.0}} ) print(f" āœ… Added 'rating' field to {listings_result.modified_count} short-stay listings") # Update short-stay listings missing reviews_count field listings_result2 = await db.listings.update_many( {"listing_type": "short-stay", "reviews_count": {"$exists": False}}, {"$set": {"reviews_count": 0}} ) print(f" āœ… Added 'reviews_count' field to {listings_result2.modified_count} short-stay listings") async def print_summary(db): """Print a summary of the current state""" print("\n" + "="*60) print("SUMMARY") print("="*60) # Count users with reviews users_with_reviews = await db.users.count_documents({"reviews_count": {"$gt": 0}}) total_users = await db.users.count_documents({}) print(f" šŸ‘„ Users with reviews: {users_with_reviews}/{total_users}") # Count listings by type for listing_type in ["rent", "sale", "short-stay", "roommate"]: count = await db.listings.count_documents({"listing_type": listing_type}) if listing_type == "short-stay": with_reviews = await db.listings.count_documents({ "listing_type": listing_type, "reviews_count": {"$gt": 0} }) print(f" šŸ  {listing_type}: {count} listings ({with_reviews} with reviews)") else: print(f" šŸ  {listing_type}: {count} listings (use owner's rating)") # Count total reviews total_reviews = await db.reviews.count_documents({}) user_reviews = await db.reviews.count_documents({"target_type": "user"}) listing_reviews = await db.reviews.count_documents({"target_type": "listing"}) print(f"\n ⭐ Total reviews: {total_reviews}") print(f" - User reviews (for landlords): {user_reviews}") print(f" - Listing reviews (for properties): {listing_reviews}") async def main(): """Main migration function""" print("\n" + "="*60) print("šŸ”§ RATING MIGRATION SCRIPT") print("="*60) print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") try: db = await get_database() print(f"āœ… Connected to MongoDB successfully") # Step 1: Ensure all fields exist await ensure_fields_exist(db) # Step 2: Recalculate user ratings await recalculate_user_ratings(db) # Step 3: Recalculate short-stay listing ratings await recalculate_listing_ratings(db) # Print summary await print_summary(db) print("\n" + "="*60) print("āœ… MIGRATION COMPLETED SUCCESSFULLY") print("="*60) print(f"Finished at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") except Exception as e: print(f"\nāŒ ERROR: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": asyncio.run(main())