Add support for Reward campaigns
This commit is contained in:
.gitignore.pre-commit-config.yaml
.vscode
core
manage.pypoetry.lockpyproject.tomltwitch_app
4
.gitignore
vendored
4
.gitignore
vendored
@ -158,3 +158,7 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
*.json
|
||||
|
||||
staticfiles/
|
||||
|
@ -29,21 +29,21 @@ repos:
|
||||
|
||||
# Automatically upgrade your Django project code
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: "1.19.0"
|
||||
rev: "1.20.0"
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [ --target-version, "5.1" ]
|
||||
|
||||
# Run Pyupgrade on all Python files. This will upgrade the code to Python 3.12.
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.16.0
|
||||
rev: v3.17.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ "--py312-plus" ]
|
||||
|
||||
# An extremely fast Python linter and formatter.
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.1
|
||||
rev: v0.5.5
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -10,8 +10,10 @@
|
||||
"PGID",
|
||||
"PUID",
|
||||
"requirepass",
|
||||
"sitewide",
|
||||
"socialaccount",
|
||||
"ttvdrops",
|
||||
"ulimits"
|
||||
"ulimits",
|
||||
"xdefiant"
|
||||
]
|
||||
}
|
||||
|
@ -7,6 +7,11 @@
|
||||
<div class="col-lg-9">
|
||||
{% include "partials/info_box.html" %}
|
||||
{% include "partials/news.html" %}
|
||||
<h2>Reward Campaigns</h2>
|
||||
{% for campaign in reward_campaigns %}
|
||||
{% include "partials/reward_campaign_card.html" %}
|
||||
{% endfor %}
|
||||
<h2>Organizations</h2>
|
||||
{% for org in orgs %}
|
||||
<h2 id="org-{{ org|slugify }}">
|
||||
<a href="#org-{{ org|slugify }}">{{ org }}</a>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<a class="nav-link" href="https://github.com/sponsors/TheLovinator1">Donate</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="nav-link" href='{% url "core:webhooks" %}'>Webhooks</a>
|
||||
<a class="nav-link" href='{% url "core:reward_campaigns" %}'>Reward campaigns</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -7,9 +7,6 @@
|
||||
<p>
|
||||
This site allows users to subscribe to Twitch drops notifications. You can choose to be alerted when new drops are found on Twitch or when the drops become available for farming.
|
||||
</p>
|
||||
<p>
|
||||
You can add a Discord Webhook <a href="{% url 'core:webhooks' %}">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
56
core/templates/partials/reward_campaign_card.html
Normal file
56
core/templates/partials/reward_campaign_card.html
Normal file
@ -0,0 +1,56 @@
|
||||
<div class="card mb-4 shadow-sm" id="campaign-{{ campaign.id }}">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2">
|
||||
<img src="{{ campaign.image.image1_x_url }}"
|
||||
alt="{{ campaign.name }}"
|
||||
class="img-fluid rounded-start"
|
||||
height="283"
|
||||
width="212"
|
||||
loading="lazy">
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title h5">{{ campaign.name }}</h2>
|
||||
<p class="card-text text-muted">{{ campaign.summary }}</p>
|
||||
<p>
|
||||
Starts at: <abbr title="{{ campaign.starts_at|date:'l d F H:i e' }}">{{ campaign.starts_at }}</abbr>
|
||||
<br>
|
||||
Ends at: <abbr title="{{ campaign.ends_at|date:'l d F H:i e' }}">{{ campaign.ends_at|timeuntil }}</abbr>
|
||||
</p>
|
||||
<a href="{{ campaign.external_url }}"
|
||||
class="btn btn-primary"
|
||||
target="_blank">Learn More</a>
|
||||
{% if campaign.instructions %}
|
||||
<div class="mt-3">
|
||||
<h3 class="h6">Instructions</h3>
|
||||
<p>{{ campaign.instructions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if campaign.rewards.exists %}
|
||||
<div class="mt-3">
|
||||
<h3 class="h6">Rewards</h3>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||
{% for reward in campaign.rewards.all %}
|
||||
<div class="col d-flex align-items-center position-relative">
|
||||
<img src="{{ reward.thumbnail_image.image1_x_url }}"
|
||||
alt="{{ reward.name }} reward image"
|
||||
class="img-fluid rounded me-3"
|
||||
height="50"
|
||||
width="50"
|
||||
loading="lazy">
|
||||
<div>
|
||||
<strong>{{ reward.name }}</strong>
|
||||
<br>
|
||||
<a href="{{ reward.redemption_url }}"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank">Redeem</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
14
core/templates/reward_campaigns.html
Normal file
14
core/templates/reward_campaigns.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<h2>Reward Campaigns</h2>
|
||||
<div>
|
||||
{% for campaign in reward_campaigns %}
|
||||
{% include "partials/reward_campaign_card.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -24,35 +24,5 @@
|
||||
</form>
|
||||
</div>
|
||||
<h2 class="mt-5">Webhooks</h2>
|
||||
{% if webhooks %}
|
||||
<ul class="list-group mt-3">
|
||||
{% for webhook in webhooks %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<img src="{{ webhook.avatar }}?size=32"
|
||||
alt="{{ webhook.name }}"
|
||||
class="rounded-circle"
|
||||
height="32"
|
||||
width="32">
|
||||
<a href="{{ webhook.url }}" target="_blank">{{ webhook.name }}</a>
|
||||
{% if webhook.status == 'Success' %}
|
||||
<span class="badge bg-success">Working</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Failed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form method="post" action="" class="mb-0">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="webhook_id" value="{{ webhook.id }}">
|
||||
<input type="hidden" name="webhook_name" value="{{ webhook.name }}">
|
||||
<input type="hidden" name="webhook_url" value="{{ webhook.webhook_url }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">No webhooks added yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
10
core/urls.py
10
core/urls.py
@ -2,9 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from django.urls import URLPattern, URLResolver, path
|
||||
|
||||
from .views.games import GameView
|
||||
from .views.index import index
|
||||
from .views.webhooks import WebhooksView
|
||||
from core.views import GameView, RewardCampaignView, index
|
||||
|
||||
app_name: str = "core"
|
||||
|
||||
@ -15,5 +13,9 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
||||
view=GameView.as_view(),
|
||||
name="games",
|
||||
),
|
||||
path("webhooks/", WebhooksView.as_view(), name="webhooks"),
|
||||
path(
|
||||
route="reward_campaigns/",
|
||||
view=RewardCampaignView.as_view(),
|
||||
name="reward_campaigns",
|
||||
),
|
||||
]
|
||||
|
@ -5,15 +5,16 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import hishel
|
||||
from django.conf import settings
|
||||
from django.db.models.manager import BaseManager
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.generic import ListView
|
||||
|
||||
from core.data import WebhookData
|
||||
from twitch_app.models import Organization
|
||||
from twitch_app.models import Game, RewardCampaign
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from django.db.models.manager import BaseManager
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from httpx import Response
|
||||
|
||||
@ -59,11 +60,24 @@ def get_webhook_data(webhook: str) -> WebhookData:
|
||||
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
"""Render the index page."""
|
||||
orgs: BaseManager[Organization] = Organization.objects.all()
|
||||
webhooks: list[WebhookData] = [get_webhook_data(webhook) for webhook in get_webhooks(request)]
|
||||
reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all()
|
||||
|
||||
return TemplateResponse(
|
||||
request=request,
|
||||
template="index.html",
|
||||
context={"orgs": orgs, "webhooks": webhooks},
|
||||
context={"reward_campaigns": reward_campaigns},
|
||||
)
|
||||
|
||||
|
||||
class GameView(ListView):
|
||||
model = Game
|
||||
template_name: str = "games.html"
|
||||
context_object_name: str = "games"
|
||||
paginate_by = 100
|
||||
|
||||
|
||||
class RewardCampaignView(ListView):
|
||||
model = RewardCampaign
|
||||
template_name: str = "reward_campaigns.html"
|
||||
context_object_name: str = "reward_campaigns"
|
||||
paginate_by = 100
|
@ -1,16 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from django.views.generic import ListView
|
||||
|
||||
from twitch_app.models import Game
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GameView(ListView):
|
||||
model = Game
|
||||
template_name: str = "games.html"
|
||||
context_object_name: str = "games"
|
||||
paginate_by = 100
|
@ -1,118 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import hishel
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http.response import HttpResponse
|
||||
from django.views.generic import FormView
|
||||
|
||||
from core.data import WebhookData
|
||||
from core.forms import DiscordSettingForm
|
||||
from twitch_app.models import Game
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
|
||||
cache_dir: Path = settings.DATA_DIR / "cache"
|
||||
cache_dir.mkdir(exist_ok=True, parents=True)
|
||||
storage = hishel.FileStorage(base_path=cache_dir)
|
||||
controller = hishel.Controller(
|
||||
cacheable_status_codes=[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501],
|
||||
allow_stale=True,
|
||||
always_revalidate=True,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
)
|
||||
from httpx import Response
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_webhooks(request: HttpRequest) -> list[str]:
|
||||
"""Get the webhooks from the cookie."""
|
||||
cookie: str = request.COOKIES.get("webhooks", "")
|
||||
return list(filter(None, cookie.split(",")))
|
||||
|
||||
|
||||
def get_avatar(webhook_response: Response) -> str:
|
||||
"""Get the avatar URL from the webhook response."""
|
||||
avatar: str = "https://cdn.discordapp.com/embed/avatars/0.png"
|
||||
if webhook_response.is_success and webhook_response.json().get("id") and webhook_response.json().get("avatar"):
|
||||
avatar = f'https://cdn.discordapp.com/avatars/{webhook_response.json().get("id")}/{webhook_response.json().get("avatar")}.png'
|
||||
return avatar
|
||||
|
||||
|
||||
def get_webhook_data(webhook: str) -> WebhookData:
|
||||
"""Get the webhook data."""
|
||||
with hishel.CacheClient(storage=storage, controller=controller) as client:
|
||||
webhook_response: Response = client.get(url=webhook, extensions={"cache_metadata": True})
|
||||
|
||||
return WebhookData(
|
||||
name=webhook_response.json().get("name") if webhook_response.is_success else "Unknown",
|
||||
url=webhook,
|
||||
avatar=get_avatar(webhook_response),
|
||||
status="Success" if webhook_response.is_success else "Failed",
|
||||
response=webhook_response.text,
|
||||
)
|
||||
|
||||
|
||||
class WebhooksView(FormView):
|
||||
model = Game
|
||||
template_name = "webhooks.html"
|
||||
form_class = DiscordSettingForm
|
||||
context_object_name: str = "webhooks"
|
||||
paginate_by = 100
|
||||
|
||||
def get_context_data(self: WebhooksView, **kwargs: dict[str, WebhooksView] | DiscordSettingForm) -> dict[str, Any]:
|
||||
"""Get the context data for the view."""
|
||||
context: dict[str, DiscordSettingForm | list[WebhookData]] = super().get_context_data(**kwargs)
|
||||
webhooks: list[str] = get_webhooks(self.request)
|
||||
|
||||
context.update({
|
||||
"webhooks": [get_webhook_data(webhook) for webhook in webhooks],
|
||||
"form": DiscordSettingForm(),
|
||||
})
|
||||
return context
|
||||
|
||||
def form_valid(self: WebhooksView, form: DiscordSettingForm) -> HttpResponse:
|
||||
"""Handle valid form submission."""
|
||||
webhook = str(form.cleaned_data["webhook_url"])
|
||||
|
||||
with hishel.CacheClient(storage=storage, controller=controller) as client:
|
||||
webhook_response: Response = client.get(url=webhook, extensions={"cache_metadata": True})
|
||||
if not webhook_response.is_success:
|
||||
messages.error(self.request, "Failed to get webhook information. Is the URL correct?")
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
webhook_name: str | None = str(webhook_response.json().get("name")) if webhook_response.is_success else None
|
||||
|
||||
cookie: str = self.request.COOKIES.get("webhooks", "")
|
||||
webhooks: list[str] = cookie.split(",")
|
||||
webhooks = list(filter(None, webhooks))
|
||||
if webhook in webhooks:
|
||||
if webhook_name:
|
||||
messages.error(self.request, f"Webhook {webhook_name} already exists.")
|
||||
else:
|
||||
messages.error(self.request, "Webhook already exists.")
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
webhooks.append(webhook)
|
||||
response: HttpResponse = self.render_to_response(self.get_context_data(form=form))
|
||||
response.set_cookie(key="webhooks", value=",".join(webhooks), max_age=315360000) # 10 years
|
||||
|
||||
messages.success(self.request, "Webhook successfully added.")
|
||||
return response
|
||||
|
||||
def form_invalid(self: WebhooksView, form: DiscordSettingForm) -> HttpResponse:
|
||||
messages.error(self.request, "Failed to add webhook.")
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
@ -16,7 +16,7 @@ def main() -> None:
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise ImportError(msg) from exc
|
||||
raise ImportError(msg) from exc # noqa: DOC501, RUF100
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
|
74
poetry.lock
generated
74
poetry.lock
generated
@ -317,17 +317,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.1b1"
|
||||
version = "5.1rc1"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "Django-5.1b1-py3-none-any.whl", hash = "sha256:da04000c01f7c216ec2b1d6d85698f4cf540a8d304c83b7e29d512b19c7da039"},
|
||||
{file = "Django-5.1b1.tar.gz", hash = "sha256:2c52fb8e630f49f1d5b9555d9be1a0e1b27279cd9aba46161ea8d6d103f6262c"},
|
||||
{file = "Django-5.1rc1-py3-none-any.whl", hash = "sha256:dc162667eb0e66352cd1b6fc2c7a107649ff293cbc8f2a9fdee1a1c0ea3d9e13"},
|
||||
{file = "Django-5.1rc1.tar.gz", hash = "sha256:4b3d5c509ccb1528f158afe831d9d98c40efc852eee0d530c5fbe92e6b54cfdf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = ">=3.7.0"
|
||||
asgiref = ">=3.8.1,<4"
|
||||
sqlparse = ">=0.3.1"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
@ -821,13 +821,13 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.7.1"
|
||||
version = "3.8.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
|
||||
{file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -856,20 +856,20 @@ dev = ["black", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs",
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.2"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2.0"
|
||||
pluggy = ">=1.5,<2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
@ -1091,40 +1091,40 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.1"
|
||||
version = "0.5.5"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
|
||||
{file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
|
||||
{file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
|
||||
{file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
|
||||
{file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
|
||||
{file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
|
||||
{file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
|
||||
{file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
|
||||
{file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"},
|
||||
{file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"},
|
||||
{file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"},
|
||||
{file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"},
|
||||
{file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"},
|
||||
{file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"},
|
||||
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.9.0"
|
||||
version = "2.11.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee"},
|
||||
{file = "sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6"},
|
||||
{file = "sentry_sdk-2.11.0-py2.py3-none-any.whl", hash = "sha256:d964710e2dbe015d9dc4ff0ad16225d68c3b36936b742a6fe0504565b760a3b7"},
|
||||
{file = "sentry_sdk-2.11.0.tar.gz", hash = "sha256:4ca16e9f5c7c6bc2fb2d5c956219f4926b148e511fffdbbde711dc94f1e0468f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1191,13 +1191,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
description = "A non-validating SQL parser."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
|
||||
{file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
|
||||
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"},
|
||||
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -1323,4 +1323,4 @@ brotli = ["brotli"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "53676c3c85411ebdd5f25d2a016b07d5f3cc48c74a36c6130f4a967f8fd9ff38"
|
||||
content-hash = "247df8360be634f1bf0c3f3db54a728aec6f7ac7f5644e92a33e70d0758d7d19"
|
||||
|
@ -9,7 +9,7 @@ package-mode = false
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
discord-webhook = "^1.3.1"
|
||||
django = { version = "^5.1b1", allow-prereleases = true }
|
||||
django = { version = "^5.1rc1", allow-prereleases = true }
|
||||
django-auto-prefetch = "^1.9.0"
|
||||
django-debug-toolbar = "^4.4.6"
|
||||
django-simple-history = "^3.7.0"
|
||||
@ -18,14 +18,14 @@ httpx = "^0.27.0"
|
||||
pillow = "^10.4.0"
|
||||
platformdirs = "^4.2.2"
|
||||
python-dotenv = "^1.0.1"
|
||||
sentry-sdk = { extras = ["django"], version = "^2.9.0" }
|
||||
sentry-sdk = { extras = ["django"], version = "^2.11.0" }
|
||||
whitenoise = { extras = ["brotli"], version = "^6.7.0" }
|
||||
undetected-playwright-patch = "^1.40.0.post1700587210000"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
djlint = "^1.34.1"
|
||||
pre-commit = "^3.7.1"
|
||||
pytest = "^8.2.2"
|
||||
pre-commit = "^3.8.0"
|
||||
pytest = "^8.3.2"
|
||||
pytest-django = "^4.8.0"
|
||||
ruff = "^0.5.1"
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -10,17 +11,12 @@ from platformdirs import user_data_dir
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
from playwright.async_api._generated import Response
|
||||
|
||||
from twitch_app.models import (
|
||||
Drop,
|
||||
DropCampaign,
|
||||
Game,
|
||||
Organization,
|
||||
)
|
||||
from twitch_app.models import Game, Image, Reward, RewardCampaign, UnlockRequirements
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from playwright.async_api._generated import BrowserContext, Page
|
||||
|
||||
# Where to store the Firefox profile
|
||||
# Where to store the Chrome profile
|
||||
data_dir = Path(
|
||||
user_data_dir(
|
||||
appname="TTVDrops",
|
||||
@ -37,120 +33,92 @@ if not data_dir:
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def insert_data(data: dict) -> None: # noqa: C901, PLR0914
|
||||
"""Insert data into the database.
|
||||
|
||||
Args:
|
||||
data: The data from Twitch.
|
||||
"""
|
||||
user_data: dict = data.get("data", {}).get("user")
|
||||
if not user_data:
|
||||
logger.debug("No user data found")
|
||||
return
|
||||
|
||||
user_data["id"]
|
||||
drop_campaign_data = user_data["dropCampaign"]
|
||||
if not drop_campaign_data:
|
||||
logger.debug("No drop campaign data found")
|
||||
return
|
||||
|
||||
# Create or get the organization
|
||||
owner_data = drop_campaign_data["owner"]
|
||||
owner, created = await sync_to_async(Organization.objects.get_or_create)(
|
||||
id=owner_data["id"],
|
||||
defaults={"name": owner_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Organization created: %s", owner)
|
||||
else:
|
||||
logger.debug("Organization found: %s", owner)
|
||||
|
||||
# Create or get the game
|
||||
game_data = drop_campaign_data["game"]
|
||||
game, created = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=game_data["id"],
|
||||
defaults={
|
||||
"slug": game_data["slug"],
|
||||
"display_name": game_data["displayName"],
|
||||
"organization": owner,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Game created: %s", game)
|
||||
|
||||
# Create the drop campaign
|
||||
drop_campaign, created = await sync_to_async(DropCampaign.objects.get_or_create)(
|
||||
id=drop_campaign_data["id"],
|
||||
defaults={
|
||||
"account_link_url": drop_campaign_data.get("accountLinkURL"),
|
||||
"description": drop_campaign_data.get("description"),
|
||||
"details_url": drop_campaign_data.get("detailsURL"),
|
||||
"end_at": drop_campaign_data.get("endAt"),
|
||||
"image_url": drop_campaign_data.get("imageURL"),
|
||||
"name": drop_campaign_data.get("name"),
|
||||
"start_at": drop_campaign_data.get("startAt"),
|
||||
"status": drop_campaign_data.get("status"),
|
||||
"game": game,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Drop campaign created: %s", drop_campaign)
|
||||
|
||||
# Create time-based drops
|
||||
for drop_data in drop_campaign_data["timeBasedDrops"]:
|
||||
drop_benefit_edges = drop_data["benefitEdges"]
|
||||
|
||||
time_based_drop, created = await sync_to_async(Drop.objects.get_or_create)(
|
||||
id=drop_data["id"],
|
||||
defaults={
|
||||
"required_subs": drop_data.get("requiredSubs"),
|
||||
"end_at": drop_data.get("endAt"),
|
||||
"name": drop_data.get("name"),
|
||||
"required_minutes_watched": drop_data.get("requiredMinutesWatched"),
|
||||
"start_at": drop_data.get("startAt"),
|
||||
"drop_campaign": drop_campaign,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Time-based drop created: %s", time_based_drop)
|
||||
|
||||
for edge in drop_benefit_edges:
|
||||
benefit_data = edge["benefit"]
|
||||
benefit_owner_data = benefit_data["ownerOrganization"]
|
||||
|
||||
org, created = await sync_to_async(
|
||||
Organization.objects.get_or_create,
|
||||
)(
|
||||
id=benefit_owner_data["id"],
|
||||
defaults={"name": benefit_owner_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Organization created: %s", org)
|
||||
|
||||
benefit_game_data = benefit_data["game"]
|
||||
benefit_game, created = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=benefit_game_data["id"],
|
||||
defaults={"display_name": benefit_game_data["name"]},
|
||||
)
|
||||
if created:
|
||||
logger.debug("Benefit game created: %s", benefit_game)
|
||||
|
||||
# Get the drop to add the data to
|
||||
drop, created = await sync_to_async(Drop.objects.get_or_create)(
|
||||
id=drop_data["id"],
|
||||
async def add_reward_campaign(json_data: dict) -> None:
|
||||
"""Add data from JSON to the database."""
|
||||
for campaign_data in json_data["data"]["rewardCampaignsAvailableToUser"]:
|
||||
# Add or get Game
|
||||
game_data = campaign_data["game"]
|
||||
if game_data:
|
||||
game, _ = await sync_to_async(Game.objects.get_or_create)(
|
||||
id=game_data["id"],
|
||||
slug=game_data["slug"],
|
||||
defaults={
|
||||
"created_at": benefit_data.get("createdAt"),
|
||||
"entitlement_limit": benefit_data.get("entitlementLimit"),
|
||||
"image_asset_url": benefit_data.get("imageAssetURL"),
|
||||
"is_ios_available": benefit_data.get("isIosAvailable"),
|
||||
"name": benefit_data.get("name"),
|
||||
"display_name": game_data["displayName"],
|
||||
"typename": game_data["__typename"],
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.warning("%s is not for a game?", campaign_data["name"])
|
||||
game = None
|
||||
|
||||
if created:
|
||||
logger.debug("Drop created: %s", drop)
|
||||
# Add or get Image
|
||||
image_data = campaign_data["image"]
|
||||
image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=image_data["image1xURL"],
|
||||
defaults={"typename": image_data["__typename"]},
|
||||
)
|
||||
|
||||
await sync_to_async(drop.save)()
|
||||
# Create Reward instances
|
||||
rewards = []
|
||||
for reward_data in campaign_data["rewards"]:
|
||||
banner_image_data = reward_data["bannerImage"]
|
||||
banner_image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=banner_image_data["image1xURL"],
|
||||
defaults={"typename": banner_image_data["__typename"]},
|
||||
)
|
||||
|
||||
thumbnail_image_data = reward_data["thumbnailImage"]
|
||||
thumbnail_image, _ = await sync_to_async(Image.objects.get_or_create)(
|
||||
image1_x_url=thumbnail_image_data["image1xURL"],
|
||||
defaults={"typename": thumbnail_image_data["__typename"]},
|
||||
)
|
||||
|
||||
reward, _ = await sync_to_async(Reward.objects.get_or_create)(
|
||||
id=reward_data["id"],
|
||||
name=reward_data["name"],
|
||||
banner_image=banner_image,
|
||||
thumbnail_image=thumbnail_image,
|
||||
earnable_until=datetime.fromisoformat(reward_data["earnableUntil"].replace("Z", "+00:00")),
|
||||
redemption_instructions=reward_data["redemptionInstructions"],
|
||||
redemption_url=reward_data["redemptionURL"],
|
||||
typename=reward_data["__typename"],
|
||||
)
|
||||
rewards.append(reward)
|
||||
|
||||
# Add or get Unlock Requirements
|
||||
unlock_requirements_data = campaign_data["unlockRequirements"]
|
||||
_, _ = await sync_to_async(UnlockRequirements.objects.get_or_create)(
|
||||
subs_goal=unlock_requirements_data["subsGoal"],
|
||||
defaults={
|
||||
"minute_watched_goal": unlock_requirements_data["minuteWatchedGoal"],
|
||||
"typename": unlock_requirements_data["__typename"],
|
||||
},
|
||||
)
|
||||
|
||||
# Create Reward Campaign
|
||||
reward_campaign, _ = await sync_to_async(RewardCampaign.objects.get_or_create)(
|
||||
id=campaign_data["id"],
|
||||
name=campaign_data["name"],
|
||||
brand=campaign_data["brand"],
|
||||
starts_at=datetime.fromisoformat(campaign_data["startsAt"].replace("Z", "+00:00")),
|
||||
ends_at=datetime.fromisoformat(campaign_data["endsAt"].replace("Z", "+00:00")),
|
||||
status=campaign_data["status"],
|
||||
summary=campaign_data["summary"],
|
||||
instructions=campaign_data["instructions"],
|
||||
external_url=campaign_data["externalURL"],
|
||||
reward_value_url_param=campaign_data["rewardValueURLParam"],
|
||||
about_url=campaign_data["aboutURL"],
|
||||
is_sitewide=campaign_data["isSitewide"],
|
||||
game=game,
|
||||
image=image,
|
||||
typename=campaign_data["__typename"],
|
||||
)
|
||||
|
||||
# Add Rewards to the Campaign
|
||||
for reward in rewards:
|
||||
await sync_to_async(reward_campaign.rewards.add)(reward)
|
||||
|
||||
await sync_to_async(reward_campaign.save)()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -217,7 +185,7 @@ class Command(BaseCommand):
|
||||
logger.debug("Page loaded. Scraping data...")
|
||||
|
||||
# Wait 5 seconds for the page to load
|
||||
await asyncio.sleep(5)
|
||||
# await asyncio.sleep(5)
|
||||
|
||||
await browser.close()
|
||||
|
||||
@ -226,14 +194,16 @@ class Command(BaseCommand):
|
||||
if not isinstance(campaign, dict):
|
||||
continue
|
||||
|
||||
if "dropCampaign" in campaign.get("data", {}).get("user", {}):
|
||||
if "rewardCampaignsAvailableToUser" in campaign["data"]:
|
||||
await add_reward_campaign(campaign)
|
||||
|
||||
if "dropCampaign" in campaign.get("data", {}).get("user", {}): # noqa: SIM102
|
||||
if not campaign["data"]["user"]["dropCampaign"]:
|
||||
continue
|
||||
|
||||
await insert_data(campaign)
|
||||
|
||||
if "dropCampaigns" in campaign.get("data", {}).get("user", {}):
|
||||
await insert_data(campaign)
|
||||
msg = "Multiple dropCampaigns not supported"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
return json_data
|
||||
|
||||
|
@ -1,160 +1,90 @@
|
||||
# Generated by Django 5.1b1 on 2024-07-09 22:26
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 21:49
|
||||
|
||||
import uuid
|
||||
|
||||
import auto_prefetch
|
||||
import django.db.models.deletion
|
||||
import django.db.models.functions.text
|
||||
import django.db.models.manager
|
||||
from django.db import migrations, models
|
||||
from django.db.migrations.operations.base import Operation
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies: list[tuple[str, str]] = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DropCampaign",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("account_link_url", models.URLField(blank=True, null=True)),
|
||||
("description", models.TextField(blank=True, null=True)),
|
||||
("details_url", models.URLField(blank=True, null=True)),
|
||||
("end_at", models.DateTimeField(blank=True, null=True)),
|
||||
("image_url", models.URLField(blank=True, null=True)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("start_at", models.DateTimeField(blank=True, null=True)),
|
||||
("status", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Drop Campaign",
|
||||
"verbose_name_plural": "Drop Campaigns",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
operations: list[Operation] = [
|
||||
migrations.CreateModel(
|
||||
name="Game",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("slug", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"twitch_url",
|
||||
models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
db_persist=True,
|
||||
expression=django.db.models.functions.text.Concat(
|
||||
models.Value("https://www.twitch.tv/directory/category/"),
|
||||
"slug",
|
||||
),
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"image_url",
|
||||
models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
db_persist=True,
|
||||
expression=django.db.models.functions.text.Concat(
|
||||
models.Value("https://static-cdn.jtvnw.net/ttv-boxart/"),
|
||||
"id",
|
||||
models.Value("_IGDB.jpg"),
|
||||
),
|
||||
output_field=models.URLField(),
|
||||
),
|
||||
),
|
||||
("display_name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Game",
|
||||
"verbose_name_plural": "Games",
|
||||
"ordering": ("display_name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
("id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("slug", models.TextField()),
|
||||
("display_name", models.TextField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Organization",
|
||||
name="Image",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Organization",
|
||||
"verbose_name_plural": "Organizations",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image1_x_url", models.URLField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Drop",
|
||||
name="UnlockRequirements",
|
||||
fields=[
|
||||
("id", models.TextField(primary_key=True, serialize=False)),
|
||||
("created_at", models.DateTimeField(blank=True, null=True)),
|
||||
("entitlement_limit", models.IntegerField(blank=True, null=True)),
|
||||
("image_asset_url", models.URLField(blank=True, null=True)),
|
||||
("name", models.TextField(blank=True, null=True)),
|
||||
("added_at", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("modified_at", models.DateTimeField(auto_now=True, null=True)),
|
||||
("required_subs", models.IntegerField(blank=True, null=True)),
|
||||
("end_at", models.DateTimeField(blank=True, null=True)),
|
||||
("required_minutes_watched", models.IntegerField(blank=True, null=True)),
|
||||
("start_at", models.DateTimeField(blank=True, null=True)),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("subs_goal", models.IntegerField()),
|
||||
("minute_watched_goal", models.IntegerField()),
|
||||
("typename", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Reward",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
("name", models.TextField()),
|
||||
("earnable_until", models.DateTimeField()),
|
||||
("redemption_instructions", models.TextField()),
|
||||
("redemption_url", models.URLField()),
|
||||
("typename", models.TextField()),
|
||||
(
|
||||
"drop_campaign",
|
||||
auto_prefetch.ForeignKey(
|
||||
"banner_image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="drops",
|
||||
to="twitch_app.dropcampaign",
|
||||
related_name="banner_rewards",
|
||||
to="twitch_app.image",
|
||||
),
|
||||
),
|
||||
(
|
||||
"thumbnail_image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="thumbnail_rewards",
|
||||
to="twitch_app.image",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Drop",
|
||||
"verbose_name_plural": "Drops",
|
||||
"ordering": ("name",),
|
||||
"abstract": False,
|
||||
"base_manager_name": "prefetch_manager",
|
||||
},
|
||||
managers=[
|
||||
("objects", django.db.models.manager.Manager()),
|
||||
("prefetch_manager", django.db.models.manager.Manager()),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RewardCampaign",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
("name", models.TextField()),
|
||||
("brand", models.TextField()),
|
||||
("starts_at", models.DateTimeField()),
|
||||
("ends_at", models.DateTimeField()),
|
||||
("status", models.TextField()),
|
||||
("summary", models.TextField()),
|
||||
("instructions", models.TextField()),
|
||||
("external_url", models.URLField()),
|
||||
("reward_value_url_param", models.TextField()),
|
||||
("about_url", models.URLField()),
|
||||
("is_sitewide", models.BooleanField()),
|
||||
("typename", models.TextField()),
|
||||
("game", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="twitch_app.game")),
|
||||
("image", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="twitch_app.image")),
|
||||
("rewards", models.ManyToManyField(to="twitch_app.reward")),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="dropcampaign",
|
||||
name="game",
|
||||
field=auto_prefetch.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
to="twitch_app.game",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="organization",
|
||||
field=auto_prefetch.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="games",
|
||||
to="twitch_app.organization",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -0,0 +1,84 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='about_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='brand',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='ends_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='game',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.game'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='instructions',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='is_sitewide',
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='reward_value_url_param',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='starts_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='status',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='summary',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,109 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0002_alter_rewardcampaign_about_url_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='display_name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='slug',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='game',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image1_x_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='banner_image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner_rewards', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='earnable_until',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='name',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='redemption_instructions',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='redemption_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='thumbnail_image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thumbnail_rewards', to='twitch_app.image'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='ends_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='is_sitewide',
|
||||
field=models.BooleanField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='rewards',
|
||||
field=models.ManyToManyField(related_name='reward_campaigns', to='twitch_app.reward'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='starts_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='minute_watched_goal',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='subs_goal',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unlockrequirements',
|
||||
name='typename',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1rc1 on 2024-07-30 23:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('twitch_app', '0003_alter_game_display_name_alter_game_slug_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='reward',
|
||||
name='id',
|
||||
field=models.UUIDField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rewardcampaign',
|
||||
name='id',
|
||||
field=models.UUIDField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
@ -1,123 +1,209 @@
|
||||
from typing import Literal
|
||||
|
||||
import auto_prefetch
|
||||
from django.db import models
|
||||
from django.db.models import Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
|
||||
class Organization(auto_prefetch.Model):
|
||||
"""The company that owns the game.
|
||||
class Game(models.Model):
|
||||
"""The game that the reward is for.
|
||||
|
||||
For example, 2K games.
|
||||
Attributes:
|
||||
id (int): The primary key of the game.
|
||||
slug (str): The slug identifier of the game.
|
||||
display_name (str): The display name of the game.
|
||||
typename (str): The type name of the object, typically "Game".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"id": "780302568",
|
||||
"slug": "xdefiant",
|
||||
"displayName": "XDefiant",
|
||||
"__typename": "Game"
|
||||
}
|
||||
"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Organization"
|
||||
verbose_name_plural: str = "Organizations"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
id = models.AutoField(primary_key=True)
|
||||
slug = models.TextField(null=True, blank=True)
|
||||
display_name = models.TextField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name or self.id
|
||||
return self.display_name or "Unknown"
|
||||
|
||||
def get_twitch_url(self) -> str:
|
||||
return f"https://www.twitch.tv/directory/game/{self.slug}"
|
||||
|
||||
|
||||
class Game(auto_prefetch.Model):
|
||||
"""The game that the drop campaign is for.
|
||||
class Image(models.Model):
|
||||
"""An image model representing URLs and type.
|
||||
|
||||
For example, MultiVersus.
|
||||
Attributes:
|
||||
image1_x_url (str): URL to the image.
|
||||
typename (str): The type name of the object, typically "RewardCampaignImageSet".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
}
|
||||
"""
|
||||
|
||||
organization = auto_prefetch.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="games",
|
||||
)
|
||||
id = models.TextField(primary_key=True)
|
||||
slug = models.TextField(blank=True, null=True)
|
||||
twitch_url = models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
expression=Concat(Value("https://www.twitch.tv/directory/category/"), "slug"),
|
||||
output_field=models.TextField(),
|
||||
db_persist=True,
|
||||
)
|
||||
image_url = models.GeneratedField( # type: ignore # noqa: PGH003
|
||||
expression=Concat(
|
||||
Value("https://static-cdn.jtvnw.net/ttv-boxart/"),
|
||||
"id",
|
||||
Value("_IGDB.jpg"),
|
||||
),
|
||||
output_field=models.URLField(),
|
||||
db_persist=True,
|
||||
)
|
||||
display_name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Game"
|
||||
verbose_name_plural: str = "Games"
|
||||
ordering: tuple[Literal["display_name"]] = ("display_name",)
|
||||
image1_x_url = models.URLField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.display_name or self.slug or self.id
|
||||
return self.image1_x_url or "Unknown"
|
||||
|
||||
|
||||
class Drop(auto_prefetch.Model):
|
||||
"""The actual drop that is being given out."""
|
||||
class Reward(models.Model):
|
||||
"""The actual reward you get when you complete the requirements.
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
created_at = models.DateTimeField(blank=True, null=True)
|
||||
entitlement_limit = models.IntegerField(blank=True, null=True)
|
||||
image_asset_url = models.URLField(blank=True, null=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
required_subs = models.IntegerField(blank=True, null=True)
|
||||
end_at = models.DateTimeField(blank=True, null=True)
|
||||
required_minutes_watched = models.IntegerField(blank=True, null=True)
|
||||
start_at = models.DateTimeField(blank=True, null=True)
|
||||
drop_campaign = auto_prefetch.ForeignKey("DropCampaign", on_delete=models.CASCADE, related_name="drops")
|
||||
Attributes:
|
||||
id (UUID): The primary key of the reward.
|
||||
name (str): The name of the reward.
|
||||
banner_image (Image): The banner image associated with the reward.
|
||||
thumbnail_image (Image): The thumbnail image associated with the reward.
|
||||
earnable_until (datetime): The date and time until the reward can be earned.
|
||||
redemption_instructions (str): Instructions on how to redeem the reward.
|
||||
redemption_url (str): URL for redeeming the reward.
|
||||
typename (str): The type name of the object, typically "Reward".
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Drop"
|
||||
verbose_name_plural: str = "Drops"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class DropCampaign(auto_prefetch.Model):
|
||||
"""Drops are grouped into campaigns.
|
||||
|
||||
For example, MultiVersus S1 Drops
|
||||
JSON example:
|
||||
{
|
||||
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "Twitchy Character Skin",
|
||||
"bannerImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"thumbnailImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"earnableUntil": "2024-07-30T06:59:59Z",
|
||||
"redemptionInstructions": "",
|
||||
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"__typename": "Reward"
|
||||
}
|
||||
"""
|
||||
|
||||
id = models.TextField(primary_key=True)
|
||||
account_link_url = models.URLField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
details_url = models.URLField(blank=True, null=True)
|
||||
end_at = models.DateTimeField(blank=True, null=True)
|
||||
image_url = models.URLField(blank=True, null=True)
|
||||
name = models.TextField(blank=True, null=True)
|
||||
start_at = models.DateTimeField(blank=True, null=True)
|
||||
status = models.TextField(blank=True, null=True)
|
||||
game = auto_prefetch.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="drop_campaigns",
|
||||
)
|
||||
added_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
|
||||
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
|
||||
|
||||
class Meta(auto_prefetch.Model.Meta):
|
||||
verbose_name: str = "Drop Campaign"
|
||||
verbose_name_plural: str = "Drop Campaigns"
|
||||
ordering: tuple[Literal["name"]] = ("name",)
|
||||
id = models.UUIDField(primary_key=True)
|
||||
name = models.TextField(null=True, blank=True)
|
||||
banner_image = models.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True)
|
||||
thumbnail_image = models.ForeignKey(Image, related_name="thumbnail_rewards", on_delete=models.CASCADE, null=True)
|
||||
earnable_until = models.DateTimeField(null=True)
|
||||
redemption_instructions = models.TextField(null=True, blank=True)
|
||||
redemption_url = models.URLField(null=True, blank=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.game.display_name} - {self.name}"
|
||||
return self.name or "Unknown"
|
||||
|
||||
|
||||
class UnlockRequirements(models.Model):
|
||||
"""Requirements to unlock a reward.
|
||||
|
||||
Attributes:
|
||||
subs_goal (int): The number of subscriptions needed to unlock the reward.
|
||||
minute_watched_goal (int): The number of minutes watched needed to unlock the reward.
|
||||
typename (str): The type name of the object, typically "QuestRewardUnlockRequirements".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"subsGoal": 2,
|
||||
"minuteWatchedGoal": 0,
|
||||
"__typename": "QuestRewardUnlockRequirements"
|
||||
}
|
||||
"""
|
||||
|
||||
subs_goal = models.IntegerField(null=True)
|
||||
minute_watched_goal = models.IntegerField(null=True)
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.subs_goal} subs and {self.minute_watched_goal} minutes watched"
|
||||
|
||||
|
||||
class RewardCampaign(models.Model):
|
||||
"""Represents a reward campaign.
|
||||
|
||||
Attributes:
|
||||
id (UUID): The primary key of the reward campaign.
|
||||
name (str): The name of the reward campaign.
|
||||
brand (str): The brand associated with the campaign.
|
||||
starts_at (datetime): The start date and time of the campaign.
|
||||
ends_at (datetime): The end date and time of the campaign.
|
||||
status (str): The status of the campaign.
|
||||
summary (str): A brief summary of the campaign.
|
||||
instructions (str): Instructions for the campaign.
|
||||
external_url (str): The external URL related to the campaign.
|
||||
reward_value_url_param (str): URL parameter for the reward value.
|
||||
about_url (str): URL with more information about the campaign.
|
||||
is_sitewide (bool): Indicates if the campaign is sitewide.
|
||||
game (Game): The game associated with the campaign.
|
||||
image (Image): The image associated with the campaign.
|
||||
rewards (ManyToManyField): The rewards available in the campaign.
|
||||
typename (str): The type name of the object, typically "RewardCampaign".
|
||||
|
||||
JSON example:
|
||||
{
|
||||
"id": "3757a2ae-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "XDefiant Season 1 Launch",
|
||||
"brand": "Ubisoft",
|
||||
"startsAt": "2024-07-02T17:00:00Z",
|
||||
"endsAt": "2024-07-30T06:59:59Z",
|
||||
"status": "UNKNOWN",
|
||||
"summary": "Get a redeemable code for the Twitchy Character Skin in XDefiant for gifting or purchasing 2 subscriptions of any tier to participating channels.",
|
||||
"instructions": "",
|
||||
"externalURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"rewardValueURLParam": "",
|
||||
"aboutURL": "https://xdefiant.com/S1-twitch-rewards",
|
||||
"isSitewide": false,
|
||||
"game": {
|
||||
"id": "780302568",
|
||||
"slug": "xdefiant",
|
||||
"displayName": "XDefiant",
|
||||
"__typename": "Game"
|
||||
},
|
||||
"image": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"rewards": [
|
||||
{
|
||||
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
|
||||
"name": "Twitchy Character Skin",
|
||||
"bannerImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"thumbnailImage": {
|
||||
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
|
||||
"__typename": "RewardCampaignImageSet"
|
||||
},
|
||||
"earnableUntil": "2024-07-30T06:59:59Z",
|
||||
"redemptionInstructions": "",
|
||||
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
|
||||
"__typename": "Reward"
|
||||
}
|
||||
],
|
||||
"__typename": "RewardCampaign"
|
||||
}
|
||||
""" # noqa: E501
|
||||
|
||||
id = models.UUIDField(primary_key=True)
|
||||
name = models.TextField(null=True, blank=True)
|
||||
brand = models.TextField(null=True, blank=True)
|
||||
starts_at = models.DateTimeField(null=True)
|
||||
ends_at = models.DateTimeField(null=True)
|
||||
status = models.TextField(null=True, blank=True)
|
||||
summary = models.TextField(null=True, blank=True)
|
||||
instructions = models.TextField(null=True, blank=True)
|
||||
external_url = models.URLField(null=True, blank=True)
|
||||
reward_value_url_param = models.TextField(null=True, blank=True)
|
||||
about_url = models.URLField(null=True, blank=True)
|
||||
is_sitewide = models.BooleanField(null=True)
|
||||
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
|
||||
image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
|
||||
rewards = models.ManyToManyField(Reward, related_name="reward_campaigns")
|
||||
typename = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name or "Unknown"
|
||||
|
57
twitch_app/testboi.py
Normal file
57
twitch_app/testboi.py
Normal file
@ -0,0 +1,57 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
id: int
|
||||
slug: str
|
||||
display_name: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
image1_x_url: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reward:
|
||||
id: UUID
|
||||
name: str
|
||||
banner_image: Image
|
||||
thumbnail_image: Image
|
||||
earnable_until: datetime
|
||||
redemption_instructions: str
|
||||
redemption_url: str
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnlockRequirements:
|
||||
subs_goal: int
|
||||
minute_watched_goal: int
|
||||
typename: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class RewardCampaign:
|
||||
id: UUID
|
||||
name: str
|
||||
brand: str
|
||||
starts_at: datetime
|
||||
ends_at: datetime
|
||||
status: str
|
||||
summary: str
|
||||
instructions: str
|
||||
external_url: str
|
||||
reward_value_url_param: str
|
||||
about_url: str
|
||||
is_sitewide: bool
|
||||
game: Game
|
||||
unlock_requirements: UnlockRequirements
|
||||
image: Image
|
||||
rewards: list[Reward]
|
||||
typename: str
|
Reference in New Issue
Block a user