Update webhook page

This commit is contained in:
2024-07-06 23:29:39 +02:00
parent 9da33b402c
commit f37975d94a
15 changed files with 122 additions and 127 deletions

View File

@ -21,9 +21,9 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: mixed-line-ending - id: mixed-line-ending
- id: name-tests-test - id: name-tests-test
args: [--pytest-test-first] args: [ --pytest-test-first ]
- id: trailing-whitespace - id: trailing-whitespace
args: [--markdown-linebreak-ext=md] args: [ --markdown-linebreak-ext=md ]
exclude_types: exclude_types:
- "html" - "html"
@ -32,22 +32,22 @@ repos:
rev: "1.19.0" rev: "1.19.0"
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version, "5.1"] args: [ --target-version, "5.1" ]
# Run Pyupgrade on all Python files. This will upgrade the code to Python 3.12. # Run Pyupgrade on all Python files. This will upgrade the code to Python 3.12.
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.16.0 rev: v3.16.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py312-plus"] args: [ "--py312-plus" ]
# An extremely fast Python linter and formatter. # An extremely fast Python linter and formatter.
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0 rev: v0.5.1
hooks: hooks:
- id: ruff-format - id: ruff-format
- id: ruff - id: ruff
args: ["--fix", "--exit-non-zero-on-fix"] args: [ "--fix", "--exit-non-zero-on-fix" ]
# Static checker for GitHub Actions workflow files. # Static checker for GitHub Actions workflow files.
- repo: https://github.com/rhysd/actionlint - repo: https://github.com/rhysd/actionlint

52
.vscode/launch.json vendored
View File

@ -1,28 +1,28 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Django: Runserver", "name": "Django: Runserver",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/manage.py", "program": "${workspaceFolder}/manage.py",
"args": [ "args": [
"runserver", "runserver",
"--nothreading" "--nothreading"
], ],
"django": true, "django": true,
"justMyCode": true "justMyCode": true
}, },
{ {
"name": "python manage.py scrape_twitch", "name": "python manage.py scrape_twitch",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/manage.py", "program": "${workspaceFolder}/manage.py",
"args": [ "args": [
"scrape_twitch" "scrape_twitch"
], ],
"django": true, "django": true,
"justMyCode": true "justMyCode": true
} }
] ]
} }

30
.vscode/settings.json vendored
View File

@ -1,17 +1,17 @@
{ {
"cSpell.words": [ "cSpell.words": [
"allauth", "allauth",
"appendonly", "appendonly",
"asgiref", "asgiref",
"forloop", "forloop",
"logdir", "logdir",
"memlock", "memlock",
"networkidle", "networkidle",
"PGID", "PGID",
"PUID", "PUID",
"requirepass", "requirepass",
"socialaccount", "socialaccount",
"ttvdrops", "ttvdrops",
"ulimits" "ulimits"
] ]
} }

View File

@ -8,7 +8,6 @@ from platformdirs import user_data_dir
load_dotenv(dotenv_path=find_dotenv(), verbose=True) load_dotenv(dotenv_path=find_dotenv(), verbose=True)
DATA_DIR = Path( DATA_DIR = Path(
user_data_dir( user_data_dir(
appname="TTVDrops", appname="TTVDrops",
@ -28,7 +27,6 @@ sentry_sdk.init(
profiles_sample_rate=0.2, profiles_sample_rate=0.2,
) )
BASE_DIR: Path = Path(__file__).resolve().parent.parent BASE_DIR: Path = Path(__file__).resolve().parent.parent
ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "tlovinator@gmail.com")] ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "tlovinator@gmail.com")]
WSGI_APPLICATION = "config.wsgi.application" WSGI_APPLICATION = "config.wsgi.application"
@ -89,7 +87,6 @@ MIDDLEWARE: list[str] = [
"simple_history.middleware.HistoryRequestMiddleware", "simple_history.middleware.HistoryRequestMiddleware",
] ]
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",

View File

@ -107,16 +107,16 @@
</div> </div>
</div> </div>
<script type="module"> <script type="module">
const scrollSpy = new bootstrap.ScrollSpy(document.body, { const scrollSpy = new bootstrap.ScrollSpy(document.body, {
target: '.toc' target: '.toc'
}) })
document.body.addEventListener('activate.bs.scrollspy', function (event) { document.body.addEventListener('activate.bs.scrollspy', function (event) {
const activeItem = document.querySelector('.toc .active'); const activeItem = document.querySelector('.toc .active');
if (activeItem) { if (activeItem) {
activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
} }
}); });
</script> </script>
</body> </body>
{% endblock content %} {% endblock content %}

