Implement Chzzk campaign management features, including models, views, and templates
Some checks failed
Deploy to Server / deploy (push) Failing after 9s
Some checks failed
Deploy to Server / deploy (push) Failing after 9s
This commit is contained in:
parent
677aedf42b
commit
9ce324fd2d
12 changed files with 594 additions and 164 deletions
|
|
@ -4,7 +4,6 @@ from typing import Any
|
|||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
from chzzk.schemas import ChzzkCampaignV1
|
||||
from chzzk.schemas import ChzzkCampaignV2
|
||||
|
||||
|
||||
|
|
@ -16,7 +15,6 @@ from django.utils import timezone
|
|||
|
||||
from chzzk.models import ChzzkCampaign
|
||||
from chzzk.models import ChzzkReward
|
||||
from chzzk.schemas import ChzzkApiResponseV1
|
||||
from chzzk.schemas import ChzzkApiResponseV2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -44,94 +42,87 @@ class Command(BaseCommand):
|
|||
def handle(self, **options) -> None:
|
||||
"""Main handler for the management command. Fetches campaign data from both API versions, validates, and stores them."""
|
||||
campaign_no: int = int(options["campaign_no"])
|
||||
for api_version, url_template in CHZZK_API_URLS:
|
||||
url: str = url_template.format(campaign_no=campaign_no)
|
||||
resp: requests.Response = requests.get(
|
||||
url,
|
||||
timeout=2,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
api_version: str = "v2" # TODO(TheLovinator): Add support for v1 API # noqa: TD003
|
||||
url: str = f"https://api.chzzk.naver.com/service/{api_version}/drops/campaigns/{campaign_no}"
|
||||
resp: requests.Response = requests.get(
|
||||
url,
|
||||
timeout=2,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
|
||||
campaign_data: ChzzkCampaignV2
|
||||
campaign_data = ChzzkApiResponseV2.model_validate(data).content
|
||||
|
||||
# Prepare raw JSON defaults for both API versions so DB inserts won't fail
|
||||
raw_json_v1_val = data if api_version == "v1" else {}
|
||||
raw_json_v2_val = data if api_version == "v2" else {}
|
||||
|
||||
# Save campaign
|
||||
campaign_obj, created = ChzzkCampaign.objects.update_or_create(
|
||||
campaign_no=campaign_data.campaign_no,
|
||||
defaults={
|
||||
"title": campaign_data.title,
|
||||
"image_url": campaign_data.image_url,
|
||||
"description": campaign_data.description,
|
||||
"category_type": campaign_data.category_type,
|
||||
"category_id": campaign_data.category_id,
|
||||
"category_value": campaign_data.category_value,
|
||||
"pc_link_url": campaign_data.pc_link_url,
|
||||
"mobile_link_url": campaign_data.mobile_link_url,
|
||||
"service_id": campaign_data.service_id,
|
||||
"state": campaign_data.state,
|
||||
"start_date": campaign_data.start_date,
|
||||
"end_date": campaign_data.end_date,
|
||||
"has_ios_based_reward": campaign_data.has_ios_based_reward,
|
||||
"drops_campaign_not_started": campaign_data.drops_campaign_not_started,
|
||||
"campaign_reward_type": getattr(
|
||||
campaign_data,
|
||||
"campaign_reward_type",
|
||||
"",
|
||||
),
|
||||
"reward_type": getattr(campaign_data, "reward_type", ""),
|
||||
"account_link_url": campaign_data.account_link_url,
|
||||
"scraped_at": timezone.now(),
|
||||
"scrape_status": "success",
|
||||
"raw_json_v1": raw_json_v1_val,
|
||||
"raw_json_v2": raw_json_v2_val,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created campaign {campaign_no}"),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
|
||||
campaign_data: ChzzkCampaignV1 | ChzzkCampaignV2
|
||||
if api_version == "v1":
|
||||
campaign_data = ChzzkApiResponseV1.model_validate(data).content
|
||||
elif api_version == "v2":
|
||||
campaign_data = ChzzkApiResponseV2.model_validate(data).content
|
||||
else:
|
||||
msg: str = f"Unknown API version: {api_version}"
|
||||
self.stdout.write(self.style.ERROR(msg))
|
||||
continue
|
||||
|
||||
# Save campaign
|
||||
campaign_obj, created = ChzzkCampaign.objects.update_or_create(
|
||||
campaign_no=campaign_data.campaign_no,
|
||||
source_api=api_version,
|
||||
for reward in campaign_data.reward_list:
|
||||
reward_, created = ChzzkReward.objects.update_or_create(
|
||||
campaign=campaign_obj,
|
||||
reward_no=reward.reward_no,
|
||||
defaults={
|
||||
"title": campaign_data.title,
|
||||
"image_url": campaign_data.image_url,
|
||||
"description": campaign_data.description,
|
||||
"category_type": campaign_data.category_type,
|
||||
"category_id": campaign_data.category_id,
|
||||
"category_value": campaign_data.category_value,
|
||||
"pc_link_url": campaign_data.pc_link_url,
|
||||
"mobile_link_url": campaign_data.mobile_link_url,
|
||||
"service_id": campaign_data.service_id,
|
||||
"state": campaign_data.state,
|
||||
"start_date": campaign_data.start_date,
|
||||
"end_date": campaign_data.end_date,
|
||||
"has_ios_based_reward": campaign_data.has_ios_based_reward,
|
||||
"drops_campaign_not_started": campaign_data.drops_campaign_not_started,
|
||||
"image_url": reward.image_url,
|
||||
"title": reward.title,
|
||||
"reward_type": reward.reward_type,
|
||||
"campaign_reward_type": getattr(
|
||||
campaign_data,
|
||||
reward,
|
||||
"campaign_reward_type",
|
||||
"",
|
||||
),
|
||||
"reward_type": getattr(campaign_data, "reward_type", ""),
|
||||
"account_link_url": campaign_data.account_link_url,
|
||||
"scraped_at": timezone.now(),
|
||||
"scrape_status": "success",
|
||||
"raw_json": data,
|
||||
"condition_type": reward.condition_type,
|
||||
"condition_for_minutes": reward.condition_for_minutes,
|
||||
"ios_based_reward": reward.ios_based_reward,
|
||||
"code_remaining_count": reward.code_remaining_count,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Created campaign {campaign_no} from {api_version}",
|
||||
f" Created reward {reward_.reward_no} for campaign {campaign_no}",
|
||||
),
|
||||
)
|
||||
for reward in campaign_data.reward_list:
|
||||
reward_, created = ChzzkReward.objects.update_or_create(
|
||||
campaign=campaign_obj,
|
||||
reward_no=reward.reward_no,
|
||||
defaults={
|
||||
"image_url": reward.image_url,
|
||||
"title": reward.title,
|
||||
"reward_type": reward.reward_type,
|
||||
"campaign_reward_type": getattr(
|
||||
reward,
|
||||
"campaign_reward_type",
|
||||
"",
|
||||
),
|
||||
"condition_type": reward.condition_type,
|
||||
"condition_for_minutes": reward.condition_for_minutes,
|
||||
"ios_based_reward": reward.ios_based_reward,
|
||||
"code_remaining_count": reward.code_remaining_count,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f" Created reward {reward_.reward_no} for campaign {campaign_no}",
|
||||
),
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Imported campaign {campaign_no} from {api_version}",
|
||||
),
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Imported campaign {campaign_no}"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 6.0.3 on 2026-03-31 19:33
|
||||
# Generated by Django 6.0.3 on 2026-04-01 01:57
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
|
@ -7,7 +7,7 @@ from django.db import models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Initial migration for ChzzkCampaign and ChzzkReward models."""
|
||||
"""Initial migration for chzzk app, creating ChzzkCampaign and ChzzkReward models."""
|
||||
|
||||
initial = True
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("campaign_no", models.BigIntegerField(unique=True)),
|
||||
("campaign_no", models.BigIntegerField()),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("image_url", models.URLField()),
|
||||
("description", models.TextField()),
|
||||
|
|
@ -53,11 +53,11 @@ class Migration(migrations.Migration):
|
|||
("scraped_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("source_api", models.CharField(max_length=16)),
|
||||
("scrape_status", models.CharField(default="success", max_length=32)),
|
||||
("raw_json", models.JSONField()),
|
||||
("raw_json_v1", models.JSONField(blank=True, null=True)),
|
||||
("raw_json_v2", models.JSONField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-start_date"],
|
||||
"unique_together": {("campaign_no", "source_api")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 6.0.3 on 2026-03-31 19:53
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Alter campaign_no field in ChzzkCampaign to remove unique constraint."""
|
||||
|
||||
dependencies = [
|
||||
("chzzk", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="chzzkcampaign",
|
||||
name="campaign_no",
|
||||
field=models.BigIntegerField(),
|
||||
),
|
||||
]
|
||||
|
|
@ -26,12 +26,12 @@ class ChzzkCampaign(models.Model):
|
|||
|
||||
# Scraping metadata
|
||||
scraped_at = models.DateTimeField(default=timezone.now)
|
||||
source_api = models.CharField(max_length=16) # 'v1' or 'v2'
|
||||
source_api = models.CharField(max_length=16)
|
||||
scrape_status = models.CharField(max_length=32, default="success")
|
||||
raw_json = models.JSONField()
|
||||
raw_json_v1 = models.JSONField(null=True, blank=True)
|
||||
raw_json_v2 = models.JSONField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("campaign_no", "source_api")
|
||||
ordering = ["-start_date"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -4,26 +4,8 @@ from pydantic import BaseModel
|
|||
from pydantic import Field
|
||||
|
||||
|
||||
class ChzzkRewardV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 reward object."""
|
||||
|
||||
title: str
|
||||
reward_no: int = Field(..., alias="rewardNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
reward_type: str = Field(..., alias="rewardType")
|
||||
condition_type: str = Field(..., alias="conditionType")
|
||||
condition_for_minutes: int = Field(..., alias="conditionForMinutes")
|
||||
ios_based_reward: bool = Field(..., alias="iosBasedReward")
|
||||
code_remaining_count: int = Field(..., alias="codeRemainingCount")
|
||||
|
||||
# Only in v1 API
|
||||
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkRewardV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 reward object."""
|
||||
"""Pydantic schema for api v2 reward object."""
|
||||
|
||||
title: str
|
||||
reward_no: int = Field(..., alias="rewardNo")
|
||||
|
|
@ -37,39 +19,14 @@ class ChzzkRewardV2(BaseModel):
|
|||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkCampaignV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 campaign object."""
|
||||
|
||||
title: str
|
||||
state: str
|
||||
description: str
|
||||
campaign_no: int = Field(..., alias="campaignNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
category_type: str = Field(..., alias="categoryType")
|
||||
category_id: str = Field(..., alias="categoryId")
|
||||
category_value: str = Field(..., alias="categoryValue")
|
||||
pc_link_url: str = Field(..., alias="pcLinkUrl")
|
||||
mobile_link_url: str = Field(..., alias="mobileLinkUrl")
|
||||
service_id: str = Field(..., alias="serviceId")
|
||||
start_date: str = Field(..., alias="startDate")
|
||||
end_date: str = Field(..., alias="endDate")
|
||||
reward_list: list[ChzzkRewardV1] = Field(..., alias="rewardList")
|
||||
has_ios_based_reward: bool = Field(..., alias="hasIosBasedReward")
|
||||
drops_campaign_not_started: bool = Field(..., alias="dropsCampaignNotStarted")
|
||||
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
|
||||
account_link_url: str = Field(..., alias="accountLinkUrl")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkCampaignV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 campaign object."""
|
||||
"""Pydantic schema for api v2 campaign object."""
|
||||
|
||||
title: str
|
||||
state: str
|
||||
description: str
|
||||
campaign_no: int = Field(..., alias="campaignNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
description: str
|
||||
category_type: str = Field(..., alias="categoryType")
|
||||
category_id: str = Field(..., alias="categoryId")
|
||||
category_value: str = Field(..., alias="categoryValue")
|
||||
|
|
@ -87,18 +44,8 @@ class ChzzkCampaignV2(BaseModel):
|
|||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkApiResponseV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 API response."""
|
||||
|
||||
code: int
|
||||
message: Any | None
|
||||
content: ChzzkCampaignV1
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkApiResponseV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 API response."""
|
||||
"""Pydantic schema for api v2 API response."""
|
||||
|
||||
code: int
|
||||
message: Any | None
|
||||
|
|
|
|||
49
chzzk/urls.py
Normal file
49
chzzk/urls.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from chzzk import views
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.urls.resolvers import URLPattern
|
||||
|
||||
app_name = "chzzk"
|
||||
|
||||
urlpatterns: list[URLPattern] = [
|
||||
# /chzzk/
|
||||
path(
|
||||
"",
|
||||
views.dashboard_view,
|
||||
name="dashboard",
|
||||
),
|
||||
# /chzzk/campaigns/
|
||||
path(
|
||||
"campaigns/",
|
||||
views.CampaignListView.as_view(),
|
||||
name="campaign_list",
|
||||
),
|
||||
# /chzzk/campaigns/<campaign_no>/
|
||||
path(
|
||||
"campaigns/<int:campaign_no>/",
|
||||
views.campaign_detail_view,
|
||||
name="campaign_detail",
|
||||
),
|
||||
# /chzzk/rss/campaigns
|
||||
path(
|
||||
"rss/campaigns",
|
||||
views.ChzzkCampaignFeed(),
|
||||
name="campaign_feed",
|
||||
),
|
||||
# /chzzk/atom/campaigns
|
||||
path(
|
||||
"atom/campaigns",
|
||||
views.ChzzkCampaignFeed(),
|
||||
name="campaign_feed_atom",
|
||||
),
|
||||
# /chzzk/discord/campaigns
|
||||
path(
|
||||
"discord/campaigns",
|
||||
views.ChzzkCampaignFeed(),
|
||||
name="campaign_feed_discord",
|
||||
),
|
||||
]
|
||||
206
chzzk/views.py
206
chzzk/views.py
|
|
@ -0,0 +1,206 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import SafeText
|
||||
from django.views import generic
|
||||
|
||||
from chzzk import models
|
||||
from twitch.feeds import TTVDropsBaseFeed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http.request import HttpRequest
|
||||
from pytest_django.asserts import QuerySet
|
||||
|
||||
|
||||
def dashboard_view(request: HttpRequest) -> HttpResponse:
|
||||
"""View function for the dashboard page showing all the active chzzk campaigns.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The HTTP response containing the rendered dashboard page.
|
||||
"""
|
||||
active_campaigns: QuerySet[models.ChzzkCampaign, models.ChzzkCampaign] = (
|
||||
models.ChzzkCampaign.objects.filter(end_date__gte=timezone.now()).order_by(
|
||||
"-start_date",
|
||||
)
|
||||
)
|
||||
return render(
|
||||
request=request,
|
||||
template_name="chzzk/dashboard.html",
|
||||
context={
|
||||
"active_campaigns": active_campaigns,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CampaignListView(generic.ListView):
|
||||
"""List view showing all chzzk campaigns."""
|
||||
|
||||
model = models.ChzzkCampaign
|
||||
template_name = "chzzk/campaign_list.html"
|
||||
context_object_name = "campaigns"
|
||||
paginate_by = 25
|
||||
|
||||
|
||||
def campaign_detail_view(request: HttpRequest, campaign_no: int) -> HttpResponse:
|
||||
"""View function for the campaign detail page showing information about a single chzzk campaign.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming HTTP request.
|
||||
campaign_no (int): The campaign number of the campaign to display.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The HTTP response containing the rendered campaign detail page.
|
||||
"""
|
||||
campaign: models.ChzzkCampaign = models.ChzzkCampaign.objects.get(
|
||||
campaign_no=campaign_no,
|
||||
)
|
||||
rewards: QuerySet[models.ChzzkReward, models.ChzzkReward] = campaign.rewards.all() # pyright: ignore[reportAttributeAccessIssue]
|
||||
return render(
|
||||
request=request,
|
||||
template_name="chzzk/campaign_detail.html",
|
||||
context={
|
||||
"campaign": campaign,
|
||||
"rewards": rewards,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ChzzkCampaignFeed(TTVDropsBaseFeed):
|
||||
"""RSS feed for the latest chzzk campaigns."""
|
||||
|
||||
title: str = "chzzk campaigns"
|
||||
link: str = "/chzzk/campaigns/"
|
||||
description: str = "Latest chzzk campaigns"
|
||||
|
||||
_limit: int | None = None
|
||||
|
||||
def __call__(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Allow an optional 'limit' query parameter to specify the number of items in the feed.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming HTTP request.
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The HTTP response containing the feed.
|
||||
"""
|
||||
if request.GET.get("limit"):
|
||||
try:
|
||||
self._limit = int(request.GET.get("limit", 50))
|
||||
except ValueError, TypeError:
|
||||
self._limit = None
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
def items(self) -> QuerySet[models.ChzzkCampaign, models.ChzzkCampaign]:
|
||||
"""Return the latest chzzk campaigns with raw_json data, ordered by start date.
|
||||
|
||||
Returns:
|
||||
QuerySet: A queryset of ChzzkCampaign objects.
|
||||
"""
|
||||
limit: int = self._limit if self._limit is not None else 50
|
||||
return models.ChzzkCampaign.objects.filter(raw_json__isnull=False).order_by(
|
||||
"-start_date",
|
||||
)[:limit]
|
||||
|
||||
def item_title(self, item: models.ChzzkCampaign) -> str:
|
||||
"""Return the title of the feed item, which is the campaign title.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
str: The title of the feed item.
|
||||
"""
|
||||
return item.title
|
||||
|
||||
def item_description(self, item: models.ChzzkCampaign) -> SafeText:
|
||||
"""Return the description of the feed item, which includes the campaign image and description.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
SafeText: The HTML description of the feed item.
|
||||
"""
|
||||
parts: list[SafeText] = []
|
||||
if getattr(item, "image_url", ""):
|
||||
parts.append(
|
||||
format_html(
|
||||
'<img src="{}" alt="{}" width="320" height="180" />',
|
||||
item.image_url,
|
||||
item.title,
|
||||
),
|
||||
)
|
||||
if getattr(item, "description", ""):
|
||||
parts.append(format_html("<p>{}</p>", item.description))
|
||||
|
||||
# Link back to the PC detail URL when available
|
||||
if getattr(item, "pc_link_url", ""):
|
||||
parts.append(
|
||||
format_html('<p><a href="{}">Details</a></p>', item.pc_link_url),
|
||||
)
|
||||
|
||||
return SafeText("".join(str(p) for p in parts))
|
||||
|
||||
def item_link(self, item: models.ChzzkCampaign) -> str:
|
||||
"""Return the URL for the feed item, which is the campaign detail page.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
str: The URL for the feed item.
|
||||
"""
|
||||
return reverse("chzzk:campaign_detail", args=[item.pk])
|
||||
|
||||
def item_pubdate(self, item: models.ChzzkCampaign) -> datetime.datetime:
|
||||
"""Return the publication date of the feed item, which is the campaign start date.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
datetime.datetime: The publication date of the feed item.
|
||||
"""
|
||||
return getattr(item, "start_date", timezone.now()) or timezone.now()
|
||||
|
||||
def item_updateddate(self, item: models.ChzzkCampaign) -> datetime.datetime:
|
||||
"""Return the last updated date of the feed item, which is the campaign scraped date.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
datetime.datetime: The last updated date of the feed item.
|
||||
"""
|
||||
return getattr(item, "scraped_at", timezone.now()) or timezone.now()
|
||||
|
||||
def item_author_name(self, item: models.ChzzkCampaign) -> str:
|
||||
"""Return the author name for the feed item. Since we don't have a specific author, return a default value.
|
||||
|
||||
Args:
|
||||
item (ChzzkCampaign): The campaign object for the feed item.
|
||||
|
||||
Returns:
|
||||
str: The author name for the feed item.
|
||||
"""
|
||||
return item.category_id or "Unknown Category"
|
||||
|
||||
def feed_url(self) -> str:
|
||||
"""Return the URL of the feed itself.
|
||||
|
||||
Returns:
|
||||
str: The URL of the feed.
|
||||
"""
|
||||
return reverse("chzzk:campaign_feed")
|
||||
Loading…
Add table
Add a link
Reference in a new issue