From 4af2b02a01c40251098f77f0ddbd0e4e11b8aa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Sat, 2 Aug 2025 05:45:20 +0200 Subject: [PATCH] Allow subscribe to orgs --- pyproject.toml | 1 + templates/base.html | 3 +- templates/twitch/campaign_detail.html | 2 +- templates/twitch/game_list.html | 4 +- templates/twitch/org_list.html | 18 ++++ templates/twitch/organization_detail.html | 37 ++++++++ ...onsubscription_unique_together_and_more.py | 34 +++++++ twitch/models.py | 9 +- twitch/urls.py | 5 +- twitch/views.py | 91 +++++++++++++++++-- 10 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 templates/twitch/org_list.html create mode 100644 templates/twitch/organization_detail.html create mode 100644 twitch/migrations/0003_alter_notificationsubscription_unique_together_and_more.py 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 %} +
    + {% csrf_token %} +
    + + +
    +
    + + +
    + +
    + {% else %} + Login to subscribe! + {% endif %} + +{% 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)