View File

@ -2,22 +2,27 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1 class="my-4">Add Discord Webhook</h1> <h1 class="my-4">Add Discord Webhook</h1>
<form method="post" class="needs-validation" novalidate> <div class="card card-body mb-3">
{% csrf_token %} Webhooks will be saved in a cookie and will be sent to the server when you subscribe to a drop.
{{ form.non_field_errors }} </div>
<div class="mb-3"> <div>
{{ form.webhook_url.errors }} <form method="post" class="needs-validation" novalidate>
<label for="{{ form.webhook_url.id_for_label }}" class="form-label">{{ form.webhook_url.label }}</label> {% csrf_token %}
<input type="url" {{ form.non_field_errors }}
name="webhook_url" <div class="mb-3">
required="" {{ form.webhook_url.errors }}
class="form-control" <label for="{{ form.webhook_url.id_for_label }}" class="form-label">{{ form.webhook_url.label }}</label>
aria-describedby="id_webhook_url_helptext" <input type="url"
id="id_webhook_url"> name="webhook_url"
<div class="form-text text-muted">{{ form.webhook_url.help_text }}</div> required=""
</div> class="form-control"
<button type="submit" class="btn btn-primary">Add Webhook</button> aria-describedby="id_webhook_url_helptext"
</form> id="id_webhook_url">
<div class="form-text text-muted">{{ form.webhook_url.help_text }}</div>
</div>
<button type="submit" class="btn btn-primary">Add Webhook</button>
</form>
</div>
<h2 class="mt-5">Webhooks</h2> <h2 class="mt-5">Webhooks</h2>
{% if webhooks %} {% if webhooks %}
<ul class="list-group mt-3"> <ul class="list-group mt-3">

View File

@ -8,13 +8,13 @@ if TYPE_CHECKING:
from django.http import HttpResponse from django.http import HttpResponse
@pytest.fixture() @pytest.fixture
def factory() -> RequestFactory: def factory() -> RequestFactory:
"""Factory for creating requests.""" """Factory for creating requests."""
return RequestFactory() return RequestFactory()
@pytest.mark.django_db() @pytest.mark.django_db
def test_index_view(client: Client) -> None: def test_index_view(client: Client) -> None:
"""Test index view.""" """Test index view."""
url: str = reverse(viewname="core:index") url: str = reverse(viewname="core:index")

View File

