diff --git a/pyproject.toml b/pyproject.toml
index aa5a76d..297126e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ lint.ignore = [
"CPY001", # Checks for the absence of copyright notices within Python files.
"D100", # Checks for undocumented public module definitions.
"D104", # Checks for undocumented public package definitions.
+ "D105", # Checks for undocumented magic method definitions.
"D106", # Checks for undocumented public class definitions, for nested classes.
"ERA001", # Checks for commented-out Python code.
"FIX002", # Checks for "TODO" comments.
diff --git a/templates/base.html b/templates/base.html
index ff010f7..5382bec 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -46,6 +46,7 @@
Dashboard |
Campaigns |
Games |
+ Organizations |
{% if user.is_authenticated %}
{% if user.is_staff %}
Admin |
@@ -60,7 +61,7 @@
{% for message in messages %}
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important:{% endif %}
- {{ message }}
+ {{ message|linebreaksbr }}
{% endfor %}
diff --git a/templates/twitch/campaign_detail.html b/templates/twitch/campaign_detail.html
index e26d424..9952918 100644
--- a/templates/twitch/campaign_detail.html
+++ b/templates/twitch/campaign_detail.html
@@ -9,7 +9,7 @@
{# TODO: Link to organization #}
- {{ campaign.owner.name }}
+ {{ campaign.owner.name }}
{% if campaign.image_url %}
Games by Organization
+ Games
{% if games_by_org %}
{% for organization, games in games_by_org.items %}
{{ organization.name }}
diff --git a/templates/twitch/org_list.html b/templates/twitch/org_list.html
new file mode 100644
index 0000000..44882d9
--- /dev/null
+++ b/templates/twitch/org_list.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% block title %}
+ Games
+{% endblock title %}
+{% block content %}
+ Organizations
+ {% if orgs %}
+
+ {% else %}
+ No games found.
+ {% endif %}
+{% endblock content %}
diff --git a/templates/twitch/organization_detail.html b/templates/twitch/organization_detail.html
new file mode 100644
index 0000000..968d5da
--- /dev/null
+++ b/templates/twitch/organization_detail.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+{% block title %}
+ {{ organization.name }}
+{% endblock title %}
+{% block content %}
+ {{ organization.name }}
+ {% if user.is_authenticated %}
+
+ {% else %}
+ Login to subscribe!
+ {% endif %}
+
+ {% for game in games %}
+ -
+ {{ game }}
+
+ {% endfor %}
+
+{% endblock content %}
diff --git a/twitch/migrations/0003_alter_notificationsubscription_unique_together_and_more.py b/twitch/migrations/0003_alter_notificationsubscription_unique_together_and_more.py
new file mode 100644
index 0000000..9b01694
--- /dev/null
+++ b/twitch/migrations/0003_alter_notificationsubscription_unique_together_and_more.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.2.4 on 2025-08-02 03:39
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('twitch', '0002_notificationsubscription'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='notificationsubscription',
+ unique_together={('user', 'game')},
+ ),
+ migrations.AddField(
+ model_name='notificationsubscription',
+ name='organization',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='twitch.organization'),
+ ),
+ migrations.AlterField(
+ model_name='notificationsubscription',
+ name='game',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='twitch.game'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='notificationsubscription',
+ unique_together={('user', 'game'), ('user', 'organization')},
+ ),
+ ]
diff --git a/twitch/models.py b/twitch/models.py
index 65ea817..86a75da 100644
--- a/twitch/models.py
+++ b/twitch/models.py
@@ -203,12 +203,17 @@ 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)
+ game = models.ForeignKey(Game, null=True, blank=True, on_delete=models.CASCADE)
+ organization = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE)
+
notify_found = models.BooleanField(default=False)
notify_live = models.BooleanField(default=False)
class Meta:
- unique_together = ("user", "game")
+ unique_together: ClassVar[list[tuple[str, str]]] = [
+ ("user", "game"),
+ ("user", "organization"),
+ ]
def __str__(self) -> str:
return f"{self.user} subscription to {Game.display_name}"
diff --git a/twitch/urls.py b/twitch/urls.py
index a0de088..14e392f 100644
--- a/twitch/urls.py
+++ b/twitch/urls.py
@@ -12,5 +12,8 @@ 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"),
+ path("games//subscribe/", views.subscribe_game_notifications, name="subscribe_notifications"),
+ path("organizations/", views.OrgListView.as_view(), name="org_list"),
+ path("organizations//", views.OrgDetailView.as_view(), name="org_detail"),
+ path("organizations//subscribe/", views.subscribe_org_notifications, name="subscribe_org_notifications"),
]
diff --git a/twitch/views.py b/twitch/views.py
index 94afb42..8c8e05f 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -8,6 +8,7 @@ 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.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.generic import DetailView, ListView
@@ -24,6 +25,37 @@ if TYPE_CHECKING:
logger: logging.Logger = logging.getLogger(__name__)
+class OrgListView(ListView):
+ """List view for organization."""
+
+ model = Organization
+ template_name = "twitch/org_list.html"
+ context_object_name = "orgs"
+
+
+class OrgDetailView(DetailView):
+ """Detail view for organization."""
+
+ model = Organization
+ template_name = "twitch/organization_detail.html"
+ context_object_name = "organization"
+
+ def get_context_data(self, **kwargs) -> dict[str, Any]:
+ """Add additional context data.
+
+ Args:
+ **kwargs: Additional arguments.
+
+ Returns:
+ dict: Context data.
+ """
+ organization: Organization = self.object
+ context = super().get_context_data(**kwargs)
+ games = Game.objects.filter(drop_campaigns__owner=organization).distinct()
+ context["games"] = games
+ return context
+
+
class DropCampaignListView(ListView):
"""List view for drop campaigns."""
@@ -318,8 +350,8 @@ def dashboard(request: HttpRequest) -> HttpResponse:
@login_required
-def subscribe_notifications(request: HttpRequest, game_id: str) -> HttpResponseRedirect:
- """Update notification for a user.
+def subscribe_game_notifications(request: HttpRequest, game_id: str) -> HttpResponseRedirect:
+ """Update Game notification for a user.
Args:
request: The HTTP request.
@@ -338,22 +370,67 @@ def subscribe_notifications(request: HttpRequest, game_id: str) -> HttpResponseR
changes = []
if not created:
if subscription.notify_found != notify_found:
- changes.append(f"Notify when drop is found: {'enabled' if notify_found else 'disabled'}")
+ changes.append(f"{'Enabled' if notify_found else 'Disabled'} notification when drop is found")
if subscription.notify_live != notify_live:
- changes.append(f"Notify when drop is farmable: {'enabled' if notify_live else 'disabled'}")
+ changes.append(f"{'Enabled' if notify_live else 'Disabled'} notification when drop is farmable")
subscription.notify_found = notify_found
subscription.notify_live = notify_live
subscription.save()
if created:
- message = "You have subscribed to notifications for this game."
+ message = f"You have subscribed to notifications for {game.display_name}"
elif changes:
- message = "Updated notification preferences: " + ", ".join(changes)
+ message = "\n".join(changes)
else:
- message = "No changes were made to your notification preferences."
+ message = ""
messages.success(request, message)
return redirect("twitch:game_detail", pk=game.id)
+ messages.warning(request, "Only POST is available for this view.")
return redirect("twitch:game_detail", pk=game.id)
+
+
+@login_required
+def subscribe_org_notifications(request: HttpRequest, org_id: str) -> HttpResponseRedirect:
+ """Update Organization notification for a user.
+
+ Args:
+ request: The HTTP request.
+ org_id: The org we are updating.
+
+ Returns:
+ Redirect back to the twitch:organization_detail.
+ """
+ organization: Organization = get_object_or_404(Organization, pk=org_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, organization=organization)
+
+ changes = []
+ if not created:
+ if subscription.notify_found != notify_found:
+ changes.append(f"{'Enabled' if notify_found else 'Disabled'} notification when drop is found")
+ if subscription.notify_live != notify_live:
+ changes.append(f"{'Enabled' if notify_live else 'Disabled'} notification when drop is farmable")
+
+ subscription.notify_found = notify_found
+ subscription.notify_live = notify_live
+ subscription.save()
+
+ if created:
+ message = f"You have subscribed to notifications for this {organization.name}"
+ elif changes:
+ message = "\n".join(changes)
+ else:
+ message = ""
+
+ messages.success(request, message)
+ return redirect("organization_detail", org_id=organization.id)
+
+ messages.warning(request, "Only POST is available for this view.")
+ return redirect("organization_detail", org_id=organization.id)