AIDA / scripts /fix_ratings.py
destinyebuka's picture
fyp
ee3d3d4
#!/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())