diff --git a/templates/base.html b/templates/base.html index 41314e3..df363e9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -50,16 +50,16 @@ max-width: 120px; max-height: 120px; } - + .hover-effect { transition: all 0.3s ease; } - + .hover-effect:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important; } - + .card-header h3 { font-size: 1.5rem; } diff --git a/templates/twitch/game_list.html b/templates/twitch/game_list.html index 8d24e30..0f70dc5 100644 --- a/templates/twitch/game_list.html +++ b/templates/twitch/game_list.html @@ -11,50 +11,50 @@ {% if games_by_org %} - {% for organization, games in games_by_org.items %} -
-
-

- {{ organization.name }} -

-
-
-
- {% for item in games %} -
-
-
-
- - {{ item.game.display_name }} - -
-
- - {{ item.campaign_count }} Campaigns - - {% if item.active_count > 0 %} - - {{ item.active_count }} Active - - {% endif %} -
-
- +{% for organization, games in games_by_org.items %} +
+
+

+ {{ organization.name }} +

+
+
+
+ {% for item in games %} +
+
+
+
+ + {{ item.game.display_name }} + +
+
+ + {{ item.campaign_count }} Campaigns + + {% if item.active_count > 0 %} + + {{ item.active_count }} Active + + {% endif %}
- {% endfor %} +
+ {% endfor %}
- {% endfor %} -{% else %} -
- No games found.
+
+{% endfor %} +{% else %} +
+ No games found. +
{% endif %} {% endblock %} \ No newline at end of file diff --git a/twitch/views.py b/twitch/views.py index 56566d9..591ba92 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -53,7 +53,7 @@ class DropCampaignListView(ListView): context = super().get_context_data(**kwargs) # Load all games in a single query instead of multiple queries per game - context["games"] = Game.objects.all().order_by('display_name') + context["games"] = Game.objects.all().order_by("display_name") # Add status options for filtering context["status_options"] = [status[0] for status in DropCampaign.STATUS_CHOICES] @@ -74,25 +74,25 @@ class DropCampaignDetailView(DetailView): model = DropCampaign template_name = "twitch/campaign_detail.html" context_object_name = "campaign" - + def get_object(self, queryset=None): """Get the campaign object with related data prefetched. - + Args: queryset: Optional queryset to use. - + Returns: DropCampaign: The campaign object with prefetched relations. """ # Prefetch all needed related objects in a single query if queryset is None: queryset = self.get_queryset() - + queryset = queryset.select_related("game", "owner") - + # We don't need to prefetch time_based_drops here since we're fetching them separately in get_context_data # with proper ordering and prefetching of benefits - + return super().get_object(queryset=queryset) def get_context_data(self, **kwargs) -> dict[str, Any]: @@ -151,86 +151,70 @@ class GameListView(ListView): def get_context_data(self, **kwargs) -> dict[str, Any]: """Add additional context data with games grouped by organization. - + Args: **kwargs: Additional keyword arguments. - + Returns: dict: Context data with games grouped by organization. """ context = super().get_context_data(**kwargs) - + # Create a dictionary to hold games by organization games_by_org = {} now = timezone.now() - + # Step 1: Get all organizations with games in a single query # We'll prefetch the games related to each organization through drop_campaigns - organizations_with_games = Organization.objects.filter( - drop_campaigns__isnull=False - ).distinct().order_by('name') - + organizations_with_games = Organization.objects.filter(drop_campaigns__isnull=False).distinct().order_by("name") + # Step 2: Get all game-organization relationships in a single efficient query # This query gets all games with their campaign counts and organization info - game_org_relations = DropCampaign.objects.values( - 'game_id', 'owner_id', 'owner__name' - ).annotate( - campaign_count=Count('id', distinct=True), - active_count=Count( - 'id', - filter=Q( - start_at__lte=now, - end_at__gte=now, - status="ACTIVE" - ), - distinct=True - ) + game_org_relations = DropCampaign.objects.values("game_id", "owner_id", "owner__name").annotate( + campaign_count=Count("id", distinct=True), + active_count=Count("id", filter=Q(start_at__lte=now, end_at__gte=now, status="ACTIVE"), distinct=True), ) - + # Step 3: Get all games in a single query with their display names - all_games = { - game.id: game for game in Game.objects.all() - } - + all_games = {game.id: game for game in Game.objects.all()} + # Step 4: Create a mapping of organization_id to organization_name - org_names = { - org.id: org.name for org in organizations_with_games - } - + org_names = {org.id: org.name for org in organizations_with_games} + # Step 5: Group games by organization game_org_map = {} for relation in game_org_relations: - org_id = relation['owner_id'] - game_id = relation['game_id'] - + org_id = relation["owner_id"] + game_id = relation["game_id"] + if org_id not in game_org_map: game_org_map[org_id] = {} - + if game_id not in game_org_map[org_id]: game = all_games.get(game_id) if game: game_org_map[org_id][game_id] = { - 'game': game, - 'campaign_count': relation['campaign_count'], - 'active_count': relation['active_count'] + "game": game, + "campaign_count": relation["campaign_count"], + "active_count": relation["active_count"], } - + # Step 6: Convert the nested dictionary to the format expected by the template for org_id, games in game_org_map.items(): if org_id in org_names: # Create an Organization-like object with id and name - org_obj = type('Organization', (), {'id': org_id, 'name': org_names[org_id]}) + org_obj = type("Organization", (), {"id": org_id, "name": org_names[org_id]}) games_by_org[org_obj] = list(games.values()) - + # Create the flattened games_with_counts for backward compatibility games_with_counts = [] for org_games in games_by_org.values(): games_with_counts.extend(org_games) - + # Add to context context["games_with_counts"] = games_with_counts # Keep the original list for backward compatibility context["games_by_org"] = games_by_org # Add the new organized structure - + return context @@ -255,25 +239,19 @@ class GameDetailView(DetailView): # Get all campaigns for this game in a single query with prefetching now = timezone.now() - all_campaigns = DropCampaign.objects.filter( - game=game - ).select_related("owner").order_by('-end_at') - + all_campaigns = DropCampaign.objects.filter(game=game).select_related("owner").order_by("-end_at") + # Filter the campaigns in Python instead of making multiple queries active_campaigns = [ - campaign for campaign in all_campaigns - if campaign.start_at <= now and campaign.end_at >= now and campaign.status == "ACTIVE" + campaign for campaign in all_campaigns if campaign.start_at <= now and campaign.end_at >= now and campaign.status == "ACTIVE" ] active_campaigns.sort(key=lambda c: c.end_at) # Sort by end_at ascending - - upcoming_campaigns = [ - campaign for campaign in all_campaigns - if campaign.start_at > now and campaign.status == "UPCOMING" - ] + + upcoming_campaigns = [campaign for campaign in all_campaigns if campaign.start_at > now and campaign.status == "UPCOMING"] upcoming_campaigns.sort(key=lambda c: c.start_at) # Sort by start_at ascending # No need to fetch expired_campaigns separately as we already have all_campaigns - + context.update({ "active_campaigns": active_campaigns, "upcoming_campaigns": upcoming_campaigns, @@ -296,19 +274,10 @@ def dashboard(request: HttpRequest) -> HttpResponse: # Get active campaigns with prefetching to reduce queries now = timezone.now() active_campaigns = ( - DropCampaign.objects.filter( - start_at__lte=now, - end_at__gte=now, - status="ACTIVE" - ) + DropCampaign.objects.filter(start_at__lte=now, end_at__gte=now, status="ACTIVE") .select_related("game", "owner") # Prefetch the time-based drops with their benefits to avoid N+1 queries - .prefetch_related( - Prefetch( - 'time_based_drops', - queryset=TimeBasedDrop.objects.prefetch_related('benefits') - ) - ) + .prefetch_related(Prefetch("time_based_drops", queryset=TimeBasedDrop.objects.prefetch_related("benefits"))) ) return render(