feat: Implement Entity Suggestion Manager and Modal components

- Added EntitySuggestionManager.vue to manage entity suggestions and authentication.
- Created EntitySuggestionModal.vue for displaying suggestions and adding new entities.
- Integrated AuthManager for user authentication within the suggestion modal.
- Enhanced signal handling in start-servers.sh for graceful shutdown of servers.
- Improved server startup script to ensure proper cleanup and responsiveness to termination signals.
- Added documentation for signal handling fixes and usage instructions.
This commit is contained in:
pacnpal
2025-08-25 10:46:54 -04:00
parent 937eee19e4
commit dcf890a55c
61 changed files with 10328 additions and 740 deletions

View File

@@ -12,6 +12,8 @@ from .forms import RideForm, RideSearchForm
from apps.parks.models import Park
from apps.moderation.mixins import EditSubmissionMixin, HistoryMixin
from apps.moderation.models import EditSubmission
from .models.rankings import RideRanking, RankingSnapshot
from .services.ranking_service import RideRankingService
class ParkContextRequired:
@@ -452,3 +454,166 @@ class RideSearchView(ListView):
context = super().get_context_data(**kwargs)
context["search_form"] = RideSearchForm(self.request.GET)
return context
class RideRankingsView(ListView):
"""View for displaying ride rankings using the Internet Roller Coaster Poll algorithm."""
model = RideRanking
template_name = "rides/rankings.html"
context_object_name = "rankings"
paginate_by = 50
def get_queryset(self):
"""Get rankings with optimized queries."""
queryset = RideRanking.objects.select_related(
"ride", "ride__park", "ride__manufacturer", "ride__ride_model"
).order_by("rank")
# Filter by category if specified
category = self.request.GET.get("category")
if category and category != "all":
queryset = queryset.filter(ride__category=category)
# Filter by minimum mutual riders
min_riders = self.request.GET.get("min_riders")
if min_riders:
try:
min_riders = int(min_riders)
queryset = queryset.filter(mutual_riders_count__gte=min_riders)
except ValueError:
pass
return queryset
def get_template_names(self):
"""Return appropriate template based on request type."""
if self.request.htmx:
return ["rides/partials/rankings_table.html"]
return [self.template_name]
def get_context_data(self, **kwargs):
"""Add context for rankings view."""
context = super().get_context_data(**kwargs)
context["category_choices"] = Categories
context["selected_category"] = self.request.GET.get("category", "all")
context["min_riders"] = self.request.GET.get("min_riders", "")
# Add statistics
if self.object_list:
context["total_ranked"] = RideRanking.objects.count()
context["last_updated"] = (
self.object_list[0].last_calculated if self.object_list else None
)
return context
class RideRankingDetailView(DetailView):
"""View for displaying detailed ranking information for a specific ride."""
model = Ride
template_name = "rides/ranking_detail.html"
slug_url_kwarg = "ride_slug"
def get_queryset(self):
"""Get ride with ranking data."""
return Ride.objects.select_related(
"park", "manufacturer", "ranking"
).prefetch_related("comparisons_as_a", "comparisons_as_b", "ranking_history")
def get_context_data(self, **kwargs):
"""Add ranking details to context."""
context = super().get_context_data(**kwargs)
# Get ranking details from service
service = RideRankingService()
ranking_details = service.get_ride_ranking_details(self.object)
if ranking_details:
context.update(ranking_details)
# Get recent movement
recent_snapshots = RankingSnapshot.objects.filter(
ride=self.object
).order_by("-snapshot_date")[:7]
if len(recent_snapshots) >= 2:
context["rank_change"] = (
recent_snapshots[0].rank - recent_snapshots[1].rank
)
context["previous_rank"] = recent_snapshots[1].rank
else:
context["not_ranked"] = True
return context
def ranking_history_chart(request: HttpRequest, ride_slug: str) -> HttpResponse:
"""HTMX endpoint for ranking history chart data."""
ride = get_object_or_404(Ride, slug=ride_slug)
# Get last 30 days of ranking history
history = RankingSnapshot.objects.filter(ride=ride).order_by("-snapshot_date")[:30]
# Prepare data for chart
chart_data = [
{
"date": snapshot.snapshot_date.isoformat(),
"rank": snapshot.rank,
"win_pct": float(snapshot.winning_percentage) * 100,
}
for snapshot in reversed(history)
]
return render(
request,
"rides/partials/ranking_chart.html",
{"chart_data": chart_data, "ride": ride},
)
def ranking_comparisons(request: HttpRequest, ride_slug: str) -> HttpResponse:
"""HTMX endpoint for ride head-to-head comparisons."""
ride = get_object_or_404(Ride, slug=ride_slug)
# Get head-to-head comparisons
from django.db.models import Q
from .models.rankings import RidePairComparison
comparisons = (
RidePairComparison.objects.filter(Q(ride_a=ride) | Q(ride_b=ride))
.select_related("ride_a", "ride_b", "ride_a__park", "ride_b__park")
.order_by("-mutual_riders_count")[:20]
)
# Format comparisons for display
comparison_data = []
for comp in comparisons:
if comp.ride_a == ride:
opponent = comp.ride_b
wins = comp.ride_a_wins
losses = comp.ride_b_wins
else:
opponent = comp.ride_a
wins = comp.ride_b_wins
losses = comp.ride_a_wins
result = "win" if wins > losses else "loss" if losses > wins else "tie"
comparison_data.append(
{
"opponent": opponent,
"wins": wins,
"losses": losses,
"ties": comp.ties,
"result": result,
"mutual_riders": comp.mutual_riders_count,
}
)
return render(
request,
"rides/partials/ranking_comparisons.html",
{"comparisons": comparison_data, "ride": ride},
)