Compare commits
No commits in common. "02ea6314c35a5ca3fef8240870619b9e893b1afc" and "28cd62b161e9f20836374f82f0c67fe1eb10776f" have entirely different histories.
02ea6314c3
...
28cd62b161
4 changed files with 151 additions and 476 deletions
|
|
@ -13,36 +13,6 @@ if TYPE_CHECKING:
|
|||
|
||||
urlpatterns: list[URLPattern | URLResolver] = [
|
||||
path(route="sitemap.xml", view=core_views.sitemap_view, name="sitemap"),
|
||||
path(
|
||||
route="sitemap-static.xml",
|
||||
view=core_views.sitemap_static_view,
|
||||
name="sitemap-static",
|
||||
),
|
||||
path(
|
||||
route="sitemap-twitch-channels.xml",
|
||||
view=core_views.sitemap_twitch_channels_view,
|
||||
name="sitemap-twitch-channels",
|
||||
),
|
||||
path(
|
||||
route="sitemap-twitch-drops.xml",
|
||||
view=core_views.sitemap_twitch_drops_view,
|
||||
name="sitemap-twitch-drops",
|
||||
),
|
||||
path(
|
||||
route="sitemap-twitch-others.xml",
|
||||
view=core_views.sitemap_twitch_others_view,
|
||||
name="sitemap-twitch-others",
|
||||
),
|
||||
path(
|
||||
route="sitemap-kick.xml",
|
||||
view=core_views.sitemap_kick_view,
|
||||
name="sitemap-kick",
|
||||
),
|
||||
path(
|
||||
route="sitemap-youtube.xml",
|
||||
view=core_views.sitemap_youtube_view,
|
||||
name="sitemap-youtube",
|
||||
),
|
||||
# Core app
|
||||
path(route="", view=include("core.urls", namespace="core")),
|
||||
# Twitch app
|
||||
|
|
|
|||
423
core/views.py
423
core/views.py
|
|
@ -11,7 +11,6 @@ from django.db import connection
|
|||
from django.db.models import Count
|
||||
from django.db.models import Exists
|
||||
from django.db.models import F
|
||||
from django.db.models import Max
|
||||
from django.db.models import OuterRef
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Q
|
||||
|
|
@ -120,357 +119,143 @@ def _build_seo_context( # noqa: PLR0913, PLR0917
|
|||
return context
|
||||
|
||||
|
||||
def _render_urlset_xml(
|
||||
url_entries: list[dict[str, str | dict[str, str]]],
|
||||
) -> str:
|
||||
"""Render a <urlset> sitemap XML string from URL entries.
|
||||
|
||||
Args:
|
||||
url_entries: List of dictionaries containing URL entry data.
|
||||
|
||||
Returns:
|
||||
A string containing the rendered XML.
|
||||
|
||||
"""
|
||||
xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||
for url_entry in url_entries:
|
||||
xml += " <url>\n"
|
||||
xml += f" <loc>{url_entry['url']}</loc>\n"
|
||||
if url_entry.get("lastmod"):
|
||||
xml += f" <lastmod>{url_entry['lastmod']}</lastmod>\n"
|
||||
xml += " </url>\n"
|
||||
xml += "</urlset>"
|
||||
return xml
|
||||
|
||||
|
||||
def _render_sitemap_index_xml(sitemap_entries: list[dict[str, str]]) -> str:
|
||||
"""Render a <sitemapindex> XML string listing sitemap URLs.
|
||||
|
||||
Args:
|
||||
sitemap_entries: List of dictionaries with "loc" and optional "lastmod".
|
||||
|
||||
Returns:
|
||||
A string containing the rendered XML.
|
||||
"""
|
||||
xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
xml += '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||
for entry in sitemap_entries:
|
||||
xml += " <sitemap>\n"
|
||||
xml += f" <loc>{entry['loc']}</loc>\n"
|
||||
if entry.get("lastmod"):
|
||||
xml += f" <lastmod>{entry['lastmod']}</lastmod>\n"
|
||||
xml += " </sitemap>\n"
|
||||
xml += "</sitemapindex>"
|
||||
return xml
|
||||
|
||||
|
||||
def _build_base_url(request: HttpRequest) -> str:
|
||||
"""Return base url including scheme and host."""
|
||||
return f"{request.scheme}://{request.get_host()}"
|
||||
|
||||
|
||||
# MARK: /sitemap.xml
|
||||
def sitemap_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap index pointing to per-section sitemap files.
|
||||
def sitemap_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0915
|
||||
"""Generate a dynamic XML sitemap for search engines.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap index XML.
|
||||
HttpResponse: XML sitemap.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
base_url: str = f"{request.scheme}://{request.get_host()}"
|
||||
|
||||
# Compute last modified per-section so search engines can more intelligently crawl.
|
||||
# Do not fabricate a lastmod date if the section has no data.
|
||||
twitch_channels_lastmod = Channel.objects.aggregate(max=Max("updated_at"))["max"]
|
||||
twitch_drops_lastmod = max(
|
||||
[
|
||||
dt
|
||||
for dt in [
|
||||
DropCampaign.objects.aggregate(max=Max("updated_at"))["max"],
|
||||
RewardCampaign.objects.aggregate(max=Max("updated_at"))["max"],
|
||||
]
|
||||
if dt is not None
|
||||
]
|
||||
or [None],
|
||||
)
|
||||
twitch_others_lastmod = max(
|
||||
[
|
||||
dt
|
||||
for dt in [
|
||||
Game.objects.aggregate(max=Max("updated_at"))["max"],
|
||||
Organization.objects.aggregate(max=Max("updated_at"))["max"],
|
||||
ChatBadgeSet.objects.aggregate(max=Max("updated_at"))["max"],
|
||||
]
|
||||
if dt is not None
|
||||
]
|
||||
or [None],
|
||||
)
|
||||
kick_lastmod = KickDropCampaign.objects.aggregate(max=Max("updated_at"))["max"]
|
||||
|
||||
sitemap_entries: list[dict[str, str]] = [
|
||||
{"loc": f"{base_url}/sitemap-static.xml"},
|
||||
{"loc": f"{base_url}/sitemap-twitch-channels.xml"},
|
||||
{"loc": f"{base_url}/sitemap-twitch-drops.xml"},
|
||||
{"loc": f"{base_url}/sitemap-twitch-others.xml"},
|
||||
{"loc": f"{base_url}/sitemap-kick.xml"},
|
||||
{"loc": f"{base_url}/sitemap-youtube.xml"},
|
||||
]
|
||||
|
||||
if twitch_channels_lastmod is not None:
|
||||
sitemap_entries[1]["lastmod"] = twitch_channels_lastmod.isoformat()
|
||||
if twitch_drops_lastmod is not None:
|
||||
sitemap_entries[2]["lastmod"] = twitch_drops_lastmod.isoformat()
|
||||
if twitch_others_lastmod is not None:
|
||||
sitemap_entries[3]["lastmod"] = twitch_others_lastmod.isoformat()
|
||||
if kick_lastmod is not None:
|
||||
sitemap_entries[4]["lastmod"] = kick_lastmod.isoformat()
|
||||
|
||||
xml_content: str = _render_sitemap_index_xml(sitemap_entries)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
# MARK: /sitemap-static.xml
|
||||
def sitemap_static_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing the main static pages.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
|
||||
# Include the canonical top-level pages, the main app dashboards, and key list views.
|
||||
# Using reverse() keeps these URLs correct if route patterns change.
|
||||
static_route_names: list[str] = [
|
||||
"core:dashboard",
|
||||
# "core:search",
|
||||
"core:dataset_backups",
|
||||
"core:docs_rss",
|
||||
# Core RSS/Atom feeds
|
||||
# "core:campaign_feed",
|
||||
# "core:game_feed",
|
||||
# "core:organization_feed",
|
||||
# "core:reward_campaign_feed",
|
||||
# "core:campaign_feed_atom",
|
||||
# "core:game_feed_atom",
|
||||
# "core:organization_feed_atom",
|
||||
# "core:reward_campaign_feed_atom",
|
||||
# "core:campaign_feed_discord",
|
||||
# "core:game_feed_discord",
|
||||
# "core:organization_feed_discord",
|
||||
# "core:reward_campaign_feed_discord",
|
||||
# Twitch pages
|
||||
"twitch:dashboard",
|
||||
"twitch:badge_list",
|
||||
"twitch:campaign_list",
|
||||
"twitch:channel_list",
|
||||
"twitch:emote_gallery",
|
||||
"twitch:games_grid",
|
||||
"twitch:games_list",
|
||||
"twitch:org_list",
|
||||
"twitch:reward_campaign_list",
|
||||
# Kick pages
|
||||
"kick:dashboard",
|
||||
"kick:campaign_list",
|
||||
"kick:game_list",
|
||||
"kick:category_list",
|
||||
"kick:organization_list",
|
||||
# Kick RSS/Atom feeds
|
||||
# "kick:campaign_feed",
|
||||
# "kick:game_feed",
|
||||
# "kick:category_feed",
|
||||
# "kick:organization_feed",
|
||||
# "kick:campaign_feed_atom",
|
||||
# "kick:game_feed_atom",
|
||||
# "kick:category_feed_atom",
|
||||
# "kick:organization_feed_atom",
|
||||
# "kick:campaign_feed_discord",
|
||||
# "kick:game_feed_discord",
|
||||
# "kick:category_feed_discord",
|
||||
# "kick:organization_feed_discord",
|
||||
# YouTube
|
||||
"youtube:index",
|
||||
]
|
||||
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = []
|
||||
for route_name in static_route_names:
|
||||
url = reverse(route_name)
|
||||
sitemap_urls.append({"url": f"{base_url}{url}"})
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
# MARK: /sitemap-twitch-channels.xml
|
||||
def sitemap_twitch_channels_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing Twitch channel pages.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
# Start building sitemap XML
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = []
|
||||
|
||||
channels: QuerySet[Channel] = Channel.objects.all()
|
||||
for channel in channels:
|
||||
resource_url: str = reverse("twitch:channel_detail", args=[channel.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {"url": full_url}
|
||||
if channel.updated_at:
|
||||
entry["lastmod"] = channel.updated_at.isoformat()
|
||||
# Static pages
|
||||
sitemap_urls.extend([
|
||||
{"url": f"{base_url}/", "priority": "1.0", "changefreq": "daily"},
|
||||
{"url": f"{base_url}/campaigns/", "priority": "0.9", "changefreq": "daily"},
|
||||
{
|
||||
"url": f"{base_url}/reward-campaigns/",
|
||||
"priority": "0.9",
|
||||
"changefreq": "daily",
|
||||
},
|
||||
{"url": f"{base_url}/games/", "priority": "0.9", "changefreq": "weekly"},
|
||||
{
|
||||
"url": f"{base_url}/organizations/",
|
||||
"priority": "0.8",
|
||||
"changefreq": "weekly",
|
||||
},
|
||||
{"url": f"{base_url}/channels/", "priority": "0.8", "changefreq": "weekly"},
|
||||
{"url": f"{base_url}/badges/", "priority": "0.7", "changefreq": "monthly"},
|
||||
{"url": f"{base_url}/emotes/", "priority": "0.7", "changefreq": "monthly"},
|
||||
{"url": f"{base_url}/search/", "priority": "0.6", "changefreq": "monthly"},
|
||||
])
|
||||
|
||||
# Dynamic detail pages - Games
|
||||
games: QuerySet[Game] = Game.objects.all()
|
||||
for game in games:
|
||||
entry: dict[str, str | dict[str, str]] = {
|
||||
"url": f"{base_url}{reverse('twitch:game_detail', args=[game.twitch_id])}",
|
||||
"priority": "0.8",
|
||||
"changefreq": "weekly",
|
||||
}
|
||||
if game.updated_at:
|
||||
entry["lastmod"] = game.updated_at.isoformat()
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
# MARK: /sitemap-twitch-drops.xml
|
||||
def sitemap_twitch_drops_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing Twitch drop campaign pages.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = []
|
||||
|
||||
# Dynamic detail pages - Campaigns
|
||||
campaigns: QuerySet[DropCampaign] = DropCampaign.objects.all()
|
||||
for campaign in campaigns:
|
||||
resource_url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str] = {"url": full_url}
|
||||
entry: dict[str, str | dict[str, str]] = {
|
||||
"url": full_url,
|
||||
"priority": "0.7",
|
||||
"changefreq": "weekly",
|
||||
}
|
||||
if campaign.updated_at:
|
||||
entry["lastmod"] = campaign.updated_at.isoformat()
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
# Dynamic detail pages - Organizations
|
||||
orgs: QuerySet[Organization] = Organization.objects.all()
|
||||
for org in orgs:
|
||||
resource_url = reverse("twitch:organization_detail", args=[org.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {
|
||||
"url": full_url,
|
||||
"priority": "0.7",
|
||||
"changefreq": "weekly",
|
||||
}
|
||||
if org.updated_at:
|
||||
entry["lastmod"] = org.updated_at.isoformat()
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
# Dynamic detail pages - Channels
|
||||
channels: QuerySet[Channel] = Channel.objects.all()
|
||||
for channel in channels:
|
||||
resource_url = reverse("twitch:channel_detail", args=[channel.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {
|
||||
"url": full_url,
|
||||
"priority": "0.6",
|
||||
"changefreq": "weekly",
|
||||
}
|
||||
if channel.updated_at:
|
||||
entry["lastmod"] = channel.updated_at.isoformat()
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
# Dynamic detail pages - Badges
|
||||
badge_sets: QuerySet[ChatBadgeSet] = ChatBadgeSet.objects.all()
|
||||
for badge_set in badge_sets:
|
||||
resource_url = reverse("twitch:badge_set_detail", args=[badge_set.set_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
sitemap_urls.append({
|
||||
"url": full_url,
|
||||
"priority": "0.5",
|
||||
"changefreq": "monthly",
|
||||
})
|
||||
|
||||
# Dynamic detail pages - Reward Campaigns
|
||||
reward_campaigns: QuerySet[RewardCampaign] = RewardCampaign.objects.all()
|
||||
for reward_campaign in reward_campaigns:
|
||||
resource_url = reverse(
|
||||
"twitch:reward_campaign_detail",
|
||||
args=[reward_campaign.twitch_id],
|
||||
args=[
|
||||
reward_campaign.twitch_id,
|
||||
],
|
||||
)
|
||||
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {"url": full_url}
|
||||
entry: dict[str, str | dict[str, str]] = {
|
||||
"url": full_url,
|
||||
"priority": "0.6",
|
||||
"changefreq": "weekly",
|
||||
}
|
||||
if reward_campaign.updated_at:
|
||||
entry["lastmod"] = reward_campaign.updated_at.isoformat()
|
||||
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
# Build XML
|
||||
xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
xml_content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||
|
||||
for url_entry in sitemap_urls:
|
||||
xml_content += " <url>\n"
|
||||
xml_content += f" <loc>{url_entry['url']}</loc>\n"
|
||||
if url_entry.get("lastmod"):
|
||||
xml_content += f" <lastmod>{url_entry['lastmod']}</lastmod>\n"
|
||||
xml_content += (
|
||||
f" <changefreq>{url_entry.get('changefreq', 'monthly')}</changefreq>\n"
|
||||
)
|
||||
xml_content += f" <priority>{url_entry.get('priority', '0.5')}</priority>\n"
|
||||
xml_content += " </url>\n"
|
||||
|
||||
# MARK: /sitemap-twitch-others.xml
|
||||
def sitemap_twitch_others_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing other Twitch pages (games, organizations, badges, emotes).
|
||||
xml_content += "</urlset>"
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = []
|
||||
|
||||
games: QuerySet[Game] = Game.objects.all()
|
||||
for game in games:
|
||||
resource_url: str = reverse("twitch:game_detail", args=[game.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {"url": full_url}
|
||||
|
||||
if game.updated_at:
|
||||
entry["lastmod"] = game.updated_at.isoformat()
|
||||
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
orgs: QuerySet[Organization] = Organization.objects.all()
|
||||
for org in orgs:
|
||||
resource_url: str = reverse("twitch:organization_detail", args=[org.twitch_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {"url": full_url}
|
||||
|
||||
if org.updated_at:
|
||||
entry["lastmod"] = org.updated_at.isoformat()
|
||||
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
badge_sets: QuerySet[ChatBadgeSet] = ChatBadgeSet.objects.all()
|
||||
for badge_set in badge_sets:
|
||||
resource_url = reverse("twitch:badge_set_detail", args=[badge_set.set_id])
|
||||
full_url = f"{base_url}{resource_url}"
|
||||
sitemap_urls.append({"url": full_url})
|
||||
|
||||
# Emotes currently don't have individual detail pages, but keep a listing here.
|
||||
sitemap_urls.append({"url": f"{base_url}/emotes/"})
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
# MARK: /sitemap-kick.xml
|
||||
def sitemap_kick_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing Kick drops and related pages.
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = []
|
||||
|
||||
kick_campaigns: QuerySet[KickDropCampaign] = KickDropCampaign.objects.all()
|
||||
for campaign in kick_campaigns:
|
||||
resource_url: str = reverse("kick:campaign_detail", args=[campaign.kick_id])
|
||||
full_url: str = f"{base_url}{resource_url}"
|
||||
entry: dict[str, str | dict[str, str]] = {"url": full_url}
|
||||
|
||||
if campaign.updated_at:
|
||||
entry["lastmod"] = campaign.updated_at.isoformat()
|
||||
|
||||
sitemap_urls.append(entry)
|
||||
|
||||
# Include Kick organizations and game list pages
|
||||
sitemap_urls.extend((
|
||||
{"url": f"{base_url}/kick/organizations/"},
|
||||
{"url": f"{base_url}/kick/games/"},
|
||||
))
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
# MARK: /sitemap-youtube.xml
|
||||
def sitemap_youtube_view(request: HttpRequest) -> HttpResponse:
|
||||
"""Sitemap containing the YouTube page(s).
|
||||
|
||||
Args:
|
||||
request: The HTTP request.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered sitemap XML.
|
||||
"""
|
||||
base_url: str = _build_base_url(request)
|
||||
|
||||
sitemap_urls: list[dict[str, str | dict[str, str]]] = [
|
||||
{"url": f"{base_url}{reverse('youtube:index')}"},
|
||||
]
|
||||
|
||||
xml_content: str = _render_urlset_xml(sitemap_urls)
|
||||
return HttpResponse(xml_content, content_type="application/xml")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ from django.test import RequestFactory
|
|||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from kick.models import KickCategory
|
||||
from kick.models import KickDropCampaign
|
||||
from kick.models import KickOrganization
|
||||
from twitch.models import Channel
|
||||
from twitch.models import ChatBadge
|
||||
from twitch.models import ChatBadgeSet
|
||||
|
|
@ -1324,61 +1321,19 @@ class TestSitemapView:
|
|||
name="ch1",
|
||||
display_name="Channel 1",
|
||||
)
|
||||
now: datetime = timezone.now()
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Test Campaign",
|
||||
description="Desc",
|
||||
game=game,
|
||||
operation_names=["DropCampaignDetails"],
|
||||
start_at=now - datetime.timedelta(days=1),
|
||||
end_at=now + datetime.timedelta(days=1),
|
||||
)
|
||||
inactive_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp2",
|
||||
name="Inactive Campaign",
|
||||
description="Desc",
|
||||
game=game,
|
||||
operation_names=["DropCampaignDetails"],
|
||||
start_at=now - datetime.timedelta(days=10),
|
||||
end_at=now - datetime.timedelta(days=5),
|
||||
)
|
||||
|
||||
kick_org: KickOrganization = KickOrganization.objects.create(
|
||||
kick_id="org1",
|
||||
name="Kick Org",
|
||||
)
|
||||
kick_cat: KickCategory = KickCategory.objects.create(
|
||||
kick_id=1,
|
||||
name="Kick Game",
|
||||
slug="kick-game",
|
||||
)
|
||||
kick_active: KickDropCampaign = KickDropCampaign.objects.create(
|
||||
kick_id="kcamp1",
|
||||
name="Kick Active Campaign",
|
||||
organization=kick_org,
|
||||
category=kick_cat,
|
||||
starts_at=now - datetime.timedelta(days=1),
|
||||
ends_at=now + datetime.timedelta(days=1),
|
||||
)
|
||||
kick_inactive: KickDropCampaign = KickDropCampaign.objects.create(
|
||||
kick_id="kcamp2",
|
||||
name="Kick Inactive Campaign",
|
||||
organization=kick_org,
|
||||
category=kick_cat,
|
||||
starts_at=now - datetime.timedelta(days=10),
|
||||
ends_at=now - datetime.timedelta(days=5),
|
||||
)
|
||||
|
||||
badge: ChatBadgeSet = ChatBadgeSet.objects.create(set_id="badge1")
|
||||
return {
|
||||
"org": org,
|
||||
"game": game,
|
||||
"channel": channel,
|
||||
"campaign": campaign,
|
||||
"inactive_campaign": inactive_campaign,
|
||||
"kick_active": kick_active,
|
||||
"kick_inactive": kick_inactive,
|
||||
"badge": badge,
|
||||
}
|
||||
|
||||
|
|
@ -1402,24 +1357,16 @@ class TestSitemapView:
|
|||
content = response.content.decode()
|
||||
assert content.startswith('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
|
||||
def test_sitemap_contains_sitemap_index(
|
||||
def test_sitemap_contains_urlset(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap index contains sitemap locations."""
|
||||
"""Test sitemap contains urlset element."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "<sitemapindex" in content
|
||||
assert "</sitemapindex>" in content
|
||||
assert "/sitemap-static.xml" in content
|
||||
assert "/sitemap-twitch-channels.xml" in content
|
||||
assert "/sitemap-twitch-drops.xml" in content
|
||||
assert "/sitemap-kick.xml" in content
|
||||
assert "/sitemap-youtube.xml" in content
|
||||
|
||||
# Ensure at least one entry includes a lastmod (there are entities created by the fixture)
|
||||
assert "<lastmod>" in content
|
||||
assert "<urlset" in content
|
||||
assert "</urlset>" in content
|
||||
|
||||
def test_sitemap_contains_static_pages(
|
||||
self,
|
||||
|
|
@ -1427,19 +1374,15 @@ class TestSitemapView:
|
|||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes static pages."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-static.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
|
||||
# Check for the homepage and a few key list views across apps.
|
||||
# Check for some static pages
|
||||
assert (
|
||||
"<loc>http://testserver/</loc>" in content
|
||||
or "<loc>http://localhost:8000/</loc>" in content
|
||||
)
|
||||
assert "http://testserver/twitch/" in content
|
||||
assert "http://testserver/kick/" in content
|
||||
assert "http://testserver/youtube/" in content
|
||||
assert "http://testserver/twitch/campaigns/" in content
|
||||
assert "http://testserver/twitch/games/" in content
|
||||
assert "/campaigns/" in content
|
||||
assert "/games/" in content
|
||||
|
||||
def test_sitemap_contains_game_detail_pages(
|
||||
self,
|
||||
|
|
@ -1448,7 +1391,7 @@ class TestSitemapView:
|
|||
) -> None:
|
||||
"""Test sitemap includes game detail pages."""
|
||||
game: Game = sample_entities["game"]
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert f"/games/{game.twitch_id}/" in content
|
||||
|
||||
|
|
@ -1459,62 +1402,10 @@ class TestSitemapView:
|
|||
) -> None:
|
||||
"""Test sitemap includes campaign detail pages."""
|
||||
campaign: DropCampaign = sample_entities["campaign"]
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert f"/campaigns/{campaign.twitch_id}/" in content
|
||||
|
||||
def test_sitemap_prioritizes_active_drops(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test active drops are prioritised and crawled more frequently."""
|
||||
active_campaign: DropCampaign = sample_entities["campaign"]
|
||||
inactive_campaign: DropCampaign = sample_entities["inactive_campaign"]
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-drops.xml")
|
||||
content: str = response.content.decode()
|
||||
|
||||
active_loc: str = f"<loc>http://testserver/twitch/campaigns/{active_campaign.twitch_id}/</loc>"
|
||||
active_index: int = content.find(active_loc)
|
||||
assert active_index != -1
|
||||
active_end: int = content.find("</url>", active_index)
|
||||
assert active_end != -1
|
||||
|
||||
inactive_loc: str = f"<loc>http://testserver/twitch/campaigns/{inactive_campaign.twitch_id}/</loc>"
|
||||
inactive_index: int = content.find(inactive_loc)
|
||||
assert inactive_index != -1
|
||||
inactive_end: int = content.find("</url>", inactive_index)
|
||||
assert inactive_end != -1
|
||||
|
||||
def test_sitemap_prioritizes_active_kick_campaigns(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test active Kick campaigns are prioritised and crawled more frequently."""
|
||||
active_campaign: KickDropCampaign = sample_entities["kick_active"]
|
||||
inactive_campaign: KickDropCampaign = sample_entities["kick_inactive"]
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-kick.xml")
|
||||
content: str = response.content.decode()
|
||||
|
||||
active_loc: str = (
|
||||
f"<loc>http://testserver/kick/campaigns/{active_campaign.kick_id}/</loc>"
|
||||
)
|
||||
active_index: int = content.find(active_loc)
|
||||
assert active_index != -1
|
||||
active_end: int = content.find("</url>", active_index)
|
||||
assert active_end != -1
|
||||
|
||||
inactive_loc: str = (
|
||||
f"<loc>http://testserver/kick/campaigns/{inactive_campaign.kick_id}/</loc>"
|
||||
)
|
||||
inactive_index: int = content.find(inactive_loc)
|
||||
assert inactive_index != -1
|
||||
inactive_end: int = content.find("</url>", inactive_index)
|
||||
assert inactive_end != -1
|
||||
|
||||
def test_sitemap_contains_organization_detail_pages(
|
||||
self,
|
||||
client: Client,
|
||||
|
|
@ -1522,7 +1413,7 @@ class TestSitemapView:
|
|||
) -> None:
|
||||
"""Test sitemap includes organization detail pages."""
|
||||
org: Organization = sample_entities["org"]
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert f"/organizations/{org.twitch_id}/" in content
|
||||
|
||||
|
|
@ -1533,9 +1424,7 @@ class TestSitemapView:
|
|||
) -> None:
|
||||
"""Test sitemap includes channel detail pages."""
|
||||
channel: Channel = sample_entities["channel"]
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
"/sitemap-twitch-channels.xml",
|
||||
)
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert f"/twitch/channels/{channel.twitch_id}/" in content
|
||||
|
||||
|
|
@ -1546,19 +1435,31 @@ class TestSitemapView:
|
|||
) -> None:
|
||||
"""Test sitemap includes badge detail pages."""
|
||||
badge: ChatBadge = sample_entities["badge"]
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert f"/badges/{badge.set_id}/" in content # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def test_sitemap_contains_youtube_pages(
|
||||
def test_sitemap_includes_priority(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes YouTube landing page."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-youtube.xml")
|
||||
"""Test sitemap includes priority values."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "/youtube/" in content
|
||||
assert "<priority>" in content
|
||||
assert "</priority>" in content
|
||||
|
||||
def test_sitemap_includes_changefreq(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes changefreq values."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "<changefreq>" in content
|
||||
assert "</changefreq>" in content
|
||||
|
||||
def test_sitemap_includes_lastmod(
|
||||
self,
|
||||
|
|
@ -1566,7 +1467,7 @@ class TestSitemapView:
|
|||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes lastmod for detail pages."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap-twitch-others.xml")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
# Check for lastmod in game or campaign entries
|
||||
assert "<lastmod>" in content
|
||||
|
|
@ -1829,7 +1730,7 @@ class TestImageObjectStructuredData:
|
|||
org: Organization,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""VideoGame ImageObject should carry attribution metadata."""
|
||||
"""VideoGame ImageObject should carry attribution and license metadata."""
|
||||
url: str = reverse("twitch:game_detail", args=[game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -1842,6 +1743,14 @@ class TestImageObjectStructuredData:
|
|||
"name": org.name,
|
||||
"url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
}
|
||||
assert (
|
||||
img["license"]
|
||||
== f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}"
|
||||
)
|
||||
assert (
|
||||
img["acquireLicensePage"]
|
||||
== f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}"
|
||||
)
|
||||
|
||||
def test_game_schema_no_image_when_no_box_art(
|
||||
self,
|
||||
|
|
@ -1941,7 +1850,7 @@ class TestImageObjectStructuredData:
|
|||
org: Organization,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Event ImageObject should carry attribution metadata."""
|
||||
"""Event ImageObject should carry attribution and license metadata."""
|
||||
url: str = reverse("twitch:campaign_detail", args=[campaign.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -1954,6 +1863,14 @@ class TestImageObjectStructuredData:
|
|||
"name": org.name,
|
||||
"url": f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}",
|
||||
}
|
||||
assert (
|
||||
img["license"]
|
||||
== f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}"
|
||||
)
|
||||
assert (
|
||||
img["acquireLicensePage"]
|
||||
== f"http://testserver{reverse('twitch:organization_detail', args=[org.twitch_id])}"
|
||||
)
|
||||
|
||||
def test_campaign_schema_no_image_when_no_image_url(
|
||||
self,
|
||||
|
|
@ -2030,6 +1947,8 @@ class TestImageObjectStructuredData:
|
|||
"name": "Twitch",
|
||||
"url": "https://www.twitch.tv/",
|
||||
}
|
||||
assert schema["image"]["license"] == "https://www.twitch.tv/"
|
||||
assert schema["image"]["acquireLicensePage"] == "https://www.twitch.tv/"
|
||||
assert "organizer" not in schema
|
||||
|
||||
# --- _pick_owner / Twitch Gaming skipping ---
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ def _build_image_object(
|
|||
*,
|
||||
copyright_notice: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a Schema.org ImageObject with attribution metadata.
|
||||
"""Build a Schema.org ImageObject with attribution and license metadata.
|
||||
|
||||
Args:
|
||||
request: The HTTP request used for absolute URL building.
|
||||
|
|
@ -96,13 +96,14 @@ def _build_image_object(
|
|||
"name": creator_name,
|
||||
"url": creator_url,
|
||||
}
|
||||
|
||||
return {
|
||||
"@type": "ImageObject",
|
||||
"contentUrl": request.build_absolute_uri(image_url),
|
||||
"creditText": creator_name,
|
||||
"copyrightNotice": copyright_notice or creator_name,
|
||||
"creator": creator,
|
||||
"license": creator_url,
|
||||
"acquireLicensePage": creator_url,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue