diff --git a/chzzk/management/commands/import_chzzk_campaign_range.py b/chzzk/management/commands/import_chzzk_campaign_range.py new file mode 100644 index 0000000..178e141 --- /dev/null +++ b/chzzk/management/commands/import_chzzk_campaign_range.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING + +from django.core.management import BaseCommand +from django.core.management import CommandError +from django.core.management import call_command + +if TYPE_CHECKING: + import argparse + + +class Command(BaseCommand): + """Django management command to import a range of Chzzk campaigns by calling import_chzzk_campaign repeatedly.""" + + help = ( + "Import a range of Chzzk campaigns by calling import_chzzk_campaign repeatedly." + ) + + def add_arguments(self, parser: argparse.ArgumentParser) -> None: + """Add command-line arguments for the management command.""" + parser.add_argument("start", type=int, help="Starting campaign number") + parser.add_argument("end", type=int, help="Ending campaign number (inclusive)") + parser.add_argument( + "--step", + type=int, + default=None, + help="Step to move between numbers (default -1 for descending, 1 for ascending)", + ) + + def handle(self, **options) -> None: + """Main handler for the management command. Calls import_chzzk_campaign for each campaign number in the specified range. + + Raises: + ValueError: If step is 0. + """ + start: int = options["start"] + end: int = options["end"] + step: int | None = options["step"] + + if step is None: + step = -1 if start > end else 1 + + if step == 0: + msg = "Step cannot be 0" + raise ValueError(msg) + + range_end: int = end + 1 if step > 0 else end - 1 + + msg: str = f"Importing campaigns from {start} to {end} with step {step}" + self.stdout.write(self.style.SUCCESS(msg)) + + for campaign_no in range(start, range_end, step): + self.stdout.write(f"Importing campaign {campaign_no}...") + try: + call_command("import_chzzk_campaign", str(campaign_no)) + except CommandError as exc: + msg = f"Failed campaign {campaign_no}: {exc}" + self.stdout.write(self.style.ERROR(msg)) + + self.stdout.write(self.style.SUCCESS("Batch import complete.")) diff --git a/chzzk/tests/test_views.py b/chzzk/tests/test_views.py index 4e036f9..465e959 100644 --- a/chzzk/tests/test_views.py +++ b/chzzk/tests/test_views.py @@ -60,3 +60,46 @@ class ChzzkDashboardViewTests(TestCase): assert included in campaigns assert all(c.state != "TESTING" for c in campaigns) + + def test_campaign_detail_view_renders_chzzk_campaign_and_rewards(self) -> None: + """Test that the campaign detail view correctly renders the details of a chzzk campaign and its rewards.""" + now: datetime = timezone.now() + + campaign: ChzzkCampaign = ChzzkCampaign.objects.create( + campaign_no=2001, + title="Campaign Detail Test", + description="Detailed campaign description", + category_type="game", + category_id="1", + category_value="TestGame", + service_id="chzzk", + state="ACTIVE", + start_date=now - timedelta(days=1), + end_date=now + timedelta(days=1), + has_ios_based_reward=False, + drops_campaign_not_started=False, + source_api="unit-test", + ) + + campaign.rewards.create( # pyright: ignore[reportAttributeAccessIssue] + reward_no=10, + title="Reward A", + reward_type="ITEM", + campaign_reward_type="Standard", + condition_type="watch", + condition_for_minutes=15, + ios_based_reward=False, + code_remaining_count=100, + ) + + response: _MonkeyPatchedWSGIResponse = self.client.get( + reverse("chzzk:campaign_detail", args=[campaign.campaign_no]), + ) + assert response.status_code == 200 + + content: str = response.content.decode() + assert campaign.title in content + assert "Reward A" in content + assert "watch" in content + assert "100" in content + assert "No" in content # ios_based_reward=False diff --git a/chzzk/views.py b/chzzk/views.py index 7687c43..3cb332b 100644 --- a/chzzk/views.py +++ b/chzzk/views.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from django.db.models.query import QuerySet +from django.shortcuts import get_object_or_404 from django.shortcuts import render from django.urls import reverse from django.utils import timezone @@ -62,7 +63,8 @@ def campaign_detail_view(request: HttpRequest, campaign_no: int) -> HttpResponse Returns: HttpResponse: The HTTP response containing the rendered campaign detail page. """ - campaign: models.ChzzkCampaign = models.ChzzkCampaign.objects.get( + campaign: models.ChzzkCampaign = get_object_or_404( + models.ChzzkCampaign, campaign_no=campaign_no, ) rewards: QuerySet[models.ChzzkReward, models.ChzzkReward] = campaign.rewards.all() # pyright: ignore[reportAttributeAccessIssue] diff --git a/templates/chzzk/campaign_detail.html b/templates/chzzk/campaign_detail.html index 7a38c83..51a36d5 100644 --- a/templates/chzzk/campaign_detail.html +++ b/templates/chzzk/campaign_detail.html @@ -1,52 +1,161 @@ {% extends "base.html" %} +{% load static %} +{% load image_tags %} {% block title %} {{ campaign.title }} {% endblock title %} +{% block extra_head %} + + + +{% endblock extra_head %} {% block content %} -
-

{{ campaign.title }}

- - {% if campaign.image_url %} - {{ campaign.title }} - {% endif %} - {% if campaign.description %} -
- {{ campaign.description|linebreaksbr }} -
- {% endif %} -
- {% if campaign.starts_at %} -
- Starts: -
+
+ +
+ {% if campaign.image_url %} + {{ campaign.title }} {% endif %} - {% if campaign.ends_at %} -
- Ends: -
- {% endif %} -
-
-

Rewards

- {% if rewards %} - - {% else %} -

No rewards available.

- {% endif %} - {% if campaign.external_url %} -

- View on chzzk -

- {% endif %} -
+ + +
+

{{ campaign.title }}

+ +
+ chzzk > Campaigns > {{ campaign.title }} +
+ +

{{ campaign.description|linebreaksbr }}

+
+ Published + {% if campaign.scraped_at %} + ({{ campaign.scraped_at|timesince }} ago) + {% elif campaign.start_date %} + ({{ campaign.start_date|timesince }} ago) + {% else %} + unknown + {% endif %} +
+
+ Last updated + {% if campaign.scraped_at %} + ({{ campaign.scraped_at|timesince }} ago) + {% elif campaign.updated_at %} + ({{ campaign.updated_at|timesince }} ago) + {% else %} + unknown + {% endif %} +
+ +
+ {% if campaign.end_date %} + {% if campaign.end_date < now %} + Ended + ({{ campaign.end_date|timesince }} ago) + {% else %} + Ends in + (in {{ campaign.end_date|timeuntil }}) + {% endif %} + {% else %} + Ends unknown + {% endif %} +
+ +
+ {% if campaign.start_date %} + {% if campaign.start_date > now %} + Starts in + (in {{ campaign.start_date|timeuntil }}) + {% else %} + Started + ({{ campaign.start_date|timesince }} ago) + {% endif %} + {% else %} + Starts unknown + {% endif %} +
+ +
+ Duration is + {% if campaign.start_date and campaign.end_date %} + + {% else %} + unknown + {% endif %} +
+ +
+ {% if campaign.pc_link_url %}[details]{% endif %} + {% if campaign.account_link_url %}[connect]{% endif %} +
+
+ +
Campaign Info
+ {% if rewards %} + + + + + + + + + + + + + {% for reward in rewards %} + + + + + + + {% if reward.reward_type == "CODE_FIRST_COME" %} + + {% else %} + + {% endif %} + + {% endfor %} + +
ImageRewardTypeConditioniOS RewardCodes Left
+ {% if reward.image_url %} + {{ reward.title }} + {% endif %} + + {{ reward.title }} + {% if reward.condition_for_minutes %} +
{{ reward.condition_for_minutes }} minutes watched
+ {% endif %} +
{{ reward.reward_type }}{{ reward.condition_type }}{{ reward.ios_based_reward|yesno:"Yes,No" }}{{ reward.code_remaining_count }}Unlimited
+ {% else %} +

No rewards available for this campaign.

+ {% endif %} {% endblock content %}