mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 22:07:03 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
# Import choices to trigger registration
|
||||
from .choices import *
|
||||
from .choices import * # noqa: F403
|
||||
|
||||
@@ -77,8 +77,6 @@ class UserProfileInline(admin.StackedInline):
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(QueryOptimizationMixin, ExportActionMixin, UserAdmin):
|
||||
"""
|
||||
@@ -332,8 +330,9 @@ class CustomUserAdmin(QueryOptimizationMixin, ExportActionMixin, UserAdmin):
|
||||
try:
|
||||
profile = user.profile
|
||||
# Credits would be recalculated from ride history here
|
||||
profile.save(update_fields=["coaster_credits", "dark_ride_credits",
|
||||
"flat_ride_credits", "water_ride_credits"])
|
||||
profile.save(
|
||||
update_fields=["coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits"]
|
||||
)
|
||||
count += 1
|
||||
except UserProfile.DoesNotExist:
|
||||
pass
|
||||
@@ -442,12 +441,14 @@ class UserProfileAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin
|
||||
@admin.display(description="Completeness")
|
||||
def profile_completeness(self, obj):
|
||||
"""Display profile completeness indicator."""
|
||||
fields_filled = sum([
|
||||
bool(obj.display_name),
|
||||
bool(obj.avatar),
|
||||
bool(obj.bio),
|
||||
bool(obj.twitter or obj.instagram or obj.youtube or obj.discord),
|
||||
])
|
||||
fields_filled = sum(
|
||||
[
|
||||
bool(obj.display_name),
|
||||
bool(obj.avatar),
|
||||
bool(obj.bio),
|
||||
bool(obj.twitter or obj.instagram or obj.youtube or obj.discord),
|
||||
]
|
||||
)
|
||||
percentage = (fields_filled / 4) * 100
|
||||
color = "green" if percentage >= 75 else "orange" if percentage >= 50 else "red"
|
||||
return format_html(
|
||||
@@ -529,12 +530,8 @@ class EmailVerificationAdmin(QueryOptimizationMixin, BaseModelAdmin):
|
||||
def expiration_status(self, obj):
|
||||
"""Display expiration status with color coding."""
|
||||
if timezone.now() - obj.last_sent > timedelta(days=1):
|
||||
return format_html(
|
||||
'<span style="color: red; font-weight: bold;">Expired</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: green; font-weight: bold;">Valid</span>'
|
||||
)
|
||||
return format_html('<span style="color: red; font-weight: bold;">Expired</span>')
|
||||
return format_html('<span style="color: green; font-weight: bold;">Valid</span>')
|
||||
|
||||
@admin.display(description="Can Resend", boolean=True)
|
||||
def can_resend(self, obj):
|
||||
@@ -665,6 +662,3 @@ class PasswordResetAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
|
||||
"Cleanup old tokens",
|
||||
)
|
||||
return actions
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ user_roles = ChoiceGroup(
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"permissions": ["create_content", "create_reviews", "create_lists"],
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="MODERATOR",
|
||||
@@ -38,7 +38,7 @@ user_roles = ChoiceGroup(
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"permissions": ["moderate_content", "review_submissions", "manage_reports"],
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="ADMIN",
|
||||
@@ -50,7 +50,7 @@ user_roles = ChoiceGroup(
|
||||
"css_class": "text-purple-600 bg-purple-50",
|
||||
"permissions": ["manage_users", "site_configuration", "advanced_moderation"],
|
||||
"sort_order": 3,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="SUPERUSER",
|
||||
@@ -62,9 +62,9 @@ user_roles = ChoiceGroup(
|
||||
"css_class": "text-red-600 bg-red-50",
|
||||
"permissions": ["full_access", "system_administration", "database_access"],
|
||||
"sort_order": 4,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -83,13 +83,9 @@ theme_preferences = ChoiceGroup(
|
||||
"color": "yellow",
|
||||
"icon": "sun",
|
||||
"css_class": "text-yellow-600 bg-yellow-50",
|
||||
"preview_colors": {
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937",
|
||||
"accent": "#3b82f6"
|
||||
},
|
||||
"preview_colors": {"background": "#ffffff", "text": "#1f2937", "accent": "#3b82f6"},
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="dark",
|
||||
@@ -99,15 +95,11 @@ theme_preferences = ChoiceGroup(
|
||||
"color": "gray",
|
||||
"icon": "moon",
|
||||
"css_class": "text-gray-600 bg-gray-50",
|
||||
"preview_colors": {
|
||||
"background": "#1f2937",
|
||||
"text": "#f9fafb",
|
||||
"accent": "#60a5fa"
|
||||
},
|
||||
"preview_colors": {"background": "#1f2937", "text": "#f9fafb", "accent": "#60a5fa"},
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -133,7 +125,7 @@ unit_systems = ChoiceGroup(
|
||||
"large_distance": "km",
|
||||
},
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="imperial",
|
||||
@@ -150,9 +142,9 @@ unit_systems = ChoiceGroup(
|
||||
"large_distance": "mi",
|
||||
},
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -177,10 +169,10 @@ privacy_levels = ChoiceGroup(
|
||||
"Profile visible to all users",
|
||||
"Activity appears in public feeds",
|
||||
"Searchable by search engines",
|
||||
"Can be found by username search"
|
||||
"Can be found by username search",
|
||||
],
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="friends",
|
||||
@@ -196,10 +188,10 @@ privacy_levels = ChoiceGroup(
|
||||
"Profile visible only to friends",
|
||||
"Activity hidden from public feeds",
|
||||
"Not searchable by search engines",
|
||||
"Requires friend request approval"
|
||||
"Requires friend request approval",
|
||||
],
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="private",
|
||||
@@ -215,12 +207,12 @@ privacy_levels = ChoiceGroup(
|
||||
"Profile completely hidden",
|
||||
"No activity in any feeds",
|
||||
"Not discoverable by other users",
|
||||
"Maximum privacy protection"
|
||||
"Maximum privacy protection",
|
||||
],
|
||||
"sort_order": 3,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -242,7 +234,7 @@ top_list_categories = ChoiceGroup(
|
||||
"ride_category": "roller_coaster",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="DR",
|
||||
@@ -255,7 +247,7 @@ top_list_categories = ChoiceGroup(
|
||||
"ride_category": "dark_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="FR",
|
||||
@@ -268,7 +260,7 @@ top_list_categories = ChoiceGroup(
|
||||
"ride_category": "flat_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 3,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="WR",
|
||||
@@ -281,7 +273,7 @@ top_list_categories = ChoiceGroup(
|
||||
"ride_category": "water_ride",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 4,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="PK",
|
||||
@@ -294,9 +286,9 @@ top_list_categories = ChoiceGroup(
|
||||
"entity_type": "park",
|
||||
"typical_list_size": 10,
|
||||
"sort_order": 5,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -320,7 +312,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="submission_rejected",
|
||||
@@ -334,7 +326,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="submission_pending",
|
||||
@@ -348,7 +340,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 3,
|
||||
}
|
||||
},
|
||||
),
|
||||
# Review related
|
||||
RichChoice(
|
||||
@@ -363,7 +355,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 4,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="review_helpful",
|
||||
@@ -377,7 +369,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 5,
|
||||
}
|
||||
},
|
||||
),
|
||||
# Social related
|
||||
RichChoice(
|
||||
@@ -392,7 +384,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 6,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="friend_accepted",
|
||||
@@ -406,7 +398,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 7,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="message_received",
|
||||
@@ -420,7 +412,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 8,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="profile_comment",
|
||||
@@ -434,7 +426,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 9,
|
||||
}
|
||||
},
|
||||
),
|
||||
# System related
|
||||
RichChoice(
|
||||
@@ -449,7 +441,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 10,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="account_security",
|
||||
@@ -463,7 +455,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "push", "inapp"],
|
||||
"priority": "high",
|
||||
"sort_order": 11,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="feature_update",
|
||||
@@ -477,7 +469,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 12,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="maintenance",
|
||||
@@ -491,7 +483,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["email", "inapp"],
|
||||
"priority": "normal",
|
||||
"sort_order": 13,
|
||||
}
|
||||
},
|
||||
),
|
||||
# Achievement related
|
||||
RichChoice(
|
||||
@@ -506,7 +498,7 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 14,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="milestone_reached",
|
||||
@@ -520,9 +512,9 @@ notification_types = ChoiceGroup(
|
||||
"default_channels": ["push", "inapp"],
|
||||
"priority": "low",
|
||||
"sort_order": 15,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -545,7 +537,7 @@ notification_priorities = ChoiceGroup(
|
||||
"batch_eligible": True,
|
||||
"delay_minutes": 60,
|
||||
"sort_order": 1,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="normal",
|
||||
@@ -559,7 +551,7 @@ notification_priorities = ChoiceGroup(
|
||||
"batch_eligible": True,
|
||||
"delay_minutes": 15,
|
||||
"sort_order": 2,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="high",
|
||||
@@ -573,7 +565,7 @@ notification_priorities = ChoiceGroup(
|
||||
"batch_eligible": False,
|
||||
"delay_minutes": 0,
|
||||
"sort_order": 3,
|
||||
}
|
||||
},
|
||||
),
|
||||
RichChoice(
|
||||
value="urgent",
|
||||
@@ -588,9 +580,9 @@ notification_priorities = ChoiceGroup(
|
||||
"delay_minutes": 0,
|
||||
"bypass_preferences": True,
|
||||
"sort_order": 4,
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -53,28 +53,34 @@ class UserExportService:
|
||||
"dark_ride": profile.dark_ride_credits,
|
||||
"flat_ride": profile.flat_ride_credits,
|
||||
"water_ride": profile.water_ride_credits,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Reviews
|
||||
park_reviews = list(ParkReview.objects.filter(user=user).values(
|
||||
"park__name", "rating", "review", "created_at", "updated_at", "is_published"
|
||||
))
|
||||
park_reviews = list(
|
||||
ParkReview.objects.filter(user=user).values(
|
||||
"park__name", "rating", "review", "created_at", "updated_at", "is_published"
|
||||
)
|
||||
)
|
||||
|
||||
ride_reviews = list(RideReview.objects.filter(user=user).values(
|
||||
"ride__name", "rating", "review", "created_at", "updated_at", "is_published"
|
||||
))
|
||||
ride_reviews = list(
|
||||
RideReview.objects.filter(user=user).values(
|
||||
"ride__name", "rating", "review", "created_at", "updated_at", "is_published"
|
||||
)
|
||||
)
|
||||
|
||||
# Lists
|
||||
user_lists = []
|
||||
for user_list in UserList.objects.filter(user=user):
|
||||
items = list(user_list.items.values("order", "content_type__model", "object_id", "comment"))
|
||||
user_lists.append({
|
||||
"title": user_list.title,
|
||||
"description": user_list.description,
|
||||
"created_at": user_list.created_at,
|
||||
"items": items
|
||||
})
|
||||
user_lists.append(
|
||||
{
|
||||
"title": user_list.title,
|
||||
"description": user_list.description,
|
||||
"created_at": user_list.created_at,
|
||||
"items": items,
|
||||
}
|
||||
)
|
||||
|
||||
export_data = {
|
||||
"account": user_data,
|
||||
@@ -85,10 +91,7 @@ class UserExportService:
|
||||
"ride_reviews": ride_reviews,
|
||||
"lists": user_lists,
|
||||
},
|
||||
"export_info": {
|
||||
"generated_at": timezone.now(),
|
||||
"version": "1.0"
|
||||
}
|
||||
"export_info": {"generated_at": timezone.now(), "version": "1.0"},
|
||||
}
|
||||
|
||||
return export_data
|
||||
|
||||
@@ -99,8 +99,6 @@ class LoginHistory(models.Model):
|
||||
|
||||
# Default cleanup for entries older than the specified days
|
||||
cutoff = timezone.now() - timedelta(days=days)
|
||||
deleted_count, _ = cls.objects.filter(
|
||||
login_timestamp__lt=cutoff
|
||||
).delete()
|
||||
deleted_count, _ = cls.objects.filter(login_timestamp__lt=cutoff).delete()
|
||||
|
||||
return deleted_count
|
||||
|
||||
@@ -22,20 +22,14 @@ class Command(BaseCommand):
|
||||
# Check SocialAccount
|
||||
self.stdout.write("\nChecking SocialAccount table:")
|
||||
for account in SocialAccount.objects.all():
|
||||
self.stdout.write(
|
||||
f"ID: {account.pk}, Provider: {account.provider}, UID: {account.uid}"
|
||||
)
|
||||
self.stdout.write(f"ID: {account.pk}, Provider: {account.provider}, UID: {account.uid}")
|
||||
|
||||
# Check SocialToken
|
||||
self.stdout.write("\nChecking SocialToken table:")
|
||||
for token in SocialToken.objects.all():
|
||||
self.stdout.write(
|
||||
f"ID: {token.pk}, Account: {token.account}, App: {token.app}"
|
||||
)
|
||||
self.stdout.write(f"ID: {token.pk}, Account: {token.account}, App: {token.app}")
|
||||
|
||||
# Check Site
|
||||
self.stdout.write("\nChecking Site table:")
|
||||
for site in Site.objects.all():
|
||||
self.stdout.write(
|
||||
f"ID: {site.pk}, Domain: {site.domain}, Name: {site.name}"
|
||||
)
|
||||
self.stdout.write(f"ID: {site.pk}, Domain: {site.domain}, Name: {site.name}")
|
||||
|
||||
@@ -17,6 +17,4 @@ class Command(BaseCommand):
|
||||
self.stdout.write(f"Name: {app.name}")
|
||||
self.stdout.write(f"Client ID: {app.client_id}")
|
||||
self.stdout.write(f"Secret: {app.secret}")
|
||||
self.stdout.write(
|
||||
f"Sites: {', '.join(str(site.domain) for site in app.sites.all())}"
|
||||
)
|
||||
self.stdout.write(f"Sites: {', '.join(str(site.domain) for site in app.sites.all())}")
|
||||
|
||||
@@ -15,14 +15,9 @@ class Command(BaseCommand):
|
||||
|
||||
# Remove migration records
|
||||
cursor.execute("DELETE FROM django_migrations WHERE app='socialaccount'")
|
||||
cursor.execute(
|
||||
"DELETE FROM django_migrations WHERE app='accounts' "
|
||||
"AND name LIKE '%social%'"
|
||||
)
|
||||
cursor.execute("DELETE FROM django_migrations WHERE app='accounts' " "AND name LIKE '%social%'")
|
||||
|
||||
# Reset sequences
|
||||
cursor.execute("DELETE FROM sqlite_sequence WHERE name LIKE '%social%'")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully cleaned up social auth configuration")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Successfully cleaned up social auth configuration"))
|
||||
|
||||
@@ -18,24 +18,18 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test users"))
|
||||
|
||||
# Delete test reviews
|
||||
reviews = ParkReview.objects.filter(
|
||||
user__username__in=["testuser", "moderator"]
|
||||
)
|
||||
reviews = ParkReview.objects.filter(user__username__in=["testuser", "moderator"])
|
||||
count = reviews.count()
|
||||
reviews.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test reviews"))
|
||||
|
||||
# Delete test photos - both park and ride photos
|
||||
park_photos = ParkPhoto.objects.filter(
|
||||
uploader__username__in=["testuser", "moderator"]
|
||||
)
|
||||
park_photos = ParkPhoto.objects.filter(uploader__username__in=["testuser", "moderator"])
|
||||
park_count = park_photos.count()
|
||||
park_photos.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {park_count} test park photos"))
|
||||
|
||||
ride_photos = RidePhoto.objects.filter(
|
||||
uploader__username__in=["testuser", "moderator"]
|
||||
)
|
||||
ride_photos = RidePhoto.objects.filter(uploader__username__in=["testuser", "moderator"])
|
||||
ride_count = ride_photos.count()
|
||||
ride_photos.delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {ride_count} test ride photos"))
|
||||
|
||||
@@ -37,18 +37,12 @@ class Command(BaseCommand):
|
||||
provider="google",
|
||||
defaults={
|
||||
"name": "Google",
|
||||
"client_id": (
|
||||
"135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2."
|
||||
"apps.googleusercontent.com"
|
||||
),
|
||||
"client_id": ("135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2." "apps.googleusercontent.com"),
|
||||
"secret": "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue",
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
google_app.client_id = (
|
||||
"135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2."
|
||||
"apps.googleusercontent.com"
|
||||
)
|
||||
google_app.client_id = "135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2." "apps.googleusercontent.com"
|
||||
google_app.secret = "GOCSPX-Wd_0Ue0Ue0Ue0Ue0Ue0Ue0Ue0Ue"
|
||||
google_app.save()
|
||||
google_app.sites.add(site)
|
||||
|
||||
@@ -14,9 +14,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
user.set_password("testpass123")
|
||||
user.save()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created test user: {user.get_username()}")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created test user: {user.get_username()}"))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING("Test user already exists"))
|
||||
|
||||
@@ -47,11 +45,7 @@ class Command(BaseCommand):
|
||||
# Add user to moderator group
|
||||
moderator.groups.add(moderator_group)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Created moderator user: {moderator.get_username()}"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Created moderator user: {moderator.get_username()}"))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING("Moderator user already exists"))
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ class Command(BaseCommand):
|
||||
help = "Delete a user while preserving all their submissions"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"username", nargs="?", type=str, help="Username of the user to delete"
|
||||
)
|
||||
parser.add_argument("username", nargs="?", type=str, help="Username of the user to delete")
|
||||
parser.add_argument(
|
||||
"--user-id",
|
||||
type=str,
|
||||
@@ -30,9 +28,7 @@ class Command(BaseCommand):
|
||||
action="store_true",
|
||||
help="Show what would be deleted without actually deleting",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force", action="store_true", help="Skip confirmation prompt"
|
||||
)
|
||||
parser.add_argument("--force", action="store_true", help="Skip confirmation prompt")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
username = options.get("username")
|
||||
@@ -52,7 +48,7 @@ class Command(BaseCommand):
|
||||
user = User.objects.get(username=username) if username else User.objects.get(user_id=user_id)
|
||||
except User.DoesNotExist:
|
||||
identifier = username or user_id
|
||||
raise CommandError(f'User "{identifier}" does not exist')
|
||||
raise CommandError(f'User "{identifier}" does not exist') from None
|
||||
|
||||
# Check if user can be deleted
|
||||
can_delete, reason = UserDeletionService.can_delete_user(user)
|
||||
@@ -61,27 +57,13 @@ class Command(BaseCommand):
|
||||
|
||||
# Count submissions
|
||||
submission_counts = {
|
||||
"park_reviews": getattr(
|
||||
user, "park_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"ride_reviews": getattr(
|
||||
user, "ride_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"uploaded_park_photos": getattr(
|
||||
user, "uploaded_park_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"uploaded_ride_photos": getattr(
|
||||
user, "uploaded_ride_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"top_lists": getattr(
|
||||
user, "top_lists", user.__class__.objects.none()
|
||||
).count(),
|
||||
"edit_submissions": getattr(
|
||||
user, "edit_submissions", user.__class__.objects.none()
|
||||
).count(),
|
||||
"photo_submissions": getattr(
|
||||
user, "photo_submissions", user.__class__.objects.none()
|
||||
).count(),
|
||||
"park_reviews": getattr(user, "park_reviews", user.__class__.objects.none()).count(),
|
||||
"ride_reviews": getattr(user, "ride_reviews", user.__class__.objects.none()).count(),
|
||||
"uploaded_park_photos": getattr(user, "uploaded_park_photos", user.__class__.objects.none()).count(),
|
||||
"uploaded_ride_photos": getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count(),
|
||||
"top_lists": getattr(user, "top_lists", user.__class__.objects.none()).count(),
|
||||
"edit_submissions": getattr(user, "edit_submissions", user.__class__.objects.none()).count(),
|
||||
"photo_submissions": getattr(user, "photo_submissions", user.__class__.objects.none()).count(),
|
||||
}
|
||||
|
||||
total_submissions = sum(submission_counts.values())
|
||||
@@ -98,9 +80,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.WARNING("\nSubmissions to preserve:"))
|
||||
for submission_type, count in submission_counts.items():
|
||||
if count > 0:
|
||||
self.stdout.write(
|
||||
f' {submission_type.replace("_", " ").title()}: {count}'
|
||||
)
|
||||
self.stdout.write(f' {submission_type.replace("_", " ").title()}: {count}')
|
||||
|
||||
self.stdout.write(f"\nTotal submissions: {total_submissions}")
|
||||
|
||||
@@ -111,9 +91,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING("\nNo submissions found for this user.")
|
||||
)
|
||||
self.stdout.write(self.style.WARNING("\nNo submissions found for this user."))
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.SUCCESS("\n[DRY RUN] No changes were made."))
|
||||
@@ -136,11 +114,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
result = UserDeletionService.delete_user_preserve_submissions(user)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'\nSuccessfully deleted user "{result["deleted_user"]["username"]}"'
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f'\nSuccessfully deleted user "{result["deleted_user"]["username"]}"'))
|
||||
|
||||
preserved_count = sum(result["preserved_submissions"].values())
|
||||
if preserved_count > 0:
|
||||
@@ -154,9 +128,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.WARNING("\nPreservation Summary:"))
|
||||
for submission_type, count in result["preserved_submissions"].items():
|
||||
if count > 0:
|
||||
self.stdout.write(
|
||||
f' {submission_type.replace("_", " ").title()}: {count}'
|
||||
)
|
||||
self.stdout.write(f' {submission_type.replace("_", " ").title()}: {count}')
|
||||
|
||||
except Exception as e:
|
||||
raise CommandError(f"Error deleting user: {str(e)}")
|
||||
raise CommandError(f"Error deleting user: {str(e)}") from None
|
||||
|
||||
@@ -7,12 +7,5 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"DELETE FROM django_migrations WHERE app='rides' "
|
||||
"AND name='0001_initial';"
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Successfully removed rides.0001_initial from migration history"
|
||||
)
|
||||
)
|
||||
cursor.execute("DELETE FROM django_migrations WHERE app='rides' " "AND name='0001_initial';")
|
||||
self.stdout.write(self.style.SUCCESS("Successfully removed rides.0001_initial from migration history"))
|
||||
|
||||
@@ -34,6 +34,4 @@ class Command(BaseCommand):
|
||||
secret=os.getenv("DISCORD_CLIENT_SECRET"),
|
||||
)
|
||||
discord_app.sites.add(site)
|
||||
self.stdout.write(
|
||||
f"Created Discord app with client_id: {discord_app.client_id}"
|
||||
)
|
||||
self.stdout.write(f"Created Discord app with client_id: {discord_app.client_id}")
|
||||
|
||||
@@ -47,9 +47,7 @@ class Command(BaseCommand):
|
||||
help = "Generate avatars for letters A-Z and numbers 0-9"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
characters = [chr(i) for i in range(65, 91)] + [
|
||||
str(i) for i in range(10)
|
||||
] # A-Z and 0-9
|
||||
characters = [chr(i) for i in range(65, 91)] + [str(i) for i in range(10)] # A-Z and 0-9
|
||||
for char in characters:
|
||||
generate_avatar(char)
|
||||
self.stdout.write(self.style.SUCCESS(f"Generated avatar for {char}"))
|
||||
|
||||
@@ -11,6 +11,4 @@ class Command(BaseCommand):
|
||||
for profile in profiles:
|
||||
# This will trigger the avatar generation logic in the save method
|
||||
profile.save()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Regenerated avatar for {profile.user.username}"))
|
||||
|
||||
@@ -69,18 +69,18 @@ class Command(BaseCommand):
|
||||
|
||||
# Security: Using Django ORM instead of raw SQL for user creation
|
||||
user = User.objects.create_superuser(
|
||||
username='admin',
|
||||
email='admin@thrillwiki.com',
|
||||
password='admin',
|
||||
role='SUPERUSER',
|
||||
username="admin",
|
||||
email="admin@thrillwiki.com",
|
||||
password="admin",
|
||||
role="SUPERUSER",
|
||||
)
|
||||
|
||||
# Create profile using ORM
|
||||
UserProfile.objects.create(
|
||||
user=user,
|
||||
display_name='Admin',
|
||||
pronouns='they/them',
|
||||
bio='ThrillWiki Administrator',
|
||||
display_name="Admin",
|
||||
pronouns="they/them",
|
||||
bio="ThrillWiki Administrator",
|
||||
)
|
||||
|
||||
self.stdout.write("Superuser created.")
|
||||
|
||||
@@ -30,9 +30,7 @@ class Command(BaseCommand):
|
||||
google_app = SocialApp.objects.create(
|
||||
provider="google",
|
||||
name="Google",
|
||||
client_id=(
|
||||
"135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2.apps.googleusercontent.com"
|
||||
),
|
||||
client_id=("135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2.apps.googleusercontent.com"),
|
||||
secret="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",
|
||||
)
|
||||
google_app.sites.add(site)
|
||||
|
||||
@@ -12,13 +12,7 @@ class Command(BaseCommand):
|
||||
cursor.execute("DELETE FROM socialaccount_socialapp_sites")
|
||||
|
||||
# Reset sequences
|
||||
cursor.execute(
|
||||
"DELETE FROM sqlite_sequence WHERE name='socialaccount_socialapp'"
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM sqlite_sequence WHERE name='socialaccount_socialapp_sites'"
|
||||
)
|
||||
cursor.execute("DELETE FROM sqlite_sequence WHERE name='socialaccount_socialapp'")
|
||||
cursor.execute("DELETE FROM sqlite_sequence WHERE name='socialaccount_socialapp_sites'")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully reset social auth configuration")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Successfully reset social auth configuration"))
|
||||
|
||||
@@ -30,9 +30,7 @@ class Command(BaseCommand):
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully set up groups and permissions")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Successfully set up groups and permissions"))
|
||||
|
||||
# Print summary
|
||||
for group in Group.objects.all():
|
||||
|
||||
@@ -10,7 +10,5 @@ class Command(BaseCommand):
|
||||
Site.objects.all().delete()
|
||||
|
||||
# Create default site
|
||||
site = Site.objects.create(
|
||||
id=1, domain="localhost:8000", name="ThrillWiki Development"
|
||||
)
|
||||
site = Site.objects.create(id=1, domain="localhost:8000", name="ThrillWiki Development")
|
||||
self.stdout.write(self.style.SUCCESS(f"Created site: {site.domain}"))
|
||||
|
||||
@@ -49,27 +49,15 @@ class Command(BaseCommand):
|
||||
discord_client_secret,
|
||||
]
|
||||
):
|
||||
self.stdout.write(
|
||||
self.style.ERROR("Missing required environment variables")
|
||||
)
|
||||
self.stdout.write(
|
||||
f"DEBUG: google_client_id is None: {google_client_id is None}"
|
||||
)
|
||||
self.stdout.write(
|
||||
f"DEBUG: google_client_secret is None: {google_client_secret is None}"
|
||||
)
|
||||
self.stdout.write(
|
||||
f"DEBUG: discord_client_id is None: {discord_client_id is None}"
|
||||
)
|
||||
self.stdout.write(
|
||||
f"DEBUG: discord_client_secret is None: {discord_client_secret is None}"
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("Missing required environment variables"))
|
||||
self.stdout.write(f"DEBUG: google_client_id is None: {google_client_id is None}")
|
||||
self.stdout.write(f"DEBUG: google_client_secret is None: {google_client_secret is None}")
|
||||
self.stdout.write(f"DEBUG: discord_client_id is None: {discord_client_id is None}")
|
||||
self.stdout.write(f"DEBUG: discord_client_secret is None: {discord_client_secret is None}")
|
||||
return
|
||||
|
||||
# Get or create the default site
|
||||
site, _ = Site.objects.get_or_create(
|
||||
id=1, defaults={"domain": "localhost:8000", "name": "localhost"}
|
||||
)
|
||||
site, _ = Site.objects.get_or_create(id=1, defaults={"domain": "localhost:8000", "name": "localhost"})
|
||||
|
||||
# Set up Google
|
||||
google_app, created = SocialApp.objects.get_or_create(
|
||||
@@ -92,11 +80,7 @@ class Command(BaseCommand):
|
||||
google_app.save()
|
||||
self.stdout.write("DEBUG: Successfully updated Google app")
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
"Google client_id or secret is None, skipping update."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("Google client_id or secret is None, skipping update."))
|
||||
google_app.sites.add(site)
|
||||
|
||||
# Set up Discord
|
||||
@@ -120,11 +104,7 @@ class Command(BaseCommand):
|
||||
discord_app.save()
|
||||
self.stdout.write("DEBUG: Successfully updated Discord app")
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
"Discord client_id or secret is None, skipping update."
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.ERROR("Discord client_id or secret is None, skipping update."))
|
||||
discord_app.sites.add(site)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully set up social auth apps"))
|
||||
|
||||
@@ -42,6 +42,4 @@ class Command(BaseCommand):
|
||||
for app in SocialApp.objects.all():
|
||||
self.stdout.write(f"- {app.name} ({app.provider}): {app.client_id}")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"\nTotal social apps: {SocialApp.objects.count()}")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"\nTotal social apps: {SocialApp.objects.count()}"))
|
||||
|
||||
@@ -40,9 +40,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Show callback URL
|
||||
callback_url = "http://localhost:8000/accounts/discord/login/callback/"
|
||||
self.stdout.write(
|
||||
"\nCallback URL to configure in Discord Developer Portal:"
|
||||
)
|
||||
self.stdout.write("\nCallback URL to configure in Discord Developer Portal:")
|
||||
self.stdout.write(callback_url)
|
||||
|
||||
# Show frontend login URL
|
||||
|
||||
@@ -18,6 +18,4 @@ class Command(BaseCommand):
|
||||
# Add all sites
|
||||
for site in sites:
|
||||
app.sites.add(site)
|
||||
self.stdout.write(
|
||||
f"Added sites: {', '.join(site.domain for site in sites)}"
|
||||
)
|
||||
self.stdout.write(f"Added sites: {', '.join(site.domain for site in sites)}")
|
||||
|
||||
@@ -22,17 +22,13 @@ class Command(BaseCommand):
|
||||
|
||||
# Show callback URL
|
||||
callback_url = "http://localhost:8000/accounts/discord/login/callback/"
|
||||
self.stdout.write(
|
||||
"\nCallback URL to configure in Discord Developer Portal:"
|
||||
)
|
||||
self.stdout.write("\nCallback URL to configure in Discord Developer Portal:")
|
||||
self.stdout.write(callback_url)
|
||||
|
||||
# Show OAuth2 settings
|
||||
self.stdout.write("\nOAuth2 settings in settings.py:")
|
||||
discord_settings = settings.SOCIALACCOUNT_PROVIDERS.get("discord", {})
|
||||
self.stdout.write(
|
||||
f"PKCE Enabled: {discord_settings.get('OAUTH_PKCE_ENABLED', False)}"
|
||||
)
|
||||
self.stdout.write(f"PKCE Enabled: {discord_settings.get('OAUTH_PKCE_ENABLED', False)}")
|
||||
self.stdout.write(f"Scopes: {discord_settings.get('SCOPE', [])}")
|
||||
|
||||
except SocialApp.DoesNotExist:
|
||||
|
||||
@@ -38,9 +38,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
models.DateTimeField(blank=True, null=True, verbose_name="last login"),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
@@ -53,29 +51,21 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
error_messages={"unique": "A user with that username already exists."},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
models.CharField(blank=True, max_length=150, verbose_name="first name"),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
models.CharField(blank=True, max_length=150, verbose_name="last name"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
|
||||
@@ -57,9 +57,7 @@ class Migration(migrations.Migration):
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
models.DateTimeField(blank=True, null=True, verbose_name="last login"),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
@@ -72,34 +70,24 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
error_messages={"unique": "A user with that username already exists."},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
models.CharField(blank=True, max_length=150, verbose_name="first name"),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
models.CharField(blank=True, max_length=150, verbose_name="last name"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
models.EmailField(blank=True, max_length=254, verbose_name="email address"),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
@@ -119,9 +107,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"),
|
||||
),
|
||||
(
|
||||
"user_id",
|
||||
|
||||
@@ -41,9 +41,7 @@ class Migration(migrations.Migration):
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"expires_at",
|
||||
models.DateTimeField(
|
||||
help_text="When this deletion request expires"
|
||||
),
|
||||
models.DateTimeField(help_text="When this deletion request expires"),
|
||||
),
|
||||
(
|
||||
"email_sent_at",
|
||||
@@ -55,9 +53,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"attempts",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of verification attempts made"
|
||||
),
|
||||
models.PositiveIntegerField(default=0, help_text="Number of verification attempts made"),
|
||||
),
|
||||
(
|
||||
"max_attempts",
|
||||
@@ -103,9 +99,7 @@ class Migration(migrations.Migration):
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"expires_at",
|
||||
models.DateTimeField(
|
||||
help_text="When this deletion request expires"
|
||||
),
|
||||
models.DateTimeField(help_text="When this deletion request expires"),
|
||||
),
|
||||
(
|
||||
"email_sent_at",
|
||||
@@ -117,9 +111,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"attempts",
|
||||
models.PositiveIntegerField(
|
||||
default=0, help_text="Number of verification attempts made"
|
||||
),
|
||||
models.PositiveIntegerField(default=0, help_text="Number of verification attempts made"),
|
||||
),
|
||||
(
|
||||
"max_attempts",
|
||||
@@ -171,21 +163,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="userdeletionrequest",
|
||||
index=models.Index(
|
||||
fields=["verification_code"], name="accounts_us_verific_94460d_idx"
|
||||
),
|
||||
index=models.Index(fields=["verification_code"], name="accounts_us_verific_94460d_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="userdeletionrequest",
|
||||
index=models.Index(
|
||||
fields=["expires_at"], name="accounts_us_expires_1d1dca_idx"
|
||||
),
|
||||
index=models.Index(fields=["expires_at"], name="accounts_us_expires_1d1dca_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="userdeletionrequest",
|
||||
index=models.Index(
|
||||
fields=["user", "is_used"], name="accounts_us_user_id_1ce18a_idx"
|
||||
),
|
||||
index=models.Index(fields=["user", "is_used"], name="accounts_us_user_id_1ce18a_idx"),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="userdeletionrequest",
|
||||
|
||||
@@ -57,9 +57,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="last_password_change",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
@@ -185,9 +183,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="userevent",
|
||||
name="last_password_change",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
||||
@@ -454,9 +454,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usernotification",
|
||||
index=models.Index(
|
||||
fields=["user", "is_read"], name="accounts_us_user_id_785929_idx"
|
||||
),
|
||||
index=models.Index(fields=["user", "is_read"], name="accounts_us_user_id_785929_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usernotification",
|
||||
@@ -467,15 +465,11 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usernotification",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="accounts_us_created_a62f54_idx"
|
||||
),
|
||||
index=models.Index(fields=["created_at"], name="accounts_us_created_a62f54_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usernotification",
|
||||
index=models.Index(
|
||||
fields=["expires_at"], name="accounts_us_expires_f267b1_idx"
|
||||
),
|
||||
index=models.Index(fields=["expires_at"], name="accounts_us_expires_f267b1_idx"),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="usernotification",
|
||||
|
||||
@@ -26,25 +26,24 @@ def safe_add_avatar_field(apps, schema_editor):
|
||||
"""
|
||||
# Check if the column already exists
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name='accounts_userprofile'
|
||||
AND column_name='avatar_id'
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
column_exists = cursor.fetchone() is not None
|
||||
|
||||
if not column_exists:
|
||||
# Column doesn't exist, add it
|
||||
UserProfile = apps.get_model('accounts', 'UserProfile')
|
||||
UserProfile = apps.get_model("accounts", "UserProfile")
|
||||
field = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True
|
||||
"django_cloudflareimages_toolkit.CloudflareImage", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
field.set_attributes_from_name('avatar')
|
||||
field.set_attributes_from_name("avatar")
|
||||
schema_editor.add_field(UserProfile, field)
|
||||
|
||||
|
||||
@@ -54,24 +53,23 @@ def reverse_safe_add_avatar_field(apps, schema_editor):
|
||||
"""
|
||||
# Check if the column exists and remove it
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name='accounts_userprofile'
|
||||
AND column_name='avatar_id'
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
column_exists = cursor.fetchone() is not None
|
||||
|
||||
if column_exists:
|
||||
UserProfile = apps.get_model('accounts', 'UserProfile')
|
||||
UserProfile = apps.get_model("accounts", "UserProfile")
|
||||
field = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True
|
||||
"django_cloudflareimages_toolkit.CloudflareImage", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
field.set_attributes_from_name('avatar')
|
||||
field.set_attributes_from_name("avatar")
|
||||
schema_editor.remove_field(UserProfile, field)
|
||||
|
||||
|
||||
@@ -89,15 +87,13 @@ class Migration(migrations.Migration):
|
||||
# First, remove the old avatar column (CloudflareImageField)
|
||||
migrations.RunSQL(
|
||||
"ALTER TABLE accounts_userprofile DROP COLUMN IF EXISTS avatar;",
|
||||
reverse_sql="-- Cannot reverse this operation"
|
||||
reverse_sql="-- Cannot reverse this operation",
|
||||
),
|
||||
|
||||
# Safely add the new avatar_id column for ForeignKey
|
||||
migrations.RunPython(
|
||||
safe_add_avatar_field,
|
||||
reverse_safe_add_avatar_field,
|
||||
),
|
||||
|
||||
# Run the data migration
|
||||
migrations.RunPython(
|
||||
migrate_avatar_data,
|
||||
|
||||
@@ -6,17 +6,16 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0010_auto_20250830_1657'),
|
||||
('django_cloudflareimages_toolkit', '0001_initial'),
|
||||
("accounts", "0010_auto_20250830_1657"),
|
||||
("django_cloudflareimages_toolkit", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Remove the old avatar field from the event table
|
||||
migrations.RunSQL(
|
||||
"ALTER TABLE accounts_userprofileevent DROP COLUMN IF EXISTS avatar;",
|
||||
reverse_sql="-- Cannot reverse this operation"
|
||||
reverse_sql="-- Cannot reverse this operation",
|
||||
),
|
||||
|
||||
# Add the new avatar_id field to match the main table (only if it doesn't exist)
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
@@ -32,6 +31,6 @@ class Migration(migrations.Migration):
|
||||
END IF;
|
||||
END $$;
|
||||
""",
|
||||
reverse_sql="ALTER TABLE accounts_userprofileevent DROP COLUMN IF EXISTS avatar_id;"
|
||||
reverse_sql="ALTER TABLE accounts_userprofileevent DROP COLUMN IF EXISTS avatar_id;",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -13,28 +13,28 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0012_alter_toplist_category_and_more'),
|
||||
("accounts", "0012_alter_toplist_category_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Add db_index to is_banned field
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='is_banned',
|
||||
model_name="user",
|
||||
name="is_banned",
|
||||
field=models.BooleanField(default=False, db_index=True),
|
||||
),
|
||||
# Add composite index for common query patterns
|
||||
migrations.AddIndex(
|
||||
model_name='user',
|
||||
index=models.Index(fields=['is_banned', 'role'], name='accounts_user_banned_role_idx'),
|
||||
model_name="user",
|
||||
index=models.Index(fields=["is_banned", "role"], name="accounts_user_banned_role_idx"),
|
||||
),
|
||||
# Add CheckConstraint for ban consistency
|
||||
migrations.AddConstraint(
|
||||
model_name='user',
|
||||
model_name="user",
|
||||
constraint=models.CheckConstraint(
|
||||
name='user_ban_consistency',
|
||||
name="user_ban_consistency",
|
||||
check=models.Q(is_banned=False) | models.Q(ban_date__isnull=False),
|
||||
violation_error_message='Banned users must have a ban_date set'
|
||||
violation_error_message="Banned users must have a ban_date set",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -18,7 +18,6 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={"verbose_name": "User", "verbose_name_plural": "Users"},
|
||||
@@ -58,9 +57,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="location",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="User's location (City, Country)", max_length=100
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="User's location (City, Country)", max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
@@ -78,9 +75,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="userprofileevent",
|
||||
name="location",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="User's location (City, Country)", max_length=100
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="User's location (City, Country)", max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofileevent",
|
||||
@@ -98,23 +93,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="emailverification",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this verification was created"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When this verification was created"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailverification",
|
||||
name="last_sent",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the verification email was last sent"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When the verification email was last sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailverification",
|
||||
name="token",
|
||||
field=models.CharField(
|
||||
help_text="Verification token", max_length=64, unique=True
|
||||
),
|
||||
field=models.CharField(help_text="Verification token", max_length=64, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailverification",
|
||||
@@ -128,16 +117,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="emailverificationevent",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this verification was created"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When this verification was created"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailverificationevent",
|
||||
name="last_sent",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the verification email was last sent"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When the verification email was last sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailverificationevent",
|
||||
@@ -181,9 +166,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="passwordreset",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this reset was requested"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When this reset was requested"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="passwordreset",
|
||||
@@ -198,9 +181,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="passwordreset",
|
||||
name="used",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this token has been used"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether this token has been used"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="passwordreset",
|
||||
@@ -214,9 +195,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="passwordresetevent",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this reset was requested"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When this reset was requested"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="passwordresetevent",
|
||||
@@ -231,9 +210,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="passwordresetevent",
|
||||
name="used",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this token has been used"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether this token has been used"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="passwordresetevent",
|
||||
@@ -267,30 +244,22 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="allow_friend_requests",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to allow friend requests"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to allow friend requests"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="allow_messages",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to allow direct messages"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to allow direct messages"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="allow_profile_comments",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to allow profile comments"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to allow profile comments"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="ban_date",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="Date the user was banned", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="Date the user was banned", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
@@ -300,37 +269,27 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="email_notifications",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to send email notifications"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to send email notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="is_banned",
|
||||
field=models.BooleanField(
|
||||
db_index=True, default=False, help_text="Whether this user is banned"
|
||||
),
|
||||
field=models.BooleanField(db_index=True, default=False, help_text="Whether this user is banned"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="last_password_change",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the password was last changed"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When the password was last changed"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="login_history_retention",
|
||||
field=models.IntegerField(
|
||||
default=90, help_text="How long to retain login history (days)"
|
||||
),
|
||||
field=models.IntegerField(default=90, help_text="How long to retain login history (days)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="login_notifications",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to send login notifications"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to send login notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
@@ -352,9 +311,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="push_notifications",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to send push notifications"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to send push notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
@@ -378,9 +335,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="search_visibility",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether profile appears in search results"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether profile appears in search results"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
@@ -390,51 +345,37 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_email",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to show email on profile"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to show email on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_join_date",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show join date on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show join date on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_photos",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show photos on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show photos on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_real_name",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show real name on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show real name on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_reviews",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show reviews on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show reviews on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_statistics",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show statistics on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show statistics on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="show_top_lists",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show top lists on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show top lists on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
@@ -452,9 +393,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="two_factor_enabled",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether two-factor authentication is enabled"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether two-factor authentication is enabled"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -476,30 +415,22 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="allow_friend_requests",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to allow friend requests"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to allow friend requests"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="allow_messages",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to allow direct messages"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to allow direct messages"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="allow_profile_comments",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to allow profile comments"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to allow profile comments"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="ban_date",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="Date the user was banned", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="Date the user was banned", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -509,37 +440,27 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="email_notifications",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to send email notifications"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to send email notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="is_banned",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this user is banned"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether this user is banned"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="last_password_change",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the password was last changed"
|
||||
),
|
||||
field=models.DateTimeField(auto_now_add=True, help_text="When the password was last changed"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="login_history_retention",
|
||||
field=models.IntegerField(
|
||||
default=90, help_text="How long to retain login history (days)"
|
||||
),
|
||||
field=models.IntegerField(default=90, help_text="How long to retain login history (days)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="login_notifications",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to send login notifications"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to send login notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -561,9 +482,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="push_notifications",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to send push notifications"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to send push notifications"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -586,9 +505,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="search_visibility",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether profile appears in search results"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether profile appears in search results"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -598,51 +515,37 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_email",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether to show email on profile"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether to show email on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_join_date",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show join date on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show join date on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_photos",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show photos on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show photos on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_real_name",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show real name on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show real name on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_reviews",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show reviews on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show reviews on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_statistics",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show statistics on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show statistics on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="show_top_lists",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether to show top lists on profile"
|
||||
),
|
||||
field=models.BooleanField(default=True, help_text="Whether to show top lists on profile"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
@@ -660,9 +563,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userevent",
|
||||
name="two_factor_enabled",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether two-factor authentication is enabled"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether two-factor authentication is enabled"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
@@ -678,23 +579,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="email_sent",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether email was sent"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether email was sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="email_sent_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When email was sent", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When email was sent", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="is_read",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this notification has been read"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether this notification has been read"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
@@ -704,30 +599,22 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="ID of related object", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="ID of related object", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="push_sent",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether push notification was sent"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether push notification was sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="push_sent_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When push notification was sent", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When push notification was sent", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
name="read_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this notification was read", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When this notification was read", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotification",
|
||||
@@ -761,23 +648,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="email_sent",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether email was sent"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether email was sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="email_sent_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When email was sent", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When email was sent", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="is_read",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether this notification has been read"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether this notification has been read"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
@@ -787,30 +668,22 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="ID of related object", null=True
|
||||
),
|
||||
field=models.PositiveIntegerField(blank=True, help_text="ID of related object", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="push_sent",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Whether push notification was sent"
|
||||
),
|
||||
field=models.BooleanField(default=False, help_text="Whether push notification was sent"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="push_sent_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When push notification was sent", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When push notification was sent", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
name="read_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this notification was read", null=True
|
||||
),
|
||||
field=models.DateTimeField(blank=True, help_text="When this notification was read", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="usernotificationevent",
|
||||
@@ -844,37 +717,27 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="bio",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="User biography", max_length=500
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="User biography", max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="coaster_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of roller coasters ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of roller coasters ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="dark_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of dark rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of dark rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="discord",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Discord username", max_length=100
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Discord username", max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="flat_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of flat rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of flat rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
@@ -884,9 +747,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="pronouns",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="User's preferred pronouns", max_length=50
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="User's preferred pronouns", max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
@@ -906,9 +767,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="water_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of water rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of water rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
@@ -932,37 +791,27 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="bio",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="User biography", max_length=500
|
||||
),
|
||||
field=models.TextField(blank=True, help_text="User biography", max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="coaster_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of roller coasters ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of roller coasters ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="dark_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of dark rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of dark rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="discord",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Discord username", max_length=100
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="Discord username", max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="flat_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of flat rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of flat rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
@@ -972,9 +821,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="pronouns",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="User's preferred pronouns", max_length=50
|
||||
),
|
||||
field=models.CharField(blank=True, help_text="User's preferred pronouns", max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
@@ -996,9 +843,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
name="water_ride_credits",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of water rides ridden"
|
||||
),
|
||||
field=models.IntegerField(default=0, help_text="Number of water rides ridden"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userprofileevent",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Mixins for authentication views.
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from apps.core.utils.turnstile import get_client_ip, validate_turnstile_token
|
||||
@@ -24,14 +25,14 @@ class TurnstileMixin:
|
||||
token = None
|
||||
|
||||
# Check POST data (form submissions)
|
||||
if hasattr(request, 'POST'):
|
||||
if hasattr(request, "POST"):
|
||||
token = request.POST.get("cf-turnstile-response")
|
||||
|
||||
# Check JSON body (API requests)
|
||||
if not token and hasattr(request, 'data'):
|
||||
data = getattr(request, 'data', {})
|
||||
if hasattr(data, 'get'):
|
||||
token = data.get('turnstile_token') or data.get('cf-turnstile-response')
|
||||
if not token and hasattr(request, "data"):
|
||||
data = getattr(request, "data", {})
|
||||
if hasattr(data, "get"):
|
||||
token = data.get("turnstile_token") or data.get("cf-turnstile-response")
|
||||
|
||||
# Get client IP
|
||||
ip = get_client_ip(request)
|
||||
@@ -39,6 +40,6 @@ class TurnstileMixin:
|
||||
# Validate the token
|
||||
result = validate_turnstile_token(token, ip)
|
||||
|
||||
if not result.get('success'):
|
||||
error_msg = result.get('error', 'Captcha verification failed. Please try again.')
|
||||
if not result.get("success"):
|
||||
error_msg = result.get("error", "Captcha verification failed. Please try again.")
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
@@ -41,10 +41,7 @@ class User(AbstractUser):
|
||||
max_length=10,
|
||||
unique=True,
|
||||
editable=False,
|
||||
help_text=(
|
||||
"Unique identifier for this user that remains constant even if the "
|
||||
"username changes"
|
||||
),
|
||||
help_text=("Unique identifier for this user that remains constant even if the " "username changes"),
|
||||
)
|
||||
|
||||
role = RichChoiceField(
|
||||
@@ -55,13 +52,9 @@ class User(AbstractUser):
|
||||
db_index=True,
|
||||
help_text="User role (user, moderator, admin)",
|
||||
)
|
||||
is_banned = models.BooleanField(
|
||||
default=False, db_index=True, help_text="Whether this user is banned"
|
||||
)
|
||||
is_banned = models.BooleanField(default=False, db_index=True, help_text="Whether this user is banned")
|
||||
ban_reason = models.TextField(blank=True, help_text="Reason for ban")
|
||||
ban_date = models.DateTimeField(
|
||||
null=True, blank=True, help_text="Date the user was banned"
|
||||
)
|
||||
ban_date = models.DateTimeField(null=True, blank=True, help_text="Date the user was banned")
|
||||
pending_email = models.EmailField(blank=True, null=True)
|
||||
theme_preference = RichChoiceField(
|
||||
choice_group="theme_preferences",
|
||||
@@ -72,12 +65,8 @@ class User(AbstractUser):
|
||||
)
|
||||
|
||||
# Notification preferences
|
||||
email_notifications = models.BooleanField(
|
||||
default=True, help_text="Whether to send email notifications"
|
||||
)
|
||||
push_notifications = models.BooleanField(
|
||||
default=False, help_text="Whether to send push notifications"
|
||||
)
|
||||
email_notifications = models.BooleanField(default=True, help_text="Whether to send email notifications")
|
||||
push_notifications = models.BooleanField(default=False, help_text="Whether to send push notifications")
|
||||
|
||||
# Privacy settings
|
||||
privacy_level = RichChoiceField(
|
||||
@@ -87,39 +76,17 @@ class User(AbstractUser):
|
||||
default="public",
|
||||
help_text="Overall privacy level",
|
||||
)
|
||||
show_email = models.BooleanField(
|
||||
default=False, help_text="Whether to show email on profile"
|
||||
)
|
||||
show_real_name = models.BooleanField(
|
||||
default=True, help_text="Whether to show real name on profile"
|
||||
)
|
||||
show_join_date = models.BooleanField(
|
||||
default=True, help_text="Whether to show join date on profile"
|
||||
)
|
||||
show_statistics = models.BooleanField(
|
||||
default=True, help_text="Whether to show statistics on profile"
|
||||
)
|
||||
show_reviews = models.BooleanField(
|
||||
default=True, help_text="Whether to show reviews on profile"
|
||||
)
|
||||
show_photos = models.BooleanField(
|
||||
default=True, help_text="Whether to show photos on profile"
|
||||
)
|
||||
show_top_lists = models.BooleanField(
|
||||
default=True, help_text="Whether to show top lists on profile"
|
||||
)
|
||||
allow_friend_requests = models.BooleanField(
|
||||
default=True, help_text="Whether to allow friend requests"
|
||||
)
|
||||
allow_messages = models.BooleanField(
|
||||
default=True, help_text="Whether to allow direct messages"
|
||||
)
|
||||
allow_profile_comments = models.BooleanField(
|
||||
default=False, help_text="Whether to allow profile comments"
|
||||
)
|
||||
search_visibility = models.BooleanField(
|
||||
default=True, help_text="Whether profile appears in search results"
|
||||
)
|
||||
show_email = models.BooleanField(default=False, help_text="Whether to show email on profile")
|
||||
show_real_name = models.BooleanField(default=True, help_text="Whether to show real name on profile")
|
||||
show_join_date = models.BooleanField(default=True, help_text="Whether to show join date on profile")
|
||||
show_statistics = models.BooleanField(default=True, help_text="Whether to show statistics on profile")
|
||||
show_reviews = models.BooleanField(default=True, help_text="Whether to show reviews on profile")
|
||||
show_photos = models.BooleanField(default=True, help_text="Whether to show photos on profile")
|
||||
show_top_lists = models.BooleanField(default=True, help_text="Whether to show top lists on profile")
|
||||
allow_friend_requests = models.BooleanField(default=True, help_text="Whether to allow friend requests")
|
||||
allow_messages = models.BooleanField(default=True, help_text="Whether to allow direct messages")
|
||||
allow_profile_comments = models.BooleanField(default=False, help_text="Whether to allow profile comments")
|
||||
search_visibility = models.BooleanField(default=True, help_text="Whether profile appears in search results")
|
||||
activity_visibility = RichChoiceField(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
@@ -129,21 +96,11 @@ class User(AbstractUser):
|
||||
)
|
||||
|
||||
# Security settings
|
||||
two_factor_enabled = models.BooleanField(
|
||||
default=False, help_text="Whether two-factor authentication is enabled"
|
||||
)
|
||||
login_notifications = models.BooleanField(
|
||||
default=True, help_text="Whether to send login notifications"
|
||||
)
|
||||
session_timeout = models.IntegerField(
|
||||
default=30, help_text="Session timeout in days"
|
||||
)
|
||||
login_history_retention = models.IntegerField(
|
||||
default=90, help_text="How long to retain login history (days)"
|
||||
)
|
||||
last_password_change = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the password was last changed"
|
||||
)
|
||||
two_factor_enabled = models.BooleanField(default=False, help_text="Whether two-factor authentication is enabled")
|
||||
login_notifications = models.BooleanField(default=True, help_text="Whether to send login notifications")
|
||||
session_timeout = models.IntegerField(default=30, help_text="Session timeout in days")
|
||||
login_history_retention = models.IntegerField(default=90, help_text="How long to retain login history (days)")
|
||||
last_password_change = models.DateTimeField(auto_now_add=True, help_text="When the password was last changed")
|
||||
|
||||
# Display name - core user data for better performance
|
||||
display_name = models.CharField(
|
||||
@@ -179,13 +136,13 @@ class User(AbstractUser):
|
||||
verbose_name = "User"
|
||||
verbose_name_plural = "Users"
|
||||
indexes = [
|
||||
models.Index(fields=['is_banned', 'role'], name='accounts_user_banned_role_idx'),
|
||||
models.Index(fields=["is_banned", "role"], name="accounts_user_banned_role_idx"),
|
||||
]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
name='user_ban_consistency',
|
||||
name="user_ban_consistency",
|
||||
check=models.Q(is_banned=False) | models.Q(ban_date__isnull=False),
|
||||
violation_error_message='Banned users must have a ban_date set'
|
||||
violation_error_message="Banned users must have a ban_date set",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -224,14 +181,10 @@ class UserProfile(models.Model):
|
||||
related_name="user_profiles",
|
||||
help_text="User's avatar image",
|
||||
)
|
||||
pronouns = models.CharField(
|
||||
max_length=50, blank=True, help_text="User's preferred pronouns"
|
||||
)
|
||||
pronouns = models.CharField(max_length=50, blank=True, help_text="User's preferred pronouns")
|
||||
|
||||
bio = models.TextField(max_length=500, blank=True, help_text="User biography")
|
||||
location = models.CharField(
|
||||
max_length=100, blank=True, help_text="User's location (City, Country)"
|
||||
)
|
||||
location = models.CharField(max_length=100, blank=True, help_text="User's location (City, Country)")
|
||||
unit_system = RichChoiceField(
|
||||
choice_group="unit_systems",
|
||||
domain="accounts",
|
||||
@@ -247,18 +200,10 @@ class UserProfile(models.Model):
|
||||
discord = models.CharField(max_length=100, blank=True, help_text="Discord username")
|
||||
|
||||
# Ride statistics
|
||||
coaster_credits = models.IntegerField(
|
||||
default=0, help_text="Number of roller coasters ridden"
|
||||
)
|
||||
dark_ride_credits = models.IntegerField(
|
||||
default=0, help_text="Number of dark rides ridden"
|
||||
)
|
||||
flat_ride_credits = models.IntegerField(
|
||||
default=0, help_text="Number of flat rides ridden"
|
||||
)
|
||||
water_ride_credits = models.IntegerField(
|
||||
default=0, help_text="Number of water rides ridden"
|
||||
)
|
||||
coaster_credits = models.IntegerField(default=0, help_text="Number of roller coasters ridden")
|
||||
dark_ride_credits = models.IntegerField(default=0, help_text="Number of dark rides ridden")
|
||||
flat_ride_credits = models.IntegerField(default=0, help_text="Number of flat rides ridden")
|
||||
water_ride_credits = models.IntegerField(default=0, help_text="Number of water rides ridden")
|
||||
|
||||
def get_avatar_url(self):
|
||||
"""
|
||||
@@ -266,12 +211,12 @@ class UserProfile(models.Model):
|
||||
"""
|
||||
if self.avatar and self.avatar.is_uploaded:
|
||||
# Try to get avatar variant first, fallback to public
|
||||
avatar_url = self.avatar.get_url('avatar')
|
||||
avatar_url = self.avatar.get_url("avatar")
|
||||
if avatar_url:
|
||||
return avatar_url
|
||||
|
||||
# Fallback to public variant
|
||||
public_url = self.avatar.get_url('public')
|
||||
public_url = self.avatar.get_url("public")
|
||||
if public_url:
|
||||
return public_url
|
||||
|
||||
@@ -298,10 +243,10 @@ class UserProfile(models.Model):
|
||||
variants = {}
|
||||
|
||||
# Try to get specific variants
|
||||
thumbnail_url = self.avatar.get_url('thumbnail')
|
||||
avatar_url = self.avatar.get_url('avatar')
|
||||
large_url = self.avatar.get_url('large')
|
||||
public_url = self.avatar.get_url('public')
|
||||
thumbnail_url = self.avatar.get_url("thumbnail")
|
||||
avatar_url = self.avatar.get_url("avatar")
|
||||
large_url = self.avatar.get_url("large")
|
||||
public_url = self.avatar.get_url("public")
|
||||
|
||||
# Use specific variants if available, otherwise fallback to public or first available
|
||||
fallback_url = public_url
|
||||
@@ -354,18 +299,10 @@ class EmailVerification(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
help_text="User this verification belongs to",
|
||||
)
|
||||
token = models.CharField(
|
||||
max_length=64, unique=True, help_text="Verification token"
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this verification was created"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this verification was last updated"
|
||||
)
|
||||
last_sent = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When the verification email was last sent"
|
||||
)
|
||||
token = models.CharField(max_length=64, unique=True, help_text="Verification token")
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="When this verification was created")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this verification was last updated")
|
||||
last_sent = models.DateTimeField(auto_now_add=True, help_text="When the verification email was last sent")
|
||||
|
||||
def __str__(self):
|
||||
return f"Email verification for {self.user.username}"
|
||||
@@ -383,9 +320,7 @@ class PasswordReset(models.Model):
|
||||
help_text="User requesting password reset",
|
||||
)
|
||||
token = models.CharField(max_length=64, help_text="Reset token")
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this reset was requested"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="When this reset was requested")
|
||||
expires_at = models.DateTimeField(help_text="When this reset token expires")
|
||||
used = models.BooleanField(default=False, help_text="Whether this token has been used")
|
||||
|
||||
@@ -397,8 +332,6 @@ class PasswordReset(models.Model):
|
||||
verbose_name_plural = "Password Resets"
|
||||
|
||||
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class UserDeletionRequest(models.Model):
|
||||
"""
|
||||
@@ -409,9 +342,7 @@ class UserDeletionRequest(models.Model):
|
||||
provide the correct code.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="deletion_request"
|
||||
)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="deletion_request")
|
||||
|
||||
verification_code = models.CharField(
|
||||
max_length=32,
|
||||
@@ -422,21 +353,13 @@ class UserDeletionRequest(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField(help_text="When this deletion request expires")
|
||||
|
||||
email_sent_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When the verification email was sent"
|
||||
)
|
||||
email_sent_at = models.DateTimeField(null=True, blank=True, help_text="When the verification email was sent")
|
||||
|
||||
attempts = models.PositiveIntegerField(
|
||||
default=0, help_text="Number of verification attempts made"
|
||||
)
|
||||
attempts = models.PositiveIntegerField(default=0, help_text="Number of verification attempts made")
|
||||
|
||||
max_attempts = models.PositiveIntegerField(
|
||||
default=5, help_text="Maximum number of verification attempts allowed"
|
||||
)
|
||||
max_attempts = models.PositiveIntegerField(default=5, help_text="Maximum number of verification attempts allowed")
|
||||
|
||||
is_used = models.BooleanField(
|
||||
default=False, help_text="Whether this deletion request has been used"
|
||||
)
|
||||
is_used = models.BooleanField(default=False, help_text="Whether this deletion request has been used")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User Deletion Request"
|
||||
@@ -466,9 +389,7 @@ class UserDeletionRequest(models.Model):
|
||||
"""Generate a unique 8-character verification code."""
|
||||
while True:
|
||||
# Generate a random 8-character alphanumeric code
|
||||
code = "".join(
|
||||
secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8)
|
||||
)
|
||||
code = "".join(secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8))
|
||||
|
||||
# Ensure it's unique
|
||||
if not UserDeletionRequest.objects.filter(verification_code=code).exists():
|
||||
@@ -480,11 +401,7 @@ class UserDeletionRequest(models.Model):
|
||||
|
||||
def is_valid(self):
|
||||
"""Check if this deletion request is still valid."""
|
||||
return (
|
||||
not self.is_used
|
||||
and not self.is_expired()
|
||||
and self.attempts < self.max_attempts
|
||||
)
|
||||
return not self.is_used and not self.is_expired() and self.attempts < self.max_attempts
|
||||
|
||||
def increment_attempts(self):
|
||||
"""Increment the number of verification attempts."""
|
||||
@@ -499,9 +416,7 @@ class UserDeletionRequest(models.Model):
|
||||
@classmethod
|
||||
def cleanup_expired(cls):
|
||||
"""Remove expired deletion requests."""
|
||||
expired_requests = cls.objects.filter(
|
||||
expires_at__lt=timezone.now(), is_used=False
|
||||
)
|
||||
expired_requests = cls.objects.filter(expires_at__lt=timezone.now(), is_used=False)
|
||||
count = expired_requests.count()
|
||||
expired_requests.delete()
|
||||
return count
|
||||
@@ -541,9 +456,7 @@ class UserNotification(TrackedModel):
|
||||
blank=True,
|
||||
help_text="Type of related object",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(
|
||||
null=True, blank=True, help_text="ID of related object"
|
||||
)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True, help_text="ID of related object")
|
||||
related_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
# Metadata
|
||||
@@ -555,24 +468,14 @@ class UserNotification(TrackedModel):
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
is_read = models.BooleanField(
|
||||
default=False, help_text="Whether this notification has been read"
|
||||
)
|
||||
read_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this notification was read"
|
||||
)
|
||||
is_read = models.BooleanField(default=False, help_text="Whether this notification has been read")
|
||||
read_at = models.DateTimeField(null=True, blank=True, help_text="When this notification was read")
|
||||
|
||||
# Delivery tracking
|
||||
email_sent = models.BooleanField(default=False, help_text="Whether email was sent")
|
||||
email_sent_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When email was sent"
|
||||
)
|
||||
push_sent = models.BooleanField(
|
||||
default=False, help_text="Whether push notification was sent"
|
||||
)
|
||||
push_sent_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When push notification was sent"
|
||||
)
|
||||
email_sent_at = models.DateTimeField(null=True, blank=True, help_text="When email was sent")
|
||||
push_sent = models.BooleanField(default=False, help_text="Whether push notification was sent")
|
||||
push_sent_at = models.DateTimeField(null=True, blank=True, help_text="When push notification was sent")
|
||||
|
||||
# Additional data (JSON field for flexibility)
|
||||
extra_data = models.JSONField(default=dict, blank=True)
|
||||
@@ -619,9 +522,7 @@ class UserNotification(TrackedModel):
|
||||
@classmethod
|
||||
def mark_all_read_for_user(cls, user):
|
||||
"""Mark all notifications as read for a specific user."""
|
||||
return cls.objects.filter(user=user, is_read=False).update(
|
||||
is_read=True, read_at=timezone.now()
|
||||
)
|
||||
return cls.objects.filter(user=user, is_read=False).update(is_read=True, read_at=timezone.now())
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
|
||||
@@ -27,16 +27,10 @@ def user_profile_optimized(*, user_id: int) -> Any:
|
||||
User.DoesNotExist: If user doesn't exist
|
||||
"""
|
||||
return (
|
||||
User.objects.prefetch_related(
|
||||
"park_reviews", "ride_reviews", "socialaccount_set"
|
||||
)
|
||||
User.objects.prefetch_related("park_reviews", "ride_reviews", "socialaccount_set")
|
||||
.annotate(
|
||||
park_review_count=Count(
|
||||
"park_reviews", filter=Q(park_reviews__is_published=True)
|
||||
),
|
||||
ride_review_count=Count(
|
||||
"ride_reviews", filter=Q(ride_reviews__is_published=True)
|
||||
),
|
||||
park_review_count=Count("park_reviews", filter=Q(park_reviews__is_published=True)),
|
||||
ride_review_count=Count("ride_reviews", filter=Q(ride_reviews__is_published=True)),
|
||||
total_review_count=F("park_review_count") + F("ride_review_count"),
|
||||
)
|
||||
.get(id=user_id)
|
||||
@@ -53,12 +47,8 @@ def active_users_with_stats() -> QuerySet:
|
||||
return (
|
||||
User.objects.filter(is_active=True)
|
||||
.annotate(
|
||||
park_review_count=Count(
|
||||
"park_reviews", filter=Q(park_reviews__is_published=True)
|
||||
),
|
||||
ride_review_count=Count(
|
||||
"ride_reviews", filter=Q(ride_reviews__is_published=True)
|
||||
),
|
||||
park_review_count=Count("park_reviews", filter=Q(park_reviews__is_published=True)),
|
||||
ride_review_count=Count("ride_reviews", filter=Q(ride_reviews__is_published=True)),
|
||||
total_review_count=F("park_review_count") + F("ride_review_count"),
|
||||
)
|
||||
.order_by("-total_review_count")
|
||||
@@ -112,12 +102,8 @@ def top_reviewers(*, limit: int = 10) -> QuerySet:
|
||||
return (
|
||||
User.objects.filter(is_active=True)
|
||||
.annotate(
|
||||
park_review_count=Count(
|
||||
"park_reviews", filter=Q(park_reviews__is_published=True)
|
||||
),
|
||||
ride_review_count=Count(
|
||||
"ride_reviews", filter=Q(ride_reviews__is_published=True)
|
||||
),
|
||||
park_review_count=Count("park_reviews", filter=Q(park_reviews__is_published=True)),
|
||||
ride_review_count=Count("ride_reviews", filter=Q(ride_reviews__is_published=True)),
|
||||
total_review_count=F("park_review_count") + F("ride_review_count"),
|
||||
)
|
||||
.filter(total_review_count__gt=0)
|
||||
@@ -159,9 +145,9 @@ def users_by_registration_date(*, start_date, end_date) -> QuerySet:
|
||||
Returns:
|
||||
QuerySet of users registered in the date range
|
||||
"""
|
||||
return User.objects.filter(
|
||||
date_joined__date__gte=start_date, date_joined__date__lte=end_date
|
||||
).order_by("-date_joined")
|
||||
return User.objects.filter(date_joined__date__gte=start_date, date_joined__date__lte=end_date).order_by(
|
||||
"-date_joined"
|
||||
)
|
||||
|
||||
|
||||
def user_search_autocomplete(*, query: str, limit: int = 10) -> QuerySet:
|
||||
@@ -176,8 +162,7 @@ def user_search_autocomplete(*, query: str, limit: int = 10) -> QuerySet:
|
||||
QuerySet of matching users for autocomplete
|
||||
"""
|
||||
return User.objects.filter(
|
||||
Q(username__icontains=query)
|
||||
| Q(display_name__icontains=query),
|
||||
Q(username__icontains=query) | Q(display_name__icontains=query),
|
||||
is_active=True,
|
||||
).order_by("username")[:limit]
|
||||
|
||||
@@ -210,11 +195,7 @@ def user_statistics_summary() -> dict[str, Any]:
|
||||
|
||||
# Users with reviews
|
||||
users_with_reviews = (
|
||||
User.objects.filter(
|
||||
Q(park_reviews__isnull=False) | Q(ride_reviews__isnull=False)
|
||||
)
|
||||
.distinct()
|
||||
.count()
|
||||
User.objects.filter(Q(park_reviews__isnull=False) | Q(ride_reviews__isnull=False)).distinct().count()
|
||||
)
|
||||
|
||||
# Recent registrations (last 30 days)
|
||||
@@ -228,9 +209,7 @@ def user_statistics_summary() -> dict[str, Any]:
|
||||
"staff_users": staff_users,
|
||||
"users_with_reviews": users_with_reviews,
|
||||
"recent_registrations": recent_registrations,
|
||||
"review_participation_rate": (
|
||||
(users_with_reviews / total_users * 100) if total_users > 0 else 0
|
||||
),
|
||||
"review_participation_rate": ((users_with_reviews / total_users * 100) if total_users > 0 else 0),
|
||||
}
|
||||
|
||||
|
||||
@@ -241,11 +220,7 @@ def users_needing_email_verification() -> QuerySet:
|
||||
Returns:
|
||||
QuerySet of users with unverified emails
|
||||
"""
|
||||
return (
|
||||
User.objects.filter(is_active=True, emailaddress__verified=False)
|
||||
.distinct()
|
||||
.order_by("date_joined")
|
||||
)
|
||||
return User.objects.filter(is_active=True, emailaddress__verified=False).distinct().order_by("date_joined")
|
||||
|
||||
|
||||
def users_by_review_activity(*, min_reviews: int = 1) -> QuerySet:
|
||||
@@ -260,12 +235,8 @@ def users_by_review_activity(*, min_reviews: int = 1) -> QuerySet:
|
||||
"""
|
||||
return (
|
||||
User.objects.annotate(
|
||||
park_review_count=Count(
|
||||
"park_reviews", filter=Q(park_reviews__is_published=True)
|
||||
),
|
||||
ride_review_count=Count(
|
||||
"ride_reviews", filter=Q(ride_reviews__is_published=True)
|
||||
),
|
||||
park_review_count=Count("park_reviews", filter=Q(park_reviews__is_published=True)),
|
||||
ride_review_count=Count("ride_reviews", filter=Q(ride_reviews__is_published=True)),
|
||||
total_review_count=F("park_review_count") + F("ride_review_count"),
|
||||
)
|
||||
.filter(total_review_count__gte=min_reviews)
|
||||
|
||||
@@ -62,12 +62,8 @@ class LoginSerializer(serializers.Serializer):
|
||||
Serializer for user login
|
||||
"""
|
||||
|
||||
username = serializers.CharField(
|
||||
max_length=254, help_text="Username or email address"
|
||||
)
|
||||
password = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}, trim_whitespace=False
|
||||
)
|
||||
username = serializers.CharField(max_length=254, help_text="Username or email address")
|
||||
password = serializers.CharField(max_length=128, style={"input_type": "password"}, trim_whitespace=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get("username")
|
||||
@@ -89,9 +85,7 @@ class SignupSerializer(serializers.ModelSerializer):
|
||||
validators=[validate_password],
|
||||
style={"input_type": "password"},
|
||||
)
|
||||
password_confirm = serializers.CharField(
|
||||
write_only=True, style={"input_type": "password"}
|
||||
)
|
||||
password_confirm = serializers.CharField(write_only=True, style={"input_type": "password"})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@@ -118,9 +112,7 @@ class SignupSerializer(serializers.ModelSerializer):
|
||||
def validate_username(self, value):
|
||||
"""Validate username is unique"""
|
||||
if UserModel.objects.filter(username=value).exists():
|
||||
raise serializers.ValidationError(
|
||||
"A user with this username already exists."
|
||||
)
|
||||
raise serializers.ValidationError("A user with this username already exists.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
@@ -129,9 +121,7 @@ class SignupSerializer(serializers.ModelSerializer):
|
||||
password_confirm = attrs.get("password_confirm")
|
||||
|
||||
if password != password_confirm:
|
||||
raise serializers.ValidationError(
|
||||
{"password_confirm": "Passwords do not match."}
|
||||
)
|
||||
raise serializers.ValidationError({"password_confirm": "Passwords do not match."})
|
||||
|
||||
return attrs
|
||||
|
||||
@@ -194,9 +184,7 @@ class PasswordResetSerializer(serializers.Serializer):
|
||||
"site_name": site.name,
|
||||
}
|
||||
|
||||
email_html = render_to_string(
|
||||
"accounts/email/password_reset.html", context
|
||||
)
|
||||
email_html = render_to_string("accounts/email/password_reset.html", context)
|
||||
|
||||
# Narrow and validate email type for the static checker
|
||||
email = getattr(self.user, "email", None)
|
||||
@@ -218,15 +206,11 @@ class PasswordChangeSerializer(serializers.Serializer):
|
||||
Serializer for password change
|
||||
"""
|
||||
|
||||
old_password = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}
|
||||
)
|
||||
old_password = serializers.CharField(max_length=128, style={"input_type": "password"})
|
||||
new_password = serializers.CharField(
|
||||
max_length=128, validators=[validate_password], style={"input_type": "password"}
|
||||
)
|
||||
new_password_confirm = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}
|
||||
)
|
||||
new_password_confirm = serializers.CharField(max_length=128, style={"input_type": "password"})
|
||||
|
||||
def validate_old_password(self, value):
|
||||
"""Validate old password is correct"""
|
||||
@@ -241,9 +225,7 @@ class PasswordChangeSerializer(serializers.Serializer):
|
||||
new_password_confirm = attrs.get("new_password_confirm")
|
||||
|
||||
if new_password != new_password_confirm:
|
||||
raise serializers.ValidationError(
|
||||
{"new_password_confirm": "New passwords do not match."}
|
||||
)
|
||||
raise serializers.ValidationError({"new_password_confirm": "New passwords do not match."})
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -81,21 +81,15 @@ class AccountService:
|
||||
"""
|
||||
# Verify old password
|
||||
if not user.check_password(old_password):
|
||||
logger.warning(
|
||||
f"Password change failed: incorrect current password for user {user.id}"
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Current password is incorrect",
|
||||
'redirect_url': None
|
||||
}
|
||||
logger.warning(f"Password change failed: incorrect current password for user {user.id}")
|
||||
return {"success": False, "message": "Current password is incorrect", "redirect_url": None}
|
||||
|
||||
# Validate new password
|
||||
if not AccountService.validate_password(new_password):
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Password must be at least 8 characters and contain uppercase, lowercase, and numbers",
|
||||
'redirect_url': None
|
||||
"success": False,
|
||||
"message": "Password must be at least 8 characters and contain uppercase, lowercase, and numbers",
|
||||
"redirect_url": None,
|
||||
}
|
||||
|
||||
# Update password
|
||||
@@ -111,9 +105,9 @@ class AccountService:
|
||||
logger.info(f"Password changed successfully for user {user.id}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': "Password changed successfully. Please check your email for confirmation.",
|
||||
'redirect_url': None
|
||||
"success": True,
|
||||
"message": "Password changed successfully. Please check your email for confirmation.",
|
||||
"redirect_url": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -125,9 +119,7 @@ class AccountService:
|
||||
"site_name": site.name,
|
||||
}
|
||||
|
||||
email_html = render_to_string(
|
||||
"accounts/email/password_change_confirmation.html", context
|
||||
)
|
||||
email_html = render_to_string("accounts/email/password_change_confirmation.html", context)
|
||||
|
||||
try:
|
||||
EmailService.send_email(
|
||||
@@ -166,26 +158,17 @@ class AccountService:
|
||||
}
|
||||
"""
|
||||
if not new_email:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "New email is required"
|
||||
}
|
||||
return {"success": False, "message": "New email is required"}
|
||||
|
||||
# Check if email is already in use
|
||||
if User.objects.filter(email=new_email).exclude(id=user.id).exists():
|
||||
return {
|
||||
'success': False,
|
||||
'message': "This email address is already in use"
|
||||
}
|
||||
return {"success": False, "message": "This email address is already in use"}
|
||||
|
||||
# Generate verification token
|
||||
token = get_random_string(64)
|
||||
|
||||
# Create or update email verification record
|
||||
EmailVerification.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={"token": token}
|
||||
)
|
||||
EmailVerification.objects.update_or_create(user=user, defaults={"token": token})
|
||||
|
||||
# Store pending email
|
||||
user.pending_email = new_email
|
||||
@@ -196,18 +179,10 @@ class AccountService:
|
||||
|
||||
logger.info(f"Email change initiated for user {user.id} to {new_email}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': "Verification email sent to your new email address"
|
||||
}
|
||||
return {"success": True, "message": "Verification email sent to your new email address"}
|
||||
|
||||
@staticmethod
|
||||
def _send_email_verification(
|
||||
request: HttpRequest,
|
||||
user: User,
|
||||
new_email: str,
|
||||
token: str
|
||||
) -> None:
|
||||
def _send_email_verification(request: HttpRequest, user: User, new_email: str, token: str) -> None:
|
||||
"""Send email verification for email change."""
|
||||
from django.urls import reverse
|
||||
|
||||
@@ -245,22 +220,14 @@ class AccountService:
|
||||
Dictionary with success status and message
|
||||
"""
|
||||
try:
|
||||
verification = EmailVerification.objects.select_related("user").get(
|
||||
token=token
|
||||
)
|
||||
verification = EmailVerification.objects.select_related("user").get(token=token)
|
||||
except EmailVerification.DoesNotExist:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Invalid or expired verification token"
|
||||
}
|
||||
return {"success": False, "message": "Invalid or expired verification token"}
|
||||
|
||||
user = verification.user
|
||||
|
||||
if not user.pending_email:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "No pending email change found"
|
||||
}
|
||||
return {"success": False, "message": "No pending email change found"}
|
||||
|
||||
# Update email
|
||||
old_email = user.email
|
||||
@@ -273,10 +240,7 @@ class AccountService:
|
||||
|
||||
logger.info(f"Email changed for user {user.id} from {old_email} to {user.email}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': "Email address updated successfully"
|
||||
}
|
||||
return {"success": True, "message": "Email address updated successfully"}
|
||||
|
||||
|
||||
class UserDeletionService:
|
||||
@@ -337,39 +301,17 @@ class UserDeletionService:
|
||||
|
||||
# Count submissions before transfer
|
||||
submission_counts = {
|
||||
"park_reviews": getattr(
|
||||
user, "park_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"ride_reviews": getattr(
|
||||
user, "ride_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"uploaded_park_photos": getattr(
|
||||
user, "uploaded_park_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"uploaded_ride_photos": getattr(
|
||||
user, "uploaded_ride_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"top_lists": getattr(
|
||||
user, "top_lists", user.__class__.objects.none()
|
||||
).count(),
|
||||
"edit_submissions": getattr(
|
||||
user, "edit_submissions", user.__class__.objects.none()
|
||||
).count(),
|
||||
"photo_submissions": getattr(
|
||||
user, "photo_submissions", user.__class__.objects.none()
|
||||
).count(),
|
||||
"moderated_park_reviews": getattr(
|
||||
user, "moderated_park_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"moderated_ride_reviews": getattr(
|
||||
user, "moderated_ride_reviews", user.__class__.objects.none()
|
||||
).count(),
|
||||
"handled_submissions": getattr(
|
||||
user, "handled_submissions", user.__class__.objects.none()
|
||||
).count(),
|
||||
"handled_photos": getattr(
|
||||
user, "handled_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"park_reviews": getattr(user, "park_reviews", user.__class__.objects.none()).count(),
|
||||
"ride_reviews": getattr(user, "ride_reviews", user.__class__.objects.none()).count(),
|
||||
"uploaded_park_photos": getattr(user, "uploaded_park_photos", user.__class__.objects.none()).count(),
|
||||
"uploaded_ride_photos": getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count(),
|
||||
"top_lists": getattr(user, "top_lists", user.__class__.objects.none()).count(),
|
||||
"edit_submissions": getattr(user, "edit_submissions", user.__class__.objects.none()).count(),
|
||||
"photo_submissions": getattr(user, "photo_submissions", user.__class__.objects.none()).count(),
|
||||
"moderated_park_reviews": getattr(user, "moderated_park_reviews", user.__class__.objects.none()).count(),
|
||||
"moderated_ride_reviews": getattr(user, "moderated_ride_reviews", user.__class__.objects.none()).count(),
|
||||
"handled_submissions": getattr(user, "handled_submissions", user.__class__.objects.none()).count(),
|
||||
"handled_photos": getattr(user, "handled_photos", user.__class__.objects.none()).count(),
|
||||
}
|
||||
|
||||
# Transfer all submissions to deleted user
|
||||
@@ -440,11 +382,17 @@ class UserDeletionService:
|
||||
return False, "Cannot delete the system deleted user placeholder"
|
||||
|
||||
if user.is_superuser:
|
||||
return False, "Superuser accounts cannot be deleted for security reasons. Please contact system administrator or remove superuser privileges first."
|
||||
return (
|
||||
False,
|
||||
"Superuser accounts cannot be deleted for security reasons. Please contact system administrator or remove superuser privileges first.",
|
||||
)
|
||||
|
||||
# Check if user has critical admin role
|
||||
if user.role == User.Roles.ADMIN and user.is_staff:
|
||||
return False, "Admin accounts with staff privileges cannot be deleted. Please remove admin privileges first or contact system administrator."
|
||||
return (
|
||||
False,
|
||||
"Admin accounts with staff privileges cannot be deleted. Please remove admin privileges first or contact system administrator.",
|
||||
)
|
||||
|
||||
# Add any other business rules here
|
||||
|
||||
@@ -492,9 +440,7 @@ class UserDeletionService:
|
||||
site = Site.objects.get_current()
|
||||
except Site.DoesNotExist:
|
||||
# Fallback to default site
|
||||
site = Site.objects.get_or_create(
|
||||
id=1, defaults={"domain": "localhost:8000", "name": "localhost:8000"}
|
||||
)[0]
|
||||
site = Site.objects.get_or_create(id=1, defaults={"domain": "localhost:8000", "name": "localhost:8000"})[0]
|
||||
|
||||
# Prepare email context
|
||||
context = {
|
||||
@@ -502,9 +448,7 @@ class UserDeletionService:
|
||||
"verification_code": deletion_request.verification_code,
|
||||
"expires_at": deletion_request.expires_at,
|
||||
"site_name": getattr(settings, "SITE_NAME", "ThrillWiki"),
|
||||
"frontend_domain": getattr(
|
||||
settings, "FRONTEND_DOMAIN", "http://localhost:3000"
|
||||
),
|
||||
"frontend_domain": getattr(settings, "FRONTEND_DOMAIN", "http://localhost:3000"),
|
||||
}
|
||||
|
||||
# Render email content
|
||||
@@ -564,11 +508,9 @@ The ThrillWiki Team
|
||||
ValueError: If verification fails
|
||||
"""
|
||||
try:
|
||||
deletion_request = UserDeletionRequest.objects.get(
|
||||
verification_code=verification_code
|
||||
)
|
||||
deletion_request = UserDeletionRequest.objects.get(verification_code=verification_code)
|
||||
except UserDeletionRequest.DoesNotExist:
|
||||
raise ValueError("Invalid verification code")
|
||||
raise ValueError("Invalid verification code") from None
|
||||
|
||||
# Check if request is still valid
|
||||
if not deletion_request.is_valid():
|
||||
|
||||
@@ -8,4 +8,4 @@ including social provider management, user authentication, and profile services.
|
||||
from .social_provider_service import SocialProviderService
|
||||
from .user_deletion_service import UserDeletionService
|
||||
|
||||
__all__ = ['SocialProviderService', 'UserDeletionService']
|
||||
__all__ = ["SocialProviderService", "UserDeletionService"]
|
||||
|
||||
@@ -139,7 +139,9 @@ class NotificationService:
|
||||
UserNotification: The created notification
|
||||
"""
|
||||
title = f"Your {submission_type} needs attention"
|
||||
message = f"Your {submission_type} submission has been reviewed and needs some changes before it can be approved."
|
||||
message = (
|
||||
f"Your {submission_type} submission has been reviewed and needs some changes before it can be approved."
|
||||
)
|
||||
message += f"\n\nReason: {rejection_reason}"
|
||||
|
||||
if additional_message:
|
||||
@@ -216,9 +218,7 @@ class NotificationService:
|
||||
preferences = NotificationPreference.objects.create(user=user)
|
||||
|
||||
# Send email notification if enabled
|
||||
if preferences.should_send_notification(
|
||||
notification.notification_type, "email"
|
||||
):
|
||||
if preferences.should_send_notification(notification.notification_type, "email"):
|
||||
NotificationService._send_email_notification(notification)
|
||||
|
||||
# Toast notifications are always created (the notification object itself)
|
||||
@@ -261,14 +261,10 @@ class NotificationService:
|
||||
notification.email_sent_at = timezone.now()
|
||||
notification.save(update_fields=["email_sent", "email_sent_at"])
|
||||
|
||||
logger.info(
|
||||
f"Email notification sent to {user.email} for notification {notification.id}"
|
||||
)
|
||||
logger.info(f"Email notification sent to {user.email} for notification {notification.id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send email notification {notification.id}: {str(e)}"
|
||||
)
|
||||
logger.error(f"Failed to send email notification {notification.id}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def get_user_notifications(
|
||||
@@ -298,9 +294,7 @@ class NotificationService:
|
||||
queryset = queryset.filter(notification_type__in=notification_types)
|
||||
|
||||
# Exclude expired notifications
|
||||
queryset = queryset.filter(
|
||||
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now())
|
||||
)
|
||||
queryset = queryset.filter(models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now()))
|
||||
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
@@ -308,9 +302,7 @@ class NotificationService:
|
||||
return list(queryset)
|
||||
|
||||
@staticmethod
|
||||
def mark_notifications_read(
|
||||
user: User, notification_ids: list[int] | None = None
|
||||
) -> int:
|
||||
def mark_notifications_read(user: User, notification_ids: list[int] | None = None) -> int:
|
||||
"""
|
||||
Mark notifications as read for a user.
|
||||
|
||||
@@ -341,9 +333,7 @@ class NotificationService:
|
||||
"""
|
||||
cutoff_date = timezone.now() - timedelta(days=days)
|
||||
|
||||
old_notifications = UserNotification.objects.filter(
|
||||
is_read=True, read_at__lt=cutoff_date
|
||||
)
|
||||
old_notifications = UserNotification.objects.filter(is_read=True, read_at__lt=cutoff_date)
|
||||
|
||||
count = old_notifications.count()
|
||||
old_notifications.delete()
|
||||
|
||||
@@ -40,23 +40,20 @@ class SocialProviderService:
|
||||
"""
|
||||
try:
|
||||
# Count remaining social accounts after disconnection
|
||||
remaining_social_accounts = user.socialaccount_set.exclude(
|
||||
provider=provider
|
||||
).count()
|
||||
remaining_social_accounts = user.socialaccount_set.exclude(provider=provider).count()
|
||||
|
||||
# Check if user has email/password auth
|
||||
has_password_auth = (
|
||||
user.email and
|
||||
user.has_usable_password() and
|
||||
bool(user.password) # Not empty/unusable
|
||||
)
|
||||
has_password_auth = user.email and user.has_usable_password() and bool(user.password) # Not empty/unusable
|
||||
|
||||
# Allow disconnection only if alternative auth exists
|
||||
can_disconnect = remaining_social_accounts > 0 or has_password_auth
|
||||
|
||||
if not can_disconnect:
|
||||
if remaining_social_accounts == 0 and not has_password_auth:
|
||||
return False, "Cannot disconnect your only authentication method. Please set up a password or connect another social provider first."
|
||||
return (
|
||||
False,
|
||||
"Cannot disconnect your only authentication method. Please set up a password or connect another social provider first.",
|
||||
)
|
||||
elif not has_password_auth:
|
||||
return False, "Please set up email/password authentication before disconnecting this provider."
|
||||
else:
|
||||
@@ -65,8 +62,7 @@ class SocialProviderService:
|
||||
return True, "Provider can be safely disconnected."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error checking disconnect permission for user {user.id}, provider {provider}: {e}")
|
||||
logger.error(f"Error checking disconnect permission for user {user.id}, provider {provider}: {e}")
|
||||
return False, "Unable to verify disconnection safety. Please try again."
|
||||
|
||||
@staticmethod
|
||||
@@ -84,18 +80,16 @@ class SocialProviderService:
|
||||
connected_providers = []
|
||||
|
||||
for social_account in user.socialaccount_set.all():
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(
|
||||
user, social_account.provider
|
||||
)
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(user, social_account.provider)
|
||||
|
||||
provider_info = {
|
||||
'provider': social_account.provider,
|
||||
'provider_name': social_account.get_provider().name,
|
||||
'uid': social_account.uid,
|
||||
'date_joined': social_account.date_joined,
|
||||
'can_disconnect': can_disconnect,
|
||||
'disconnect_reason': reason if not can_disconnect else None,
|
||||
'extra_data': social_account.extra_data
|
||||
"provider": social_account.provider,
|
||||
"provider_name": social_account.get_provider().name,
|
||||
"uid": social_account.uid,
|
||||
"date_joined": social_account.date_joined,
|
||||
"can_disconnect": can_disconnect,
|
||||
"disconnect_reason": reason if not can_disconnect else None,
|
||||
"extra_data": social_account.extra_data,
|
||||
}
|
||||
|
||||
connected_providers.append(provider_info)
|
||||
@@ -122,28 +116,25 @@ class SocialProviderService:
|
||||
available_providers = []
|
||||
|
||||
# Get all social apps configured for this site
|
||||
social_apps = SocialApp.objects.filter(sites=site).order_by('provider')
|
||||
social_apps = SocialApp.objects.filter(sites=site).order_by("provider")
|
||||
|
||||
for social_app in social_apps:
|
||||
try:
|
||||
provider = registry.by_id(social_app.provider)
|
||||
|
||||
provider_info = {
|
||||
'id': social_app.provider,
|
||||
'name': provider.name,
|
||||
'auth_url': request.build_absolute_uri(
|
||||
f'/accounts/{social_app.provider}/login/'
|
||||
"id": social_app.provider,
|
||||
"name": provider.name,
|
||||
"auth_url": request.build_absolute_uri(f"/accounts/{social_app.provider}/login/"),
|
||||
"connect_url": request.build_absolute_uri(
|
||||
f"/api/v1/auth/social/connect/{social_app.provider}/"
|
||||
),
|
||||
'connect_url': request.build_absolute_uri(
|
||||
f'/api/v1/auth/social/connect/{social_app.provider}/'
|
||||
)
|
||||
}
|
||||
|
||||
available_providers.append(provider_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error processing provider {social_app.provider}: {e}")
|
||||
logger.warning(f"Error processing provider {social_app.provider}: {e}")
|
||||
continue
|
||||
|
||||
return available_providers
|
||||
@@ -166,8 +157,7 @@ class SocialProviderService:
|
||||
"""
|
||||
try:
|
||||
# First check if disconnection is allowed
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(
|
||||
user, provider)
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(user, provider)
|
||||
|
||||
if not can_disconnect:
|
||||
return False, reason
|
||||
@@ -182,8 +172,7 @@ class SocialProviderService:
|
||||
deleted_count = social_accounts.count()
|
||||
social_accounts.delete()
|
||||
|
||||
logger.info(
|
||||
f"User {user.id} disconnected {deleted_count} {provider} account(s)")
|
||||
logger.info(f"User {user.id} disconnected {deleted_count} {provider} account(s)")
|
||||
|
||||
return True, f"{provider.title()} account disconnected successfully."
|
||||
|
||||
@@ -205,31 +194,24 @@ class SocialProviderService:
|
||||
try:
|
||||
connected_providers = SocialProviderService.get_connected_providers(user)
|
||||
|
||||
has_password_auth = (
|
||||
user.email and
|
||||
user.has_usable_password() and
|
||||
bool(user.password)
|
||||
)
|
||||
has_password_auth = user.email and user.has_usable_password() and bool(user.password)
|
||||
|
||||
auth_methods_count = len(connected_providers) + \
|
||||
(1 if has_password_auth else 0)
|
||||
auth_methods_count = len(connected_providers) + (1 if has_password_auth else 0)
|
||||
|
||||
return {
|
||||
'user_id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'has_password_auth': has_password_auth,
|
||||
'connected_providers': connected_providers,
|
||||
'total_auth_methods': auth_methods_count,
|
||||
'can_disconnect_any': auth_methods_count > 1,
|
||||
'requires_password_setup': not has_password_auth and len(connected_providers) == 1
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"has_password_auth": has_password_auth,
|
||||
"connected_providers": connected_providers,
|
||||
"total_auth_methods": auth_methods_count,
|
||||
"can_disconnect_any": auth_methods_count > 1,
|
||||
"requires_password_setup": not has_password_auth and len(connected_providers) == 1,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting auth status for user {user.id}: {e}")
|
||||
return {
|
||||
'error': 'Unable to retrieve authentication status'
|
||||
}
|
||||
return {"error": "Unable to retrieve authentication status"}
|
||||
|
||||
@staticmethod
|
||||
def validate_provider_exists(provider: str) -> tuple[bool, str]:
|
||||
|
||||
@@ -59,7 +59,7 @@ class UserDeletionService:
|
||||
return False, "Cannot delete staff accounts"
|
||||
|
||||
# Check for system users (if you have any special system accounts)
|
||||
if hasattr(user, 'role') and user.role in ['ADMIN', 'MODERATOR']:
|
||||
if hasattr(user, "role") and user.role in ["ADMIN", "MODERATOR"]:
|
||||
return False, "Cannot delete admin or moderator accounts"
|
||||
|
||||
return True, None
|
||||
@@ -84,8 +84,7 @@ class UserDeletionService:
|
||||
raise ValueError(reason)
|
||||
|
||||
# Generate verification code
|
||||
verification_code = ''.join(secrets.choice(
|
||||
string.ascii_uppercase + string.digits) for _ in range(8))
|
||||
verification_code = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
|
||||
|
||||
# Set expiration (24 hours from now)
|
||||
expires_at = timezone.now() + timezone.timedelta(hours=24)
|
||||
@@ -97,8 +96,7 @@ class UserDeletionService:
|
||||
UserDeletionService._deletion_requests[verification_code] = deletion_request
|
||||
|
||||
# Send verification email
|
||||
UserDeletionService._send_deletion_verification_email(
|
||||
user, verification_code, expires_at)
|
||||
UserDeletionService._send_deletion_verification_email(user, verification_code, expires_at)
|
||||
|
||||
return deletion_request
|
||||
|
||||
@@ -136,10 +134,10 @@ class UserDeletionService:
|
||||
del UserDeletionService._deletion_requests[verification_code]
|
||||
|
||||
# Add verification info to result
|
||||
result['deletion_request'] = {
|
||||
'verification_code': verification_code,
|
||||
'created_at': deletion_request.created_at,
|
||||
'verified_at': timezone.now(),
|
||||
result["deletion_request"] = {
|
||||
"verification_code": verification_code,
|
||||
"created_at": deletion_request.created_at,
|
||||
"verified_at": timezone.now(),
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -180,13 +178,13 @@ class UserDeletionService:
|
||||
"""
|
||||
# Get or create the "deleted_user" placeholder
|
||||
deleted_user_placeholder, created = User.objects.get_or_create(
|
||||
username='deleted_user',
|
||||
username="deleted_user",
|
||||
defaults={
|
||||
'email': 'deleted@thrillwiki.com',
|
||||
'first_name': 'Deleted',
|
||||
'last_name': 'User',
|
||||
'is_active': False,
|
||||
}
|
||||
"email": "deleted@thrillwiki.com",
|
||||
"first_name": "Deleted",
|
||||
"last_name": "User",
|
||||
"is_active": False,
|
||||
},
|
||||
)
|
||||
|
||||
# Count submissions before transfer
|
||||
@@ -197,22 +195,22 @@ class UserDeletionService:
|
||||
|
||||
# Store user info before deletion
|
||||
deleted_user_info = {
|
||||
'username': user.username,
|
||||
'user_id': getattr(user, 'user_id', user.id),
|
||||
'email': user.email,
|
||||
'date_joined': user.date_joined,
|
||||
"username": user.username,
|
||||
"user_id": getattr(user, "user_id", user.id),
|
||||
"email": user.email,
|
||||
"date_joined": user.date_joined,
|
||||
}
|
||||
|
||||
# Delete the user account
|
||||
user.delete()
|
||||
|
||||
return {
|
||||
'deleted_user': deleted_user_info,
|
||||
'preserved_submissions': submission_counts,
|
||||
'transferred_to': {
|
||||
'username': deleted_user_placeholder.username,
|
||||
'user_id': getattr(deleted_user_placeholder, 'user_id', deleted_user_placeholder.id),
|
||||
}
|
||||
"deleted_user": deleted_user_info,
|
||||
"preserved_submissions": submission_counts,
|
||||
"transferred_to": {
|
||||
"username": deleted_user_placeholder.username,
|
||||
"user_id": getattr(deleted_user_placeholder, "user_id", deleted_user_placeholder.id),
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -222,20 +220,13 @@ class UserDeletionService:
|
||||
|
||||
# Count different types of submissions
|
||||
# Note: These are placeholder counts - adjust based on your actual models
|
||||
counts['park_reviews'] = getattr(
|
||||
user, 'park_reviews', user.__class__.objects.none()).count()
|
||||
counts['ride_reviews'] = getattr(
|
||||
user, 'ride_reviews', user.__class__.objects.none()).count()
|
||||
counts['uploaded_park_photos'] = getattr(
|
||||
user, 'uploaded_park_photos', user.__class__.objects.none()).count()
|
||||
counts['uploaded_ride_photos'] = getattr(
|
||||
user, 'uploaded_ride_photos', user.__class__.objects.none()).count()
|
||||
counts['top_lists'] = getattr(
|
||||
user, 'top_lists', user.__class__.objects.none()).count()
|
||||
counts['edit_submissions'] = getattr(
|
||||
user, 'edit_submissions', user.__class__.objects.none()).count()
|
||||
counts['photo_submissions'] = getattr(
|
||||
user, 'photo_submissions', user.__class__.objects.none()).count()
|
||||
counts["park_reviews"] = getattr(user, "park_reviews", user.__class__.objects.none()).count()
|
||||
counts["ride_reviews"] = getattr(user, "ride_reviews", user.__class__.objects.none()).count()
|
||||
counts["uploaded_park_photos"] = getattr(user, "uploaded_park_photos", user.__class__.objects.none()).count()
|
||||
counts["uploaded_ride_photos"] = getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count()
|
||||
counts["top_lists"] = getattr(user, "top_lists", user.__class__.objects.none()).count()
|
||||
counts["edit_submissions"] = getattr(user, "edit_submissions", user.__class__.objects.none()).count()
|
||||
counts["photo_submissions"] = getattr(user, "photo_submissions", user.__class__.objects.none()).count()
|
||||
|
||||
return counts
|
||||
|
||||
@@ -247,30 +238,30 @@ class UserDeletionService:
|
||||
# Note: Adjust these based on your actual model relationships
|
||||
|
||||
# Park reviews
|
||||
if hasattr(user, 'park_reviews'):
|
||||
if hasattr(user, "park_reviews"):
|
||||
user.park_reviews.all().update(user=placeholder_user)
|
||||
|
||||
# Ride reviews
|
||||
if hasattr(user, 'ride_reviews'):
|
||||
if hasattr(user, "ride_reviews"):
|
||||
user.ride_reviews.all().update(user=placeholder_user)
|
||||
|
||||
# Uploaded photos
|
||||
if hasattr(user, 'uploaded_park_photos'):
|
||||
if hasattr(user, "uploaded_park_photos"):
|
||||
user.uploaded_park_photos.all().update(user=placeholder_user)
|
||||
|
||||
if hasattr(user, 'uploaded_ride_photos'):
|
||||
if hasattr(user, "uploaded_ride_photos"):
|
||||
user.uploaded_ride_photos.all().update(user=placeholder_user)
|
||||
|
||||
# Top lists
|
||||
if hasattr(user, 'top_lists'):
|
||||
if hasattr(user, "top_lists"):
|
||||
user.top_lists.all().update(user=placeholder_user)
|
||||
|
||||
# Edit submissions
|
||||
if hasattr(user, 'edit_submissions'):
|
||||
if hasattr(user, "edit_submissions"):
|
||||
user.edit_submissions.all().update(user=placeholder_user)
|
||||
|
||||
# Photo submissions
|
||||
if hasattr(user, 'photo_submissions'):
|
||||
if hasattr(user, "photo_submissions"):
|
||||
user.photo_submissions.all().update(user=placeholder_user)
|
||||
|
||||
@staticmethod
|
||||
@@ -278,18 +269,16 @@ class UserDeletionService:
|
||||
"""Send verification email for account deletion."""
|
||||
try:
|
||||
context = {
|
||||
'user': user,
|
||||
'verification_code': verification_code,
|
||||
'expires_at': expires_at,
|
||||
'site_name': 'ThrillWiki',
|
||||
'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'),
|
||||
"user": user,
|
||||
"verification_code": verification_code,
|
||||
"expires_at": expires_at,
|
||||
"site_name": "ThrillWiki",
|
||||
"site_url": getattr(settings, "SITE_URL", "https://thrillwiki.com"),
|
||||
}
|
||||
|
||||
subject = 'ThrillWiki: Confirm Account Deletion'
|
||||
html_message = render_to_string(
|
||||
'emails/account_deletion_verification.html', context)
|
||||
plain_message = render_to_string(
|
||||
'emails/account_deletion_verification.txt', context)
|
||||
subject = "ThrillWiki: Confirm Account Deletion"
|
||||
html_message = render_to_string("emails/account_deletion_verification.html", context)
|
||||
plain_message = render_to_string("emails/account_deletion_verification.txt", context)
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
@@ -303,6 +292,5 @@ class UserDeletionService:
|
||||
logger.info(f"Deletion verification email sent to {user.email}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send deletion verification email to {user.email}: {str(e)}")
|
||||
logger.error(f"Failed to send deletion verification email to {user.email}: {str(e)}")
|
||||
raise
|
||||
|
||||
@@ -108,7 +108,7 @@ def sync_user_role_with_groups(sender, instance, **kwargs):
|
||||
User.Roles.MODERATOR,
|
||||
]:
|
||||
instance.is_staff = True
|
||||
elif old_instance.role in [
|
||||
elif old_instance.role in [ # noqa: SIM102
|
||||
User.Roles.ADMIN,
|
||||
User.Roles.MODERATOR,
|
||||
]:
|
||||
@@ -119,9 +119,7 @@ def sync_user_role_with_groups(sender, instance, **kwargs):
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error syncing role with groups for user {instance.username}: {str(e)}"
|
||||
)
|
||||
print(f"Error syncing role with groups for user {instance.username}: {str(e)}")
|
||||
|
||||
|
||||
def create_default_groups():
|
||||
@@ -200,19 +198,19 @@ def log_successful_login(sender, user, request, **kwargs):
|
||||
"""
|
||||
try:
|
||||
# Get IP address
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
ip_address = x_forwarded_for.split(',')[0].strip() if x_forwarded_for else request.META.get('REMOTE_ADDR')
|
||||
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
ip_address = x_forwarded_for.split(",")[0].strip() if x_forwarded_for else request.META.get("REMOTE_ADDR")
|
||||
|
||||
# Get user agent
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')[:500]
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")[:500]
|
||||
|
||||
# Determine login method from session or request
|
||||
login_method = 'PASSWORD'
|
||||
if hasattr(request, 'session'):
|
||||
sociallogin = getattr(request, '_sociallogin', None)
|
||||
login_method = "PASSWORD"
|
||||
if hasattr(request, "session"):
|
||||
sociallogin = getattr(request, "_sociallogin", None)
|
||||
if sociallogin:
|
||||
provider = sociallogin.account.provider.upper()
|
||||
if provider in ['GOOGLE', 'DISCORD']:
|
||||
if provider in ["GOOGLE", "DISCORD"]:
|
||||
login_method = provider
|
||||
|
||||
# Create login history entry
|
||||
|
||||
@@ -113,16 +113,10 @@ class SignalsTestCase(TestCase):
|
||||
|
||||
moderator_group = Group.objects.get(name=User.Roles.MODERATOR)
|
||||
self.assertIsNotNone(moderator_group)
|
||||
self.assertTrue(
|
||||
moderator_group.permissions.filter(codename="change_review").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
moderator_group.permissions.filter(codename="change_user").exists()
|
||||
)
|
||||
self.assertTrue(moderator_group.permissions.filter(codename="change_review").exists())
|
||||
self.assertFalse(moderator_group.permissions.filter(codename="change_user").exists())
|
||||
|
||||
admin_group = Group.objects.get(name=User.Roles.ADMIN)
|
||||
self.assertIsNotNone(admin_group)
|
||||
self.assertTrue(
|
||||
admin_group.permissions.filter(codename="change_review").exists()
|
||||
)
|
||||
self.assertTrue(admin_group.permissions.filter(codename="change_review").exists())
|
||||
self.assertTrue(admin_group.permissions.filter(codename="change_user").exists())
|
||||
|
||||
@@ -150,6 +150,3 @@ class TestPasswordResetAdmin(TestCase):
|
||||
request.user = UserModel(is_superuser=True)
|
||||
actions = self.admin.get_actions(request)
|
||||
assert "cleanup_old_tokens" in actions
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,16 +85,16 @@ class UserIndexTests(TestCase):
|
||||
|
||||
def test_is_banned_field_is_indexed(self):
|
||||
"""Verify is_banned field has db_index=True."""
|
||||
field = User._meta.get_field('is_banned')
|
||||
field = User._meta.get_field("is_banned")
|
||||
self.assertTrue(field.db_index)
|
||||
|
||||
def test_role_field_is_indexed(self):
|
||||
"""Verify role field has db_index=True."""
|
||||
field = User._meta.get_field('role')
|
||||
field = User._meta.get_field("role")
|
||||
self.assertTrue(field.db_index)
|
||||
|
||||
def test_composite_index_exists(self):
|
||||
"""Verify composite index on (is_banned, role) exists."""
|
||||
indexes = User._meta.indexes
|
||||
index_names = [idx.name for idx in indexes]
|
||||
self.assertIn('accounts_user_banned_role_idx', index_names)
|
||||
self.assertIn("accounts_user_banned_role_idx", index_names)
|
||||
|
||||
@@ -15,9 +15,7 @@ class UserDeletionServiceTest(TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
# Create test users
|
||||
self.user = User.objects.create_user(
|
||||
username="testuser", email="test@example.com", password="testpass123"
|
||||
)
|
||||
self.user = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||
|
||||
self.admin_user = User.objects.create_user(
|
||||
username="admin",
|
||||
@@ -27,13 +25,9 @@ class UserDeletionServiceTest(TestCase):
|
||||
)
|
||||
|
||||
# Create user profiles
|
||||
UserProfile.objects.create(
|
||||
user=self.user, display_name="Test User", bio="Test bio"
|
||||
)
|
||||
UserProfile.objects.create(user=self.user, display_name="Test User", bio="Test bio")
|
||||
|
||||
UserProfile.objects.create(
|
||||
user=self.admin_user, display_name="Admin User", bio="Admin bio"
|
||||
)
|
||||
UserProfile.objects.create(user=self.admin_user, display_name="Admin User", bio="Admin bio")
|
||||
|
||||
def test_get_or_create_deleted_user(self):
|
||||
"""Test that deleted user placeholder is created correctly."""
|
||||
@@ -108,9 +102,7 @@ class UserDeletionServiceTest(TestCase):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
UserDeletionService.delete_user_preserve_submissions(deleted_user)
|
||||
|
||||
self.assertIn(
|
||||
"Cannot delete the system deleted user placeholder", str(context.exception)
|
||||
)
|
||||
self.assertIn("Cannot delete the system deleted user placeholder", str(context.exception))
|
||||
|
||||
def test_delete_user_with_submissions_transfers_correctly(self):
|
||||
"""Test that user submissions are transferred to deleted user placeholder."""
|
||||
@@ -141,7 +133,7 @@ class UserDeletionServiceTest(TestCase):
|
||||
original_user_count = User.objects.count()
|
||||
|
||||
# Mock a failure during the deletion process
|
||||
with self.assertRaises(Exception), transaction.atomic():
|
||||
with self.assertRaises(Exception), transaction.atomic(): # noqa: B017
|
||||
# Start the deletion process
|
||||
UserDeletionService.get_or_create_deleted_user()
|
||||
|
||||
|
||||
@@ -61,11 +61,7 @@ class CustomLoginView(TurnstileMixin, LoginView):
|
||||
context={"user_id": user.id, "username": user.username},
|
||||
request=self.request,
|
||||
)
|
||||
return (
|
||||
HttpResponseClientRefresh()
|
||||
if getattr(self.request, "htmx", False)
|
||||
else response
|
||||
)
|
||||
return HttpResponseClientRefresh() if getattr(self.request, "htmx", False) else response
|
||||
|
||||
def form_invalid(self, form):
|
||||
log_security_event(
|
||||
@@ -116,11 +112,7 @@ class CustomSignupView(TurnstileMixin, SignupView):
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
return (
|
||||
HttpResponseClientRefresh()
|
||||
if getattr(self.request, "htmx", False)
|
||||
else response
|
||||
)
|
||||
return HttpResponseClientRefresh() if getattr(self.request, "htmx", False) else response
|
||||
|
||||
def form_invalid(self, form):
|
||||
if getattr(self.request, "htmx", False):
|
||||
@@ -260,9 +252,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
and bool(re.search(r"[0-9]", password))
|
||||
)
|
||||
|
||||
def _send_password_change_confirmation(
|
||||
self, request: HttpRequest, user: User
|
||||
) -> None:
|
||||
def _send_password_change_confirmation(self, request: HttpRequest, user: User) -> None:
|
||||
"""Send password change confirmation email."""
|
||||
site = get_current_site(request)
|
||||
context = {
|
||||
@@ -270,9 +260,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
"site_name": site.name,
|
||||
}
|
||||
|
||||
email_html = render_to_string(
|
||||
"accounts/email/password_change_confirmation.html", context
|
||||
)
|
||||
email_html = render_to_string("accounts/email/password_change_confirmation.html", context)
|
||||
|
||||
EmailService.send_email(
|
||||
to=user.email,
|
||||
@@ -282,9 +270,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
html=email_html,
|
||||
)
|
||||
|
||||
def _handle_password_change(
|
||||
self, request: HttpRequest
|
||||
) -> HttpResponseRedirect | None:
|
||||
def _handle_password_change(self, request: HttpRequest) -> HttpResponseRedirect | None:
|
||||
user = cast(User, request.user)
|
||||
old_password = request.POST.get("old_password", "")
|
||||
new_password = request.POST.get("new_password", "")
|
||||
@@ -327,9 +313,7 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
def _handle_email_change(self, request: HttpRequest) -> None:
|
||||
if new_email := request.POST.get("new_email"):
|
||||
self._send_email_verification(request, new_email)
|
||||
messages.success(
|
||||
request, "Verification email sent to your new email address"
|
||||
)
|
||||
messages.success(request, "Verification email sent to your new email address")
|
||||
else:
|
||||
messages.error(request, "New email is required")
|
||||
|
||||
@@ -385,9 +369,7 @@ def create_password_reset_token(user: User) -> str:
|
||||
return token
|
||||
|
||||
|
||||
def send_password_reset_email(
|
||||
user: User, site: Site | RequestSite, token: str
|
||||
) -> None:
|
||||
def send_password_reset_email(user: User, site: Site | RequestSite, token: str) -> None:
|
||||
reset_url = reverse("password_reset_confirm", kwargs={"token": token})
|
||||
context = {
|
||||
"user": user,
|
||||
@@ -457,16 +439,12 @@ def handle_password_reset(
|
||||
messages.success(request, "Password reset successfully")
|
||||
|
||||
|
||||
def send_password_reset_confirmation(
|
||||
user: User, site: Site | RequestSite
|
||||
) -> None:
|
||||
def send_password_reset_confirmation(user: User, site: Site | RequestSite) -> None:
|
||||
context = {
|
||||
"user": user,
|
||||
"site_name": site.name,
|
||||
}
|
||||
email_html = render_to_string(
|
||||
"accounts/email/password_reset_complete.html", context
|
||||
)
|
||||
email_html = render_to_string("accounts/email/password_reset_complete.html", context)
|
||||
|
||||
EmailService.send_email(
|
||||
to=user.email,
|
||||
@@ -479,9 +457,7 @@ def send_password_reset_confirmation(
|
||||
|
||||
def reset_password(request: HttpRequest, token: str) -> HttpResponse:
|
||||
try:
|
||||
reset = PasswordReset.objects.select_related("user").get(
|
||||
token=token, expires_at__gt=timezone.now(), used=False
|
||||
)
|
||||
reset = PasswordReset.objects.select_related("user").get(token=token, expires_at__gt=timezone.now(), used=False)
|
||||
|
||||
if request.method == "POST":
|
||||
if new_password := request.POST.get("new_password"):
|
||||
|
||||
Reference in New Issue
Block a user