Skip to content

Commit e3d170a

Browse files
vrajpatelllpre-commit-ci[bot]Devasy
authored
Bug Fix: Inconsistent Field Naming - avatar vs imageUrl (#89)
* fix(services): fix mismatch between imageUrl and avatar * feat(migration-script): add migration, rollback and backup script * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(review): address coderabbit review comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Devasy Patel <[email protected]>
1 parent 4c7f054 commit e3d170a

File tree

7 files changed

+258
-14
lines changed

7 files changed

+258
-14
lines changed

backend/app/auth/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class UserResponse(BaseModel):
4242
id: str = Field(alias="_id")
4343
email: str
4444
name: str
45-
avatar: Optional[str] = None
45+
imageUrl: Optional[str] = None
4646
currency: str = "USD"
4747
created_at: datetime
4848

backend/app/auth/service.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ async def create_user_with_email(
115115
"email": email,
116116
"hashed_password": get_password_hash(password),
117117
"name": name,
118-
"avatar": None,
118+
"imageUrl": None,
119119
"currency": "USD",
120120
"created_at": datetime.now(timezone.utc),
121121
"auth_provider": "email",
@@ -202,8 +202,8 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
202202
update_data = {}
203203
if user.get("firebase_uid") != firebase_uid:
204204
update_data["firebase_uid"] = firebase_uid
205-
if user.get("avatar") != picture and picture:
206-
update_data["avatar"] = picture
205+
if user.get("imageUrl") != picture and picture:
206+
update_data["imageUrl"] = picture
207207

208208
if update_data:
209209
await db.users.update_one(
@@ -215,7 +215,7 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
215215
user_doc = {
216216
"email": email,
217217
"name": name,
218-
"avatar": picture,
218+
"imageUrl": picture,
219219
"currency": "USD",
220220
"created_at": datetime.now(timezone.utc),
221221
"auth_provider": "google",

backend/app/groups/service.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,13 @@ async def _enrich_members_with_user_details(
5454
if user
5555
else f"{member_user_id}@example.com"
5656
),
57-
"avatar": (
58-
user.get("imageUrl") or user.get("avatar")
59-
if user
60-
else None
61-
),
57+
"imageUrl": (user.get("imageUrl") if user else None),
6258
}
6359
if user
6460
else {
6561
"name": f"User {member_user_id[-4:]}",
6662
"email": f"{member_user_id}@example.com",
67-
"avatar": None,
63+
"imageUrl": None,
6864
}
6965
),
7066
}
@@ -79,7 +75,7 @@ async def _enrich_members_with_user_details(
7975
"user": {
8076
"name": f"User {member_user_id[-4:]}",
8177
"email": f"{member_user_id}@example.com",
82-
"avatar": None,
78+
"imageUrl": None,
8379
},
8480
}
8581
)

backend/app/user/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def iso(dt):
4141
"id": user_id,
4242
"name": user.get("name"),
4343
"email": user.get("email"),
44-
"imageUrl": user.get("imageUrl") or user.get("avatar"),
44+
"imageUrl": user.get("imageUrl"),
4545
"currency": user.get("currency", "USD"),
4646
"createdAt": iso(user.get("created_at")),
4747
"updatedAt": iso(user.get("updated_at") or user.get("created_at")),

backend/scripts/backup_db.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Database backup script for Splitwiser.
3+
Creates a backup of all collections before performing migrations.
4+
"""
5+
6+
import json
7+
import os
8+
from datetime import datetime
9+
10+
from dotenv import load_dotenv
11+
from pymongo import MongoClient
12+
13+
# Get the script's directory and backend directory
14+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
15+
BACKEND_DIR = os.path.dirname(SCRIPT_DIR)
16+
17+
# Load environment variables from .env file in backend directory
18+
load_dotenv(os.path.join(BACKEND_DIR, ".env"))
19+
20+
# Get MongoDB connection details from environment
21+
MONGODB_URL = os.getenv("MONGODB_URL")
22+
DATABASE_NAME = os.getenv("DATABASE_NAME")
23+
24+
25+
def create_backup():
26+
"""Create a backup of all collections."""
27+
try:
28+
# Create backup directory if it doesn't exist
29+
backup_dir = "backups"
30+
backup_time = datetime.now().strftime("%Y%m%d_%H%M%S")
31+
backup_path = os.path.join(backup_dir, f"backup_{backup_time}")
32+
os.makedirs(backup_path, exist_ok=True)
33+
34+
# Connect to MongoDB
35+
client = MongoClient(MONGODB_URL)
36+
db = client[DATABASE_NAME]
37+
38+
# Get all collections
39+
collections = db.list_collection_names()
40+
backup_stats = {}
41+
42+
for collection_name in collections:
43+
collection = db[collection_name]
44+
documents = list(collection.find({}))
45+
46+
# Convert ObjectId to string for JSON serialization
47+
for doc in documents:
48+
doc["_id"] = str(doc["_id"])
49+
50+
# Save to file
51+
backup_file = os.path.join(backup_path, f"{collection_name}.json")
52+
with open(backup_file, "w") as f:
53+
json.dump(documents, f, indent=2, default=str)
54+
55+
backup_stats[collection_name] = len(documents)
56+
57+
# Save backup metadata
58+
metadata = {
59+
"timestamp": datetime.now().isoformat(),
60+
"database": DATABASE_NAME,
61+
"collections": backup_stats,
62+
"total_documents": sum(backup_stats.values()),
63+
}
64+
65+
with open(os.path.join(backup_path, "backup_metadata.json"), "w") as f:
66+
json.dump(metadata, f, indent=2)
67+
68+
return backup_path, metadata
69+
70+
except Exception as e:
71+
print(f"Backup failed: {str(e)}")
72+
raise
73+
74+
75+
if __name__ == "__main__":
76+
backup_path, metadata = create_backup()
77+
print(f"Backup created successfully at: {backup_path}")
78+
print("\nBackup statistics:")
79+
print(f"Total documents: {metadata['total_documents']}")
80+
for coll, count in metadata["collections"].items():
81+
print(f"{coll}: {count} documents")
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
Migration script to standardize user avatar fields to imageUrl.
3+
This script:
4+
1. Identifies users with avatar field but no imageUrl field
5+
2. Copies avatar values to imageUrl field
6+
3. Removes the deprecated avatar field
7+
4. Logs migration statistics
8+
"""
9+
10+
import json
11+
import logging
12+
import os
13+
import sys
14+
from datetime import datetime
15+
16+
from backup_db import create_backup
17+
from bson import ObjectId
18+
from dotenv import load_dotenv
19+
from pymongo import MongoClient, UpdateOne
20+
21+
# Add the script's directory to Python path
22+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
23+
sys.path.append(SCRIPT_DIR)
24+
25+
26+
# Load environment variables from the backend directory
27+
BACKEND_DIR = os.path.dirname(SCRIPT_DIR)
28+
load_dotenv(os.path.join(BACKEND_DIR, ".env"))
29+
30+
# Get MongoDB connection details from environment
31+
MONGODB_URL = os.getenv("MONGODB_URL")
32+
DATABASE_NAME = os.getenv("DATABASE_NAME")
33+
34+
# Configure logging
35+
logging.basicConfig(level=logging.INFO)
36+
logger = logging.getLogger(__name__)
37+
38+
# Set up file logging
39+
log_dir = "logs"
40+
os.makedirs(log_dir, exist_ok=True)
41+
log_file = os.path.join(
42+
log_dir, f"migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
43+
)
44+
file_handler = logging.FileHandler(log_file)
45+
file_handler.setFormatter(
46+
logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
47+
)
48+
logger.addHandler(file_handler)
49+
50+
# Validate required environment variables
51+
if not MONGODB_URL:
52+
logger.error("MONGODB_URL environment variable is required")
53+
sys.exit(1)
54+
if not DATABASE_NAME:
55+
logger.error("DATABASE_NAME environment variable is required")
56+
sys.exit(1)
57+
58+
59+
def migrate_avatar_to_imageurl():
60+
"""
61+
Migrate avatar field to imageUrl in users collection.
62+
Returns statistics about the migration.
63+
"""
64+
try:
65+
# First create a backup
66+
logger.info("Creating database backup...")
67+
backup_path, backup_metadata = create_backup()
68+
logger.info(f"Backup created at: {backup_path}")
69+
70+
# Connect to MongoDB
71+
client = MongoClient(MONGODB_URL)
72+
db = client[DATABASE_NAME]
73+
users = db.users
74+
75+
# Find users with avatar field
76+
users_with_avatar = users.find({"avatar": {"$exists": True}})
77+
users_to_update = []
78+
stats = {
79+
"total_users": users.count_documents({}),
80+
"users_with_avatar": 0,
81+
"users_with_both_fields": 0,
82+
"users_updated": 0,
83+
"conflicts": 0,
84+
}
85+
86+
for user in users_with_avatar:
87+
stats["users_with_avatar"] += 1
88+
89+
# Check for conflicts (users with both fields)
90+
if "imageUrl" in user and user["imageUrl"] is not None:
91+
if user["imageUrl"] != user["avatar"]:
92+
logger.warning(
93+
f"Conflict found for user {user['_id']}: "
94+
f"avatar='{user['avatar']}', imageUrl='{user['imageUrl']}'"
95+
)
96+
stats["conflicts"] += 1
97+
continue
98+
stats["users_with_both_fields"] += 1
99+
100+
# Prepare update
101+
users_to_update.append(
102+
UpdateOne(
103+
{"_id": user["_id"]},
104+
{"$set": {"imageUrl": user["avatar"]}, "$unset": {
105+
"avatar": ""}},
106+
)
107+
)
108+
109+
# Perform bulk update if there are users to update
110+
if users_to_update:
111+
result = users.bulk_write(users_to_update)
112+
stats["users_updated"] = result.modified_count
113+
logger.info(f"Successfully updated {result.modified_count} users")
114+
115+
return stats
116+
117+
except Exception as e:
118+
logger.error(f"Migration failed: {str(e)}")
119+
raise
120+
121+
122+
def rollback_migration(backup_path):
123+
"""
124+
Rollback the migration using a specified backup.
125+
"""
126+
try:
127+
client = MongoClient(MONGODB_URL)
128+
db = client[DATABASE_NAME]
129+
130+
backup_file_path = os.path.join(backup_path, "users.json")
131+
if not os.path.exists(backup_file_path):
132+
raise FileNotFoundError(
133+
f"Backup file not found: {backup_file_path}")
134+
135+
# Read users collection backup
136+
with open(backup_file_path, "r") as f:
137+
users_backup = json.load(f)
138+
139+
# Convert string IDs back to ObjectId
140+
for user in users_backup:
141+
user["_id"] = ObjectId(user["_id"])
142+
143+
# Replace current users collection with backup
144+
db.users.drop()
145+
if users_backup:
146+
db.users.insert_many(users_backup)
147+
148+
logger.info(f"Successfully rolled back to backup: {backup_path}")
149+
return True
150+
151+
except Exception as e:
152+
logger.error(f"Rollback failed: {str(e)}")
153+
raise
154+
155+
156+
if __name__ == "__main__":
157+
logger.info("Starting avatar to imageUrl migration...")
158+
stats = migrate_avatar_to_imageurl()
159+
160+
logger.info("\nMigration completed. Statistics:")
161+
logger.info(f"Total users: {stats['total_users']}")
162+
logger.info(f"Users with avatar field: {stats['users_with_avatar']}")
163+
logger.info(f"Users with both fields: {stats['users_with_both_fields']}")
164+
logger.info(f"Users updated: {stats['users_updated']}")
165+
logger.info(f"Conflicts found: {stats['conflicts']}")
166+
167+
print("\nMigration completed. Check the log file for details:", log_file)

backend/tests/auth/test_auth_routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ async def test_login_with_email_success(mock_db):
168168
"email": user_email,
169169
"hashed_password": hashed_password,
170170
"name": "Login User",
171-
"avatar": None,
171+
"imageUrl": None,
172172
"currency": "USD",
173173
# Ensure datetime is used
174174
"created_at": datetime.now(timezone.utc),

0 commit comments

Comments
 (0)