This commit is contained in:
parent
17ef09465d
commit
4663a827e4
12 changed files with 434 additions and 405 deletions
|
|
@ -1,17 +1,19 @@
|
|||
{% extends "base.html" %}
|
||||
{% load image_tags %}
|
||||
{% block title %}
|
||||
Chat Badges - ttvdrops
|
||||
Chat Badges
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<h1>{{ badge_sets.count }} Twitch Chat Badges</h1>
|
||||
{% if badge_sets %}
|
||||
{% for data in badge_data %}
|
||||
<!-- {{ data.set.set_id }} - {{ data.badges|length }} version{% if data.badges|length > 1 %}s{% endif %} -->
|
||||
<h2>
|
||||
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}">{{ data.set.set_id }}</a>
|
||||
</h2>
|
||||
<table>
|
||||
{% for badge in data.badges %}
|
||||
<!-- {{ badge.title }} {% if badge.description != badge.title %}- {{ badge.description }}{% else %}{% endif %} -->
|
||||
<tr>
|
||||
<td style="width: 40px;">
|
||||
<a href="{% url 'twitch:badge_set_detail' set_id=data.set.set_id %}">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<table>
|
||||
<tbody>
|
||||
{% for badge in badges %}
|
||||
<!-- {{ badge.title }} {% if badge.description != badge.title %}- {{ badge.description }}{% else %}{% endif %} -->
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ badge.badge_id }}</code>
|
||||
|
|
@ -24,18 +25,16 @@
|
|||
<a href="{{ badge.image_url_4x }}" rel="nofollow ugc">[72px]</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if badge.click_url %}
|
||||
<a href="{{ badge.click_url }}" rel="nofollow ugc">{{ badge.click_action }}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
{% if badge.click_url %}<a href="{{ badge.click_url }}" rel="nofollow ugc">{{ badge.click_action }}</a>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if badge.award_campaigns %}
|
||||
<!-- If there are campaigns awarding this badge, show them in a nested row -->
|
||||
<div>
|
||||
The following campaigns have the same name as this badge and may be awarding it:
|
||||
<ul>
|
||||
{% for campaign in badge.award_campaigns %}
|
||||
<!-- Note: We can't be sure if these campaigns are actually awarding this badge, but it's likely given the name match. -->
|
||||
<li>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}">{{ campaign.clean_name }}</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -17,24 +17,28 @@
|
|||
{% endif %}
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<!-- Campaign Title -->
|
||||
<h1>
|
||||
{% if campaign.game %}
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.get_game_name }}</a> - {{ campaign.clean_name }}
|
||||
{% else %}
|
||||
{{ campaign.clean_name }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<!-- Campaign Owners -->
|
||||
{% for org in owners %}
|
||||
<p>
|
||||
<a href="{% url 'twitch:organization_detail' org.twitch_id %}">{{ org.name }}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
<div style="display: flex; align-items: flex-start;">
|
||||
<!-- Campaign image -->
|
||||
<div style="margin-right: 16px;">
|
||||
{% if campaign.image_best_url %}
|
||||
{% picture campaign.image_best_url alt=campaign.name width=160 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign Title -->
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<h1 style="margin-top: 0; margin-bottom: 0px;">{{ campaign.clean_name }}</h1>
|
||||
<div>
|
||||
{% if campaign.game %}
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">{{ campaign.game.get_game_name }}</a>
|
||||
{% endif %}
|
||||
{% if owners %}
|
||||
-
|
||||
<!-- Campaign Owners -->
|
||||
{% for org in owners %}
|
||||
<a href="{% url 'twitch:organization_detail' org.twitch_id %}">{{ org.name }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Campaign description -->
|
||||
<p>{{ campaign.description|linebreaksbr }}</p>
|
||||
<!-- Campaign end times -->
|
||||
|
|
@ -72,6 +76,7 @@
|
|||
<strong>Duration</strong> {{ campaign.end_at|timeuntil:campaign.start_at }}
|
||||
</time>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div>
|
||||
<!-- Campaign Detail URL -->
|
||||
{% if campaign.details_url %}<a href="{{ campaign.details_url }}" rel="nofollow ugc">[details]</a>{% endif %}
|
||||
|
|
@ -87,40 +92,24 @@
|
|||
title="Atom feed for {{ campaign.game.display_name }} campaigns">[atom]</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if allowed_channels %}
|
||||
<h5>Allowed Channels</h5>
|
||||
<div>
|
||||
{% for channel in allowed_channels %}
|
||||
<a href="{% url 'twitch:channel_detail' channel.twitch_id %}">{{ channel.display_name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ campaign.game.twitch_directory_url }}"
|
||||
rel="nofollow ugc"
|
||||
title="Find streamers playing {{ campaign.game.display_name }} with drops enabled">
|
||||
Go to a participating live channel
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h5>Campaign Info</h5>
|
||||
{% if drops %}
|
||||
<table id="drops-table" style="border-collapse: collapse; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Drop Name</th>
|
||||
<th>Requirements</th>
|
||||
<th>Period</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Drops table -->
|
||||
<table style="border-collapse: collapse; width: 100%;">
|
||||
<tbody>
|
||||
{% for drop in drops %}
|
||||
<tr id="drop-{{ drop.drop.twitch_id }}">
|
||||
<!-- {{ drop.drop.name }} - {{ drop.drop.benefits.all|join:", " }} -->
|
||||
<tr>
|
||||
<td>
|
||||
{% for benefit in drop.drop.benefits.all %}
|
||||
{% if benefit.image_asset_url %}
|
||||
<!-- Show the benefit image if available -->
|
||||
{% picture benefit.image_best_url|default:benefit.image_asset_url alt=benefit.name width=160 height=160 style="object-fit: cover; margin-right: 3px" %}
|
||||
{% endif %}
|
||||
{% if benefit.distribution_type == "BADGE" and drop.awarded_badge %}
|
||||
<!-- If the drop awards a badge, show the badge prominently with its description -->
|
||||
<div style="margin-top: 6px; font-size: 0.9em;">
|
||||
<strong>Awards Badge:</strong>
|
||||
<a href="{% url 'twitch:badge_set_detail' set_id=drop.awarded_badge.badge_set.set_id %}">
|
||||
|
|
@ -128,6 +117,7 @@
|
|||
{{ drop.awarded_badge.title }}
|
||||
</a>
|
||||
{% if drop.awarded_badge.description %}
|
||||
<!-- Show the badge description if available -->
|
||||
<div style="margin-top: 4px; color: #a9a9a9; font-size: 0.9em;">{{ drop.awarded_badge.description|linebreaksbr }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -138,6 +128,7 @@
|
|||
<div style="margin-bottom: 5px;">{{ drop.drop.name }}</div>
|
||||
{% for benefit in drop.drop.benefits.all %}
|
||||
{% if benefit.name != drop.drop.name %}
|
||||
<!-- Show additional benefits if they have a different name than the drop itself -->
|
||||
<div style="font-size: 0.9em; color: #a9a9a9; margin-bottom: 3px;">{{ benefit.name }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
@ -163,8 +154,27 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<!-- If there are no drops, show a message indicating that there are no drops available for this campaign -->
|
||||
<p>No drops available for this campaign.</p>
|
||||
{% endif %}
|
||||
<!-- Allowed channels -->
|
||||
{% if allowed_channels %}
|
||||
<!-- If specific allowed channels are specified, list them -->
|
||||
<h5>Allowed Channels</h5>
|
||||
<div>
|
||||
{% for channel in allowed_channels %}
|
||||
<!-- {{ channel.display_name }} https://www.twitch.tv/{{ channel.display_name }} -->
|
||||
<a href="{% url 'twitch:channel_detail' channel.twitch_id %}">{{ channel.display_name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- If no allowed channels are specified, link to the game's Twitch directory for channels with drops enabled -->
|
||||
<a href="{{ campaign.game.twitch_directory_url }}"
|
||||
rel="nofollow ugc"
|
||||
title="Find streamers playing {{ campaign.game.display_name }} with drops enabled">
|
||||
Go to a participating live channel
|
||||
</a>
|
||||
{% endif %}
|
||||
<!-- Campaign JSON -->
|
||||
{{ campaign_data|safe }}
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% load image_tags %}
|
||||
{% load image_tags %}
|
||||
{% block title %}
|
||||
Drop Campaigns - Twitch Drops Tracker
|
||||
Drop Campaigns
|
||||
{% endblock title %}
|
||||
{% block extra_head %}
|
||||
<link rel="alternate"
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
<select name="game">
|
||||
<option value="">All Games</option>
|
||||
{% for game in games %}
|
||||
<!-- Game option with Twitch ID {{ game.twitch_id }} and display name "{{ game.display_name }}" -->
|
||||
<option value="{{ game.twitch_id }}"
|
||||
{% if selected_game == game.twitch_id %}selected{% endif %}>
|
||||
{{ game.display_name|default:game.name|default:game.slug|default:game.twitch_id }}
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
<select id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
{% for status in status_options %}
|
||||
<!-- Status option "{{ status }}" -->
|
||||
<option value="{{ status }}"
|
||||
{% if selected_status == status %}selected{% endif %}>{{ status|title }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -63,6 +65,7 @@
|
|||
{% if campaigns %}
|
||||
{% regroup campaigns by game as campaigns_by_game %}
|
||||
{% for game_group in campaigns_by_game %}
|
||||
<!-- Game group for game "{{ game_group.grouper.display_name }}" with {{ game_group.list|length }} campaigns -->
|
||||
<section>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div>
|
||||
|
|
@ -99,6 +102,9 @@
|
|||
<div style="overflow-x: auto;">
|
||||
<div style="display: flex; gap: 1rem; min-width: max-content;">
|
||||
{% for campaign in game_group.list %}
|
||||
<!-- Campaign "{{ campaign.clean_name }}" with Twitch ID {{ campaign.twitch_id }} -->
|
||||
<!-- https://ttvdrops.lovinator.space{% url 'twitch:campaign_detail' campaign.twitch_id %} -->
|
||||
<!-- https://ttvdrops.lovinator.space{{ campaign.image_best_url }} -->
|
||||
<article style="display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -108,7 +114,7 @@
|
|||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}"
|
||||
style="text-decoration: none">
|
||||
{% if campaign.image_best_url %}
|
||||
{% picture campaign.image_best_url alt="Campaign artwork for "|add:campaign.name width=120 height=120 style="border-radius: 4px" %}
|
||||
{% picture campaign.image_best_url alt="Campaign artwork for "|add:campaign.name width=120 %}
|
||||
{% endif %}
|
||||
<h4 style="margin: 0.5rem 0; text-align: left">{{ campaign.clean_name }}</h4>
|
||||
</a>
|
||||
|
|
@ -153,61 +159,34 @@
|
|||
{% endif %}
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav style="margin-top: 3rem; text-align: center;">
|
||||
<div style="display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap">
|
||||
<!-- {{ page_obj.paginator.count }} total campaigns, showing {{ page_obj.start_index }} to {{ page_obj.end_index }} on page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} -->
|
||||
<nav style="text-align: center;">
|
||||
<div>
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page=1"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none">[first]</a>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page=1">[first]</a>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ page_obj.previous_page_number }}"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none"
|
||||
aria-label="Previous">[previous]</a>
|
||||
{% else %}
|
||||
<span style="padding: 0.5rem 1rem;">[first]</span>
|
||||
<span style="padding: 0.5rem 1rem;">[previous]</span>
|
||||
<span>[first]</span>
|
||||
<span>[previous]</span>
|
||||
{% endif %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<span style="padding: 0.5rem 1rem; border-radius: 4px; font-weight: 600">{{ num }}</span>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ num }}"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none">{{ num }}</a>
|
||||
<span>{{ num }}</span>
|
||||
{% elif num > page_obj.number|add:'-10' and num < page_obj.number|add:'10' %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ num }}">{{ num }}</a>
|
||||
{% elif num == 1 or num == page_obj.paginator.num_pages %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ num }}"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none">{{ num }}</a>
|
||||
{% elif num == page_obj.number|add:'-4' or num == page_obj.number|add:'4' %}
|
||||
<span style="padding: 0.5rem 1rem;">...</span>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ num }}">{{ num }}</a>
|
||||
{% elif num == page_obj.number|add:'-10' or num == page_obj.number|add:'10' %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ page_obj.next_page_number }}"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none">[next]</a>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
|
||||
style="padding: 0.5rem 1rem;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
text-decoration: none">[last]</a>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ page_obj.next_page_number }}">[next]</a>
|
||||
<a href="?{% if selected_status %}status={{ selected_status }}&{% endif %}{% if selected_game %}game={{ selected_game }}&{% endif %}{% if selected_per_page != 100 %}per_page={{ selected_per_page }}&{% endif %}page={{ page_obj.paginator.num_pages }}">[last]</a>
|
||||
{% else %}
|
||||
<span style="padding: 0.5rem 1rem;">[next]</span>
|
||||
<span style="padding: 0.5rem 1rem;">[last]</span>
|
||||
<span>[next]</span>
|
||||
<span>[last]</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small style="display: block; margin-top: 1rem;">
|
||||
|
|
|
|||
|
|
@ -19,22 +19,26 @@
|
|||
border: none">
|
||||
</iframe>
|
||||
<!-- Channel Info -->
|
||||
<p>
|
||||
<strong>Channel ID:</strong> {{ channel.twitch_id }}
|
||||
</p>
|
||||
<p>Channel ID: {{ channel.twitch_id }}</p>
|
||||
{% if active_campaigns %}
|
||||
<h5 id="active-campaigns-header">Active Campaigns</h5>
|
||||
<table id="active-campaigns-table">
|
||||
<h5>Active Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in active_campaigns %}
|
||||
<tr id="campaign-row-{{ campaign.twitch_id }}">
|
||||
<!-- Campaign {{ campaign.name }} ({{ campaign.twitch_id }}) -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}">{{ campaign.clean_name }}</a>
|
||||
{% if campaign.time_based_drops.all %}
|
||||
<div class="campaign-benefits">
|
||||
<!-- If the campaign has time-based drops, show the benefits in a nested div -->
|
||||
<div>
|
||||
<!-- swag swag swag {{campaign.sorted_benefits}} -->
|
||||
{% for benefit in campaign.sorted_benefits %}
|
||||
<span class="benefit-item" title="{{ benefit.name }}">
|
||||
<!-- Benefit {{ benefit.name }} ({{ benefit.twitch_id }}) -->
|
||||
<!-- {{ benefit.image_best_url }} -->
|
||||
<span title="{{ benefit.name }}">
|
||||
{% if benefit.image_best_url or benefit.image_asset_url %}
|
||||
<!-- Show the benefit image if available -->
|
||||
<img src="{{ benefit.image_best_url|default:benefit.image_asset_url }}"
|
||||
alt="{{ benefit.name }}"
|
||||
width="24"
|
||||
|
|
@ -51,6 +55,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if campaign.game %}
|
||||
<!-- If the campaign has an associated game, show the game name with a link to the game detail page -->
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">
|
||||
{{ campaign.game.display_name|default:campaign.game.name }}
|
||||
</a>
|
||||
|
|
@ -67,18 +72,23 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
{% if upcoming_campaigns %}
|
||||
<h5 id="upcoming-campaigns-header">Upcoming Campaigns</h5>
|
||||
<table id="upcoming-campaigns-table">
|
||||
<!-- If there are upcoming campaigns, show them in a separate section -->
|
||||
<h5>Upcoming Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in upcoming_campaigns %}
|
||||
<tr id="campaign-row-{{ campaign.twitch_id }}">
|
||||
<!-- Campaign {{ campaign.name }} ({{ campaign.twitch_id }}) -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}">{{ campaign.clean_name }}</a>
|
||||
{% if campaign.time_based_drops.all %}
|
||||
<div class="campaign-benefits">
|
||||
<!-- If the campaign has time-based drops, show the benefits in a nested div -->
|
||||
<div>
|
||||
{% for benefit in campaign.sorted_benefits %}
|
||||
<span class="benefit-item" title="{{ benefit.name }}">
|
||||
<!-- Benefit {{ benefit.name }} ({{ benefit.twitch_id }}) -->
|
||||
<span title="{{ benefit.name }}">
|
||||
{% if benefit.image_best_url or benefit.image_asset_url %}
|
||||
<!-- Show the benefit image if available -->
|
||||
<img src="{{ benefit.image_best_url|default:benefit.image_asset_url }}"
|
||||
alt="{{ benefit.name }}"
|
||||
width="24"
|
||||
|
|
@ -95,6 +105,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if campaign.game %}
|
||||
<!-- If the campaign has an associated game, show the game name with a link to the game detail page -->
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">
|
||||
{{ campaign.game.display_name|default:campaign.game.name }}
|
||||
</a>
|
||||
|
|
@ -111,17 +122,21 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
{% if expired_campaigns %}
|
||||
<h5 id="expired-campaigns-header">Past Campaigns</h5>
|
||||
<table id="expired-campaigns-table">
|
||||
<!-- If there are expired campaigns, show them in a separate section -->
|
||||
<h5>Past Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in expired_campaigns %}
|
||||
<tr id="campaign-row-{{ campaign.twitch_id }}">
|
||||
<!-- Campaign {{ campaign.name }} ({{ campaign.twitch_id }}) -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.twitch_id %}">{{ campaign.clean_name }}</a>
|
||||
{% if campaign.time_based_drops.all %}
|
||||
<div class="campaign-benefits">
|
||||
<!-- If the campaign has time-based drops, show the benefits in a nested div -->
|
||||
<div>
|
||||
{% for benefit in campaign.sorted_benefits %}
|
||||
<span class="benefit-item" title="{{ benefit.name }}">
|
||||
<!-- Benefit {{ benefit.name }} ({{ benefit.twitch_id }}) -->
|
||||
<span title="{{ benefit.name }}">
|
||||
{% if benefit.image_best_url or benefit.image_asset_url %}
|
||||
<img src="{{ benefit.image_best_url|default:benefit.image_asset_url }}"
|
||||
alt="{{ benefit.name }}"
|
||||
|
|
@ -139,6 +154,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if campaign.game %}
|
||||
<!-- If the campaign has an associated game, show the game name with a link to the game detail page -->
|
||||
<a href="{% url 'twitch:game_detail' campaign.game.twitch_id %}">
|
||||
{{ campaign.game.display_name|default:campaign.game.name }}
|
||||
</a>
|
||||
|
|
@ -155,7 +171,7 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
{% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %}
|
||||
<p id="no-campaigns-message">No campaigns found for this channel.</p>
|
||||
<p>No campaigns found for this channel.</p>
|
||||
{% endif %}
|
||||
{{ channel_data|safe }}
|
||||
{% endblock content %}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,24 @@
|
|||
{% block content %}
|
||||
<main>
|
||||
<h1>Active Twitch Drops Campaigns</h1>
|
||||
<p>
|
||||
This page lists all currently active Twitch Drops campaigns
|
||||
organized by game.
|
||||
<br />
|
||||
Click on a campaign for more details about it and how to
|
||||
earn drops.
|
||||
<br />
|
||||
Individual RSS feeds are available under each game, and
|
||||
there is also a global feed for all campaigns:
|
||||
</p>
|
||||
<!-- RSS Feeds -->
|
||||
<div>
|
||||
<a href="{% url 'twitch:campaign_feed' %}"
|
||||
title="RSS feed for all campaigns">[rss - all campaigns]</a>
|
||||
title="RSS feed for all campaigns">[rss]</a>
|
||||
<a href="{% url 'twitch:campaign_feed_atom' %}"
|
||||
title="Atom feed for campaigns">[atom - all campaigns]</a>
|
||||
title="Atom feed for campaigns">[atom]</a>
|
||||
</div>
|
||||
<hr />
|
||||
{% if campaigns_by_game %}
|
||||
{% for game_id, game_data in campaigns_by_game.items %}
|
||||
<article id="game-article-{{ game_id }}" style="margin-bottom: 2rem;">
|
||||
|
|
@ -42,23 +53,22 @@
|
|||
{% endif %}
|
||||
</header>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div style="flex-shrink: 0;">
|
||||
{% picture game_data.box_art alt="Box art for "|add:game_data.name width=200 height=267 style="border-radius: 8px" %}
|
||||
</div>
|
||||
<div style="flex-shrink: 0;">{% picture game_data.box_art alt="Box art for "|add:game_data.name width=200 %}</div>
|
||||
<div style="flex: 1; overflow-x: auto;">
|
||||
<div style="display: flex; gap: 1rem; min-width: max-content;">
|
||||
{% for campaign_data in game_data.campaigns %}
|
||||
<article id="campaign-article-{{ campaign_data.campaign.twitch_id }}"
|
||||
style="display: flex;
|
||||
<!-- {{ campaign_data.campaign.name }} -->
|
||||
<article style="display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
flex-shrink: 0">
|
||||
<div>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign_data.campaign.twitch_id %}">
|
||||
{% picture campaign_data.campaign.image_best_url|default:campaign_data.campaign.image_url alt="Image for "|add:campaign_data.campaign.name width=120 height=120 style="border-radius: 4px" %}
|
||||
{% picture campaign_data.campaign.image_best_url|default:campaign_data.campaign.image_url alt="Image for "|add:campaign_data.campaign.name width=120 %}
|
||||
<h4 style="margin: 0.5rem 0; text-align: left;">{{ campaign_data.campaign.clean_name }}</h4>
|
||||
</a>
|
||||
<!-- End time -->
|
||||
<time datetime="{{ campaign_data.campaign.end_at|date:'c' }}"
|
||||
title="{{ campaign_data.campaign.end_at|date:'DATETIME_FORMAT' }}"
|
||||
style="font-size: 0.9rem;
|
||||
|
|
@ -66,6 +76,7 @@
|
|||
text-align: left">
|
||||
Ends in {{ campaign_data.campaign.end_at|timeuntil }}
|
||||
</time>
|
||||
<!-- Start time -->
|
||||
<time datetime="{{ campaign_data.campaign.start_at|date:'c' }}"
|
||||
title="{{ campaign_data.campaign.start_at|date:'DATETIME_FORMAT' }}"
|
||||
style="font-size: 0.9rem;
|
||||
|
|
@ -73,6 +84,7 @@
|
|||
text-align: left">
|
||||
Started {{ campaign_data.campaign.start_at|timesince }} ago
|
||||
</time>
|
||||
<!-- Duration -->
|
||||
<time datetime="{{ campaign_data.campaign.duration_iso }}"
|
||||
title="{{ campaign_data.campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign_data.campaign.end_at|date:'DATETIME_FORMAT' }}"
|
||||
style="font-size: 0.9rem;
|
||||
|
|
@ -87,27 +99,20 @@
|
|||
list-style-type: none">
|
||||
{% if campaign_data.campaign.allow_is_enabled %}
|
||||
{% if campaign_data.allowed_channels %}
|
||||
{% for channel in campaign_data.allowed_channels %}
|
||||
{% if forloop.counter <= 5 %}
|
||||
{% for channel in campaign_data.allowed_channels|slice:":5" %}
|
||||
<!-- {{ channel.name }} -->
|
||||
<li style="margin-bottom: 0.1rem;">
|
||||
<a href="https://twitch.tv/{{ channel.name }}"
|
||||
rel="nofollow ugc"
|
||||
title="Watch {{ channel.display_name }} on Twitch">
|
||||
{{ channel.display_name }}
|
||||
</a>
|
||||
<a href="{% url 'twitch:channel_detail' channel.twitch_id %}"
|
||||
{{ channel.display_name }}</a><a href="{% url 'twitch:channel_detail' channel.twitch_id %}"
|
||||
title="View {{ channel.display_name }} details"
|
||||
style="font-family: monospace;
|
||||
text-decoration: none">[i]</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if campaign_data.allowed_channels|length > 5 %}
|
||||
<li style="margin-bottom: 0.1rem; color: #666; font-style: italic;">
|
||||
... and {{ campaign_data.allowed_channels|length|add:"-5" }} more
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- No allowed channels means drops are available in any stream of the game's category -->
|
||||
{% if campaign.game.twitch_directory_url %}
|
||||
<li>
|
||||
<a href="{{ campaign.game.twitch_directory_url }}"
|
||||
|
|
@ -120,8 +125,15 @@
|
|||
<li>Failed to get Twitch category URL :(</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if campaign_data.allowed_channels|length > 5 %}
|
||||
<!-- {{ campaign_data.allowed_channels|length }} allowed channels -->
|
||||
<li style="margin-bottom: 0.1rem; color: #666; font-style: italic;">
|
||||
... and {{ campaign_data.allowed_channels|length|add:"-5" }} more
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if campaign_data.campaign.game.twitch_directory_url %}
|
||||
<!--{{ campaign_data.campaign.game.display_name }} Twitch directory URL: {{ campaign_data.campaign.game.twitch_directory_url }} -->
|
||||
<li>
|
||||
<a href="{{ campaign_data.campaign.game.twitch_directory_url }}"
|
||||
rel="nofollow ugc"
|
||||
|
|
@ -130,6 +142,7 @@
|
|||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<!-- {{ campaign_data.campaign.game.display_name }} Twitch directory URL not available -->
|
||||
<li>Failed to get Twitch directory URL :(</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
{% block content %}
|
||||
<h1>Emotes</h1>
|
||||
{% for emote in emotes %}
|
||||
<!-- Emote from campaign {{ emote.campaign.name }} -->
|
||||
<!-- https://ttvdrops.lovinator.space{{ emote.image_url }} -->
|
||||
<a href="{% url 'twitch:campaign_detail' emote.campaign.twitch_id %}"
|
||||
title="{{ emote.campaign.name }}"
|
||||
style="display: inline-block">
|
||||
|
|
|
|||
|
|
@ -16,31 +16,41 @@
|
|||
{% endif %}
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<!-- Game Title -->
|
||||
<h1>
|
||||
{{ game.display_name }}
|
||||
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
|
||||
</h1>
|
||||
<div style="display: flex; align-items: flex-start;">
|
||||
<!-- Game image -->
|
||||
<div style="margin-right: 16px;">
|
||||
{% if game.box_art_best_url %}
|
||||
{% picture game.box_art_best_url alt=game.name width=160 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Game Title and Details -->
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<h1 style="margin-top: 0; margin-bottom: 0px;">
|
||||
{{ game.display_name }}
|
||||
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
|
||||
</h1>
|
||||
<!-- Game owner -->
|
||||
{% if owners %}
|
||||
<small>
|
||||
Owned by
|
||||
{% for owner in owners %}
|
||||
<a id="owner-link-{{ owner.twitch_id }}"
|
||||
href="{% url 'twitch:organization_detail' owner.twitch_id %}">{{ owner.name }}</a>
|
||||
<a href="{% url 'twitch:organization_detail' owner.twitch_id %}">{{ owner.name }}</a>
|
||||
{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</small>
|
||||
{% endif %}
|
||||
<div>
|
||||
Twitch ID: <a href="https://www.twitch.tv/directory/category/{{ game.slug|urlencode }}">{{ game.twitch_id }}</a>
|
||||
</div>
|
||||
<div>Twitch slug: {{ game.slug }}</div>
|
||||
<!-- RSS Feeds -->
|
||||
<div>
|
||||
<a href="{% url 'twitch:game_campaign_feed' game.twitch_id %}"
|
||||
title="RSS feed for {{ game.display_name }} campaigns">[rss - {{ game.display_name|default:game.name|lower }}]</a>
|
||||
title="RSS feed for {{ game.display_name }} campaigns">[rss]</a>
|
||||
<a href="{% url 'twitch:game_campaign_feed_atom' game.twitch_id %}"
|
||||
title="Atom feed for {{ game.display_name }} campaigns">[atom - {{ game.display_name|default:game.name|lower }}]</a>
|
||||
title="Atom feed for {{ game.display_name }} campaigns">[atom]</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if active_campaigns %}
|
||||
<h5 id="active-campaigns-header">Active Campaigns</h5>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
Reward Campaigns - Twitch Drops Tracker
|
||||
Reward Campaigns
|
||||
{% endblock title %}
|
||||
{% block extra_head %}
|
||||
<link rel="alternate"
|
||||
|
|
@ -14,47 +14,27 @@
|
|||
href="{% url 'twitch:reward_campaign_feed_atom' %}" />
|
||||
{% endblock extra_head %}
|
||||
{% block content %}
|
||||
<h1 id="page-title">Reward Campaigns (Quest Rewards)</h1>
|
||||
<p>Browse all available quest reward campaigns</p>
|
||||
<h1>Reward Campaigns</h1>
|
||||
<!-- RSS Feeds -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div>
|
||||
<a href="{% url 'twitch:reward_campaign_feed' %}"
|
||||
style="margin-right: 1rem"
|
||||
title="RSS feed for all reward campaigns">RSS feed for all reward campaigns</a>
|
||||
title="RSS feed for all reward campaigns">[rss]</a>
|
||||
<a href="{% url 'twitch:reward_campaign_feed_atom' %}"
|
||||
title="Atom feed for all reward campaigns">[atom]</a>
|
||||
</div>
|
||||
<!-- Filter Form -->
|
||||
<form id="filter-form"
|
||||
method="get"
|
||||
action="{% url 'twitch:reward_campaign_list' %}">
|
||||
<label for="game">Game:</label>
|
||||
<select id="game" name="game">
|
||||
<option value="">All Games</option>
|
||||
{% for game in games %}
|
||||
<option value="{{ game.twitch_id }}"
|
||||
{% if selected_game == game.twitch_id %}selected{% endif %}>
|
||||
{{ game.display_name|default:game.name|default:game.slug|default:game.twitch_id }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="status">Status:</label>
|
||||
<select id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
{% for status in status_options %}
|
||||
<option value="{{ status }}"
|
||||
{% if selected_status == status %}selected{% endif %}>{{ status|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="apply-filters-button" type="submit">Apply Filters</button>
|
||||
</form>
|
||||
<p>This is an archive of old Twitch reward campaigns because we do not monitor them.</p>
|
||||
<p>
|
||||
Feel free to submit a pull request on <a href="https://github.com/TheLovinator1/ttvdrops">GitHub</a>
|
||||
with a working implementation :-).
|
||||
</p>
|
||||
{% if reward_campaigns %}
|
||||
{% comment %}
|
||||
<h5>Active Reward Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in reward_campaigns %}
|
||||
{% if campaign.starts_at <= now and campaign.ends_at >= now %}
|
||||
<tr id="reward-campaign-{{ campaign.twitch_id }}">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||
{% if campaign.brand %}
|
||||
|
|
@ -83,12 +63,15 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endcomment %}
|
||||
{% comment %}
|
||||
<h5>Upcoming Reward Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in reward_campaigns %}
|
||||
|
||||
{% if campaign.starts_at > now %}
|
||||
<tr id="reward-campaign-{{ campaign.twitch_id }}">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||
{% if campaign.brand %}
|
||||
|
|
@ -117,12 +100,13 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endcomment %}
|
||||
<h5>Past Reward Campaigns</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for campaign in reward_campaigns %}
|
||||
{% if campaign.ends_at < now %}
|
||||
<tr id="reward-campaign-{{ campaign.twitch_id }}">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'twitch:reward_campaign_detail' campaign.twitch_id %}">
|
||||
{% if campaign.brand %}
|
||||
|
|
|
|||
|
|
@ -599,6 +599,14 @@ class DropCampaign(auto_prefetch.Model):
|
|||
"""Return the campaign image URL for RSS enclosures."""
|
||||
return self.image_best_url
|
||||
|
||||
@property
|
||||
def sorted_benefits(self) -> list[DropBenefit]:
|
||||
"""Return a sorted list of benefits for the campaign."""
|
||||
benefits: list[DropBenefit] = []
|
||||
for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
benefits.extend(drop.benefits.all()) # pyright: ignore[reportAttributeAccessIssue]
|
||||
return sorted(benefits, key=lambda benefit: benefit.name)
|
||||
|
||||
|
||||
# MARK: DropBenefit
|
||||
class DropBenefit(auto_prefetch.Model):
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ class TestChannelListView:
|
|||
)
|
||||
game.owners.add(org1, org2)
|
||||
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Campaign",
|
||||
game=game,
|
||||
|
|
@ -519,14 +519,11 @@ class TestChannelListView:
|
|||
if isinstance(context, list):
|
||||
context = context[-1]
|
||||
|
||||
# campaigns_by_game should include one deduplicated campaign entry for the game.
|
||||
assert "campaigns_by_game" in context
|
||||
assert game.twitch_id in context["campaigns_by_game"]
|
||||
assert len(context["campaigns_by_game"][game.twitch_id]["campaigns"]) == 1
|
||||
|
||||
# Template renders each campaign with a stable id, so we can assert it appears once.
|
||||
html = response.content.decode("utf-8")
|
||||
assert html.count(f"campaign-article-{campaign.twitch_id}") == 1
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_debug_view(self, client: Client) -> None:
|
||||
"""Test debug view returns 200 and has games_without_owner in context."""
|
||||
|
|
|
|||
|
|
@ -290,23 +290,29 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
|||
results["games"] = Game.objects.filter(
|
||||
Q(name__istartswith=query) | Q(display_name__istartswith=query),
|
||||
)
|
||||
|
||||
results["campaigns"] = DropCampaign.objects.filter(
|
||||
Q(name__istartswith=query) | Q(description__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["drops"] = TimeBasedDrop.objects.filter(
|
||||
name__istartswith=query,
|
||||
).select_related("campaign")
|
||||
|
||||
results["benefits"] = DropBenefit.objects.filter(
|
||||
name__istartswith=query,
|
||||
).prefetch_related("drops__campaign")
|
||||
|
||||
results["reward_campaigns"] = RewardCampaign.objects.filter(
|
||||
Q(name__istartswith=query)
|
||||
| Q(brand__istartswith=query)
|
||||
| Q(summary__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["badge_sets"] = ChatBadgeSet.objects.filter(
|
||||
set_id__istartswith=query,
|
||||
)
|
||||
|
||||
results["badges"] = ChatBadge.objects.filter(
|
||||
Q(title__istartswith=query) | Q(description__icontains=query),
|
||||
).select_related("badge_set")
|
||||
|
|
@ -317,20 +323,25 @@ def search_view(request: HttpRequest) -> HttpResponse:
|
|||
results["games"] = Game.objects.filter(
|
||||
Q(name__icontains=query) | Q(display_name__icontains=query),
|
||||
)
|
||||
|
||||
results["campaigns"] = DropCampaign.objects.filter(
|
||||
Q(name__icontains=query) | Q(description__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["drops"] = TimeBasedDrop.objects.filter(
|
||||
name__icontains=query,
|
||||
).select_related("campaign")
|
||||
|
||||
results["benefits"] = DropBenefit.objects.filter(
|
||||
name__icontains=query,
|
||||
).prefetch_related("drops__campaign")
|
||||
|
||||
results["reward_campaigns"] = RewardCampaign.objects.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(brand__icontains=query)
|
||||
| Q(summary__icontains=query),
|
||||
).select_related("game")
|
||||
|
||||
results["badge_sets"] = ChatBadgeSet.objects.filter(set_id__icontains=query)
|
||||
results["badges"] = ChatBadge.objects.filter(
|
||||
Q(title__icontains=query) | Q(description__icontains=query),
|
||||
|
|
@ -1127,42 +1138,13 @@ class GameDetailView(DetailView):
|
|||
either end date or status.
|
||||
"""
|
||||
context: dict[str, Any] = super().get_context_data(**kwargs)
|
||||
game: Game = self.get_object() # pyright: ignore[reportAssignmentType]
|
||||
game: Game = self.object # pyright: ignore[reportAssignmentType]
|
||||
|
||||
now: datetime.datetime = timezone.now()
|
||||
# For each drop, find awarded badge (distribution_type BADGE)
|
||||
drop_awarded_badges: dict[str, ChatBadge] = {}
|
||||
drops: QuerySet[TimeBasedDrop, TimeBasedDrop] = TimeBasedDrop.objects.filter(
|
||||
campaign__game=game,
|
||||
).prefetch_related("benefits")
|
||||
|
||||
# Materialize drops so we can iterate multiple times without extra DB hits
|
||||
drops_list: list[TimeBasedDrop] = list(drops)
|
||||
|
||||
# Collect all benefit names that award badges
|
||||
benefit_badge_titles: set[str] = set()
|
||||
for drop in drops_list:
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE" and benefit.name:
|
||||
benefit_badge_titles.add(benefit.name)
|
||||
|
||||
# Bulk-load all matching ChatBadge instances to avoid N+1 queries
|
||||
badges_by_title: dict[str, ChatBadge] = {
|
||||
badge.title: badge
|
||||
for badge in ChatBadge.objects.filter(title__in=benefit_badge_titles)
|
||||
}
|
||||
|
||||
for drop in drops_list:
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE":
|
||||
badge: ChatBadge | None = badges_by_title.get(benefit.name)
|
||||
if badge:
|
||||
drop_awarded_badges[drop.twitch_id] = badge
|
||||
|
||||
all_campaigns: QuerySet[DropCampaign] = (
|
||||
DropCampaign.objects
|
||||
.filter(game=game)
|
||||
.prefetch_related("game__owners")
|
||||
.select_related("game")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
|
|
@ -1177,9 +1159,34 @@ class GameDetailView(DetailView):
|
|||
.order_by("-end_at")
|
||||
)
|
||||
|
||||
campaigns_list: list[DropCampaign] = list(all_campaigns)
|
||||
|
||||
# For each drop, find awarded badge (distribution_type BADGE)
|
||||
drop_awarded_badges: dict[str, ChatBadge] = {}
|
||||
benefit_badge_titles: set[str] = set()
|
||||
for campaign in campaigns_list:
|
||||
for drop in campaign.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE" and benefit.name:
|
||||
benefit_badge_titles.add(benefit.name)
|
||||
|
||||
# Bulk-load all matching ChatBadge instances to avoid N+1 queries
|
||||
badges_by_title: dict[str, ChatBadge] = {
|
||||
badge.title: badge
|
||||
for badge in ChatBadge.objects.filter(title__in=benefit_badge_titles)
|
||||
}
|
||||
|
||||
for campaign in campaigns_list:
|
||||
for drop in campaign.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
for benefit in drop.benefits.all():
|
||||
if benefit.distribution_type == "BADGE":
|
||||
badge: ChatBadge | None = badges_by_title.get(benefit.name)
|
||||
if badge:
|
||||
drop_awarded_badges[drop.twitch_id] = badge
|
||||
|
||||
active_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None
|
||||
and campaign.start_at <= now
|
||||
and campaign.end_at is not None
|
||||
|
|
@ -1195,7 +1202,7 @@ class GameDetailView(DetailView):
|
|||
|
||||
upcoming_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None and campaign.start_at > now
|
||||
]
|
||||
|
||||
|
|
@ -1209,7 +1216,7 @@ class GameDetailView(DetailView):
|
|||
|
||||
expired_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.end_at is not None and campaign.end_at < now
|
||||
]
|
||||
|
||||
|
|
@ -1229,10 +1236,10 @@ class GameDetailView(DetailView):
|
|||
)
|
||||
game_data: list[dict[str, Any]] = json.loads(serialized_game)
|
||||
|
||||
if all_campaigns.exists():
|
||||
if campaigns_list:
|
||||
serialized_campaigns = serialize(
|
||||
"json",
|
||||
all_campaigns,
|
||||
campaigns_list,
|
||||
fields=(
|
||||
"twitch_id",
|
||||
"name",
|
||||
|
|
@ -2028,13 +2035,13 @@ class ChannelDetailView(DetailView):
|
|||
dict: Context data with active, upcoming, and expired campaigns.
|
||||
"""
|
||||
context: dict[str, Any] = super().get_context_data(**kwargs)
|
||||
channel: Channel = self.get_object() # pyright: ignore[reportAssignmentType]
|
||||
channel: Channel = self.object # pyright: ignore[reportAssignmentType]
|
||||
|
||||
now: datetime.datetime = timezone.now()
|
||||
all_campaigns: QuerySet[DropCampaign] = (
|
||||
DropCampaign.objects
|
||||
.filter(allow_channels=channel)
|
||||
.prefetch_related("game__owners")
|
||||
.select_related("game")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"time_based_drops",
|
||||
|
|
@ -2049,9 +2056,11 @@ class ChannelDetailView(DetailView):
|
|||
.order_by("-start_at")
|
||||
)
|
||||
|
||||
campaigns_list: list[DropCampaign] = list(all_campaigns)
|
||||
|
||||
active_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None
|
||||
and campaign.start_at <= now
|
||||
and campaign.end_at is not None
|
||||
|
|
@ -2067,7 +2076,7 @@ class ChannelDetailView(DetailView):
|
|||
|
||||
upcoming_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.start_at is not None and campaign.start_at > now
|
||||
]
|
||||
upcoming_campaigns.sort(
|
||||
|
|
@ -2080,7 +2089,7 @@ class ChannelDetailView(DetailView):
|
|||
|
||||
expired_campaigns: list[DropCampaign] = [
|
||||
campaign
|
||||
for campaign in all_campaigns
|
||||
for campaign in campaigns_list
|
||||
if campaign.end_at is not None and campaign.end_at < now
|
||||
]
|
||||
|
||||
|
|
@ -2091,10 +2100,10 @@ class ChannelDetailView(DetailView):
|
|||
)
|
||||
channel_data: list[dict[str, Any]] = json.loads(serialized_channel)
|
||||
|
||||
if all_campaigns.exists():
|
||||
if campaigns_list:
|
||||
serialized_campaigns: str = serialize(
|
||||
"json",
|
||||
all_campaigns,
|
||||
campaigns_list,
|
||||
fields=(
|
||||
"twitch_id",
|
||||
"name",
|
||||
|
|
@ -2112,7 +2121,7 @@ class ChannelDetailView(DetailView):
|
|||
channel_data[0]["fields"]["campaigns"] = campaigns_data
|
||||
|
||||
name: str = channel.display_name or channel.name or channel.twitch_id
|
||||
total_campaigns: int = len(all_campaigns)
|
||||
total_campaigns: int = len(campaigns_list)
|
||||
description: str = f"{name} participates in {total_campaigns} drop campaign"
|
||||
if total_campaigns > 1:
|
||||
description += "s"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue