diff --git a/templates/base.html b/templates/base.html
index 3310930..ff010f7 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -55,6 +55,16 @@
Login |
Sign Up
{% endif %}
+ {% if messages %}
+
+ {% for message in messages %}
+ -
+ {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important:{% endif %}
+ {{ message }}
+
+ {% endfor %}
+
+ {% endif %}
{% block content %}
{% endblock content %}
diff --git a/templates/twitch/game_detail.html b/templates/twitch/game_detail.html
index d041079..3335444 100644
--- a/templates/twitch/game_detail.html
+++ b/templates/twitch/game_detail.html
@@ -4,14 +4,29 @@
{% endblock title %}
{% block content %}
{{ game.display_name }}
-
-
-
-
-
-
-
-
+ {% if user.is_authenticated %}
+
+ {% else %}
+ Login to subscribe!
+ {% endif %}
{% if active_campaigns %}
Active Campaigns
diff --git a/twitch/migrations/0002_notificationsubscription.py b/twitch/migrations/0002_notificationsubscription.py
new file mode 100644
index 0000000..305cb53
--- /dev/null
+++ b/twitch/migrations/0002_notificationsubscription.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.2.4 on 2025-08-02 02:08
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('twitch', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NotificationSubscription',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('notify_found', models.BooleanField(default=False)),
+ ('notify_live', models.BooleanField(default=False)),
+ ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='twitch.game')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'game')},
+ },
+ ),
+ ]
diff --git a/twitch/models.py b/twitch/models.py
index da07aec..65ea817 100644
--- a/twitch/models.py
+++ b/twitch/models.py
@@ -5,6 +5,8 @@ from typing import ClassVar
from django.db import models
from django.utils import timezone
+from accounts.models import User
+
class Game(models.Model):
"""Represents a game on Twitch."""
@@ -195,3 +197,18 @@ class DropBenefitEdge(models.Model):
def __str__(self) -> str:
"""Return a string representation of the drop benefit edge."""
return f"{self.drop.name} - {self.benefit.name}"
+
+
+class NotificationSubscription(models.Model):
+ """Users can subscribe to games to get notified."""
+
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ game = models.ForeignKey(Game, on_delete=models.CASCADE)
+ notify_found = models.BooleanField(default=False)
+ notify_live = models.BooleanField(default=False)
+
+ class Meta:
+ unique_together = ("user", "game")
+
+ def __str__(self) -> str:
+ return f"{self.user} subscription to {Game.display_name}"
diff --git a/twitch/urls.py b/twitch/urls.py
index 0c36b52..a0de088 100644
--- a/twitch/urls.py
+++ b/twitch/urls.py
@@ -12,4 +12,5 @@ urlpatterns = [
path("campaigns//", views.DropCampaignDetailView.as_view(), name="campaign_detail"),
path("games/", views.GameListView.as_view(), name="game_list"),
path("games//", views.GameDetailView.as_view(), name="game_detail"),
+ path("games//subscribe/", views.subscribe_notifications, name="subscribe_notifications"),
]
diff --git a/twitch/views.py b/twitch/views.py
index 6f5b296..94afb42 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -4,19 +4,22 @@ import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, cast
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
from django.db.models import Count, Prefetch, Q
from django.db.models.query import QuerySet
-from django.shortcuts import render
+from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.generic import DetailView, ListView
-from twitch.models import DropCampaign, Game, Organization, TimeBasedDrop
+from twitch.models import DropCampaign, Game, NotificationSubscription, Organization, TimeBasedDrop
if TYPE_CHECKING:
import datetime
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse
+ from django.http.response import HttpResponseRedirect
logger: logging.Logger = logging.getLogger(__name__)
@@ -229,6 +232,12 @@ class GameDetailView(DetailView):
context: dict[str, Any] = super().get_context_data(**kwargs)
game: Game = self.get_object()
+ user = self.request.user
+ if not user.is_authenticated:
+ subscription: NotificationSubscription | None = None
+ else:
+ subscription = NotificationSubscription.objects.filter(user=user, game=game).first()
+
now: datetime.datetime = timezone.now()
all_campaigns: QuerySet[DropCampaign, DropCampaign] = (
DropCampaign.objects.filter(game=game).select_related("owner").order_by("-end_at")
@@ -248,6 +257,7 @@ class GameDetailView(DetailView):
"active_campaigns": active_campaigns,
"upcoming_campaigns": upcoming_campaigns,
"expired_campaigns": expired_campaigns,
+ "subscription": subscription,
"now": now,
})
@@ -305,3 +315,45 @@ def dashboard(request: HttpRequest) -> HttpResponse:
"now": now,
},
)
+
+
+@login_required
+def subscribe_notifications(request: HttpRequest, game_id: str) -> HttpResponseRedirect:
+ """Update notification for a user.
+
+ Args:
+ request: The HTTP request.
+ game_id: The game we are updating.
+
+ Returns:
+ Redirect back to the twitch:game_detail.
+ """
+ game: Game = get_object_or_404(Game, pk=game_id)
+ if request.method == "POST":
+ notify_found = bool(request.POST.get("notify_found"))
+ notify_live = bool(request.POST.get("notify_live"))
+
+ subscription, created = NotificationSubscription.objects.get_or_create(user=request.user, game=game)
+
+ changes = []
+ if not created:
+ if subscription.notify_found != notify_found:
+ changes.append(f"Notify when drop is found: {'enabled' if notify_found else 'disabled'}")
+ if subscription.notify_live != notify_live:
+ changes.append(f"Notify when drop is farmable: {'enabled' if notify_live else 'disabled'}")
+
+ subscription.notify_found = notify_found
+ subscription.notify_live = notify_live
+ subscription.save()
+
+ if created:
+ message = "You have subscribed to notifications for this game."
+ elif changes:
+ message = "Updated notification preferences: " + ", ".join(changes)
+ else:
+ message = "No changes were made to your notification preferences."
+
+ messages.success(request, message)
+ return redirect("twitch:game_detail", pk=game.id)
+
+ return redirect("twitch:game_detail", pk=game.id)