@ -6,7 +6,6 @@ from . import views
app_name: str = "core" app_name: str = "core"
urlpatterns: list[URLPattern | URLResolver] = [ urlpatterns: list[URLPattern | URLResolver] = [
path(route="", view=views.index, name="index"), path(route="", view=views.index, name="index"),
path( path(

View File

@ -11,7 +11,6 @@ from django.contrib import messages
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.generic import FormView, ListView from django.views.generic import FormView, ListView
from httpx._models import Response
from twitch_app.models import ( from twitch_app.models import (
DropBenefit, DropBenefit,
@ -33,7 +32,6 @@ if TYPE_CHECKING:
logger: logging.Logger = logging.getLogger(__name__) logger: logging.Logger = logging.getLogger(__name__)
cache_dir: Path = settings.DATA_DIR / "cache" cache_dir: Path = settings.DATA_DIR / "cache"
cache_dir.mkdir(exist_ok=True, parents=True) cache_dir.mkdir(exist_ok=True, parents=True)
storage = hishel.FileStorage(base_path=cache_dir) storage = hishel.FileStorage(base_path=cache_dir)
@ -284,7 +282,7 @@ class WebhooksView(FormView):
webhooks.append(webhook) webhooks.append(webhook)
response: HttpResponse = self.render_to_response(self.get_context_data(form=form)) response: HttpResponse = self.render_to_response(self.get_context_data(form=form))
response.set_cookie(key="webhooks", value=",".join(webhooks), max_age=60 * 60 * 24 * 365) response.set_cookie(key="webhooks", value=",".join(webhooks), max_age=315360000) # 10 years
messages.success(self.request, "Webhook successfully added.") messages.success(self.request, "Webhook successfully added.")
return response return response

View File

@ -6,7 +6,7 @@ services:
restart: always restart: always
ulimits: ulimits:
memlock: -1 memlock: -1
command: ["--auth", "Password", "--password", "${GARNET_PASSWORD}", "--storage-tier", "--logdir", "/logs", "--aof", "--port", "6380"] command: [ "--auth", "Password", "--password", "${GARNET_PASSWORD}", "--storage-tier", "--logdir", "/logs", "--aof", "--port", "6380" ]
ports: ports:
- "6380:6380" - "6380:6380"
volumes: volumes:

View File

@ -8,36 +8,36 @@ lint.select = ["ALL"]
line-length = 119 line-length = 119
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"
lint.ignore = [ lint.ignore = [
"CPY001", # Missing copyright notice at top of file "CPY001", # Missing copyright notice at top of file
"D100", # Checks for undocumented public module definitions. "D100", # Checks for undocumented public module definitions.
"D101", # Checks for undocumented public class definitions. "D101", # Checks for undocumented public class definitions.
"D102", # Checks for undocumented public method definitions. "D102", # Checks for undocumented public method definitions.
"D104", # Missing docstring in public package. "D104", # Missing docstring in public package.
"D105", # Missing docstring in magic method. "D105", # Missing docstring in magic method.
"D106", # Checks for undocumented public class definitions, for nested classes. "D106", # Checks for undocumented public class definitions, for nested classes.
"ERA001", # Found commented-out code "ERA001", # Found commented-out code
"FIX002", # Line contains TODO "FIX002", # Line contains TODO
"COM812", # Checks for the absence of trailing commas. "COM812", # Checks for the absence of trailing commas.
"ISC001", # Checks for implicitly concatenated strings on a single line. "ISC001", # Checks for implicitly concatenated strings on a single line.
"DJ001", # Checks nullable string-based fields (like CharField and TextField) in Django models. "DJ001", # Checks nullable string-based fields (like CharField and TextField) in Django models.
] ]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"**/tests/**" = [ "**/tests/**" = [
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant... "ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
"PLR2004", # Magic value used in comparison, ... "PLR2004", # Magic value used in comparison, ...
"S101", # asserts allowed in tests... "S101", # asserts allowed in tests...
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
] ]
"**/migrations/**" = [ "**/migrations/**" = [
"RUF012", # Checks for mutable default values in class attributes. "RUF012", # Checks for mutable default values in class attributes.
] ]
[tool.djlint] [tool.djlint]
profile = "django" profile = "django"
format_attribute_template_tags = true format_attribute_template_tags = true
ignore="H006" # Img tag should have height and width attributes. ignore = "H006" # Img tag should have height and width attributes.
[tool.pytest.ini_options] [tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings" DJANGO_SETTINGS_MODULE = "config.settings"

View File

@ -1,5 +1,6 @@
djlint djlint
pip
pre-commit pre-commit
ruff
pytest pytest
pytest-django pytest-django
ruff

View File

@ -8,7 +8,6 @@ httpx
pillow pillow
platformdirs platformdirs
playwright playwright
psycopg[binary]
python-dotenv python-dotenv
sentry-sdk[django] sentry-sdk[django]
whitenoise[brotli] whitenoise[brotli]

View File

@ -86,9 +86,7 @@ class DropCampaignSchema(Schema):
# http://localhost:8000/api/twitch/organizations # http://localhost:8000/api/twitch/organizations
@router.get("/organizations", response=list[OrganizationSchema]) @router.get("/organizations", response=list[OrganizationSchema])
def get_organizations( def get_organizations(request: HttpRequest) -> BaseManager[Organization]: # noqa: ARG001
request: HttpRequest, # noqa: ARG001
) -> BaseManager[Organization]:
"""Get all organizations.""" """Get all organizations."""
return Organization.objects.all() return Organization.objects.all()
@ -109,17 +107,13 @@ def get_drop_benefits(request: HttpRequest) -> BaseManager[DropBenefit]: # noqa
# http://localhost:8000/api/twitch/drop_campaigns # http://localhost:8000/api/twitch/drop_campaigns
@router.get("/drop_campaigns", response=list[DropCampaignSchema]) @router.get("/drop_campaigns", response=list[DropCampaignSchema])
def get_drop_campaigns( def get_drop_campaigns(request: HttpRequest) -> BaseManager[DropCampaign]: # noqa: ARG001
request: HttpRequest, # noqa: ARG001
) -> BaseManager[DropCampaign]:
"""Get all drop campaigns.""" """Get all drop campaigns."""
return DropCampaign.objects.all() return DropCampaign.objects.all()
# http://localhost:8000/api/twitch/time_based_drops # http://localhost:8000/api/twitch/time_based_drops
@router.get("/time_based_drops", response=list[TimeBasedDropSchema]) @router.get("/time_based_drops", response=list[TimeBasedDropSchema])
def get_time_based_drops( def get_time_based_drops(request: HttpRequest) -> BaseManager[TimeBasedDrop]: # noqa: ARG001
request: HttpRequest, # noqa: ARG001
) -> BaseManager[TimeBasedDrop]:
"""Get all time-based drops.""" """Get all time-based drops."""
return TimeBasedDrop.objects.all() return TimeBasedDrop.objects.all()

View File

@ -1,3 +1,5 @@
from typing import Literal
import auto_prefetch import auto_prefetch
from django.db import models from django.db import models
from django.db.models import Value from django.db.models import Value
@ -19,9 +21,9 @@ class Organization(auto_prefetch.Model):
modified_at = models.DateTimeField(blank=True, null=True, auto_now=True) modified_at = models.DateTimeField(blank=True, null=True, auto_now=True)
class Meta(auto_prefetch.Model.Meta): class Meta(auto_prefetch.Model.Meta):
verbose_name = "Organization" verbose_name: str = "Organization"
verbose_name_plural = "Organizations" verbose_name_plural: str = "Organizations"
ordering = ("name",) ordering: tuple[Literal["name"]] = ("name",)
def __str__(self) -> str: def __str__(self) -> str:
return self.name or self.id return self.name or self.id
@ -55,9 +57,9 @@ class Game(auto_prefetch.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta(auto_prefetch.Model.Meta): class Meta(auto_prefetch.Model.Meta):
verbose_name = "Game" verbose_name: str = "Game"
verbose_name_plural = "Games" verbose_name_plural: str = "Games"
ordering = ("display_name",) ordering: tuple[Literal["display_name"]] = ("display_name",)
def __str__(self) -> str: def __str__(self) -> str:
return self.display_name or self.slug or self.id return self.display_name or self.slug or self.id
@ -83,9 +85,9 @@ class DropBenefit(auto_prefetch.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta(auto_prefetch.Model.Meta): class Meta(auto_prefetch.Model.Meta):
verbose_name = "Drop Benefit" verbose_name: str = "Drop Benefit"
verbose_name_plural = "Drop Benefits" verbose_name_plural: str = "Drop Benefits"
ordering = ("name",) ordering: tuple[Literal["name"]] = ("name",)
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.owner_organization.name} - {self.game.display_name} - {self.name}" return f"{self.owner_organization.name} - {self.game.display_name} - {self.name}"
@ -106,9 +108,9 @@ class TimeBasedDrop(auto_prefetch.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta(auto_prefetch.Model.Meta): class Meta(auto_prefetch.Model.Meta):
verbose_name = "Time-Based Drop" verbose_name: str = "Time-Based Drop"
verbose_name_plural = "Time-Based Drops" verbose_name_plural: str = "Time-Based Drops"
ordering = ("name",) ordering: tuple[Literal["name"]] = ("name",)
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.benefits.first()} - {self.name}" return f"{self.benefits.first()} - {self.name}"
@ -145,9 +147,9 @@ class DropCampaign(auto_prefetch.Model):
history = HistoricalRecords() history = HistoricalRecords()
class Meta(auto_prefetch.Model.Meta): class Meta(auto_prefetch.Model.Meta):
verbose_name = "Drop Campaign" verbose_name: str = "Drop Campaign"
verbose_name_plural = "Drop Campaigns" verbose_name_plural: str = "Drop Campaigns"
ordering = ("name",) ordering: tuple[Literal["name"]] = ("name",)
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.owner.name} - {self.game.display_name} - {self.name}" return f"{self.owner.name} - {self.game.display_name} - {self.name}"