Add working prototype

This commit is contained in:
2024-06-22 05:33:42 +02:00
parent 67dc4639a0
commit e8f7e55135
60 changed files with 982 additions and 19571 deletions

6
core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"

View File

View File

@ -1,167 +0,0 @@
import os
from pathlib import Path
import sentry_sdk
from dotenv import find_dotenv, load_dotenv
# Load environment variables from a .env file
load_dotenv(dotenv_path=find_dotenv(), verbose=True)
# Run Django in debug mode
DEBUG: bool = os.getenv(key="DEBUG", default="True").lower() == "true"
# Use Sentry for error reporting
USE_SENTRY: bool = os.getenv(key="USE_SENTRY", default="True").lower() == "true"
if USE_SENTRY:
sentry_sdk.init(
dsn="https://35519536b56710e51cac49522b2cc29f@o4505228040339456.ingest.sentry.io/4506447308914688",
environment="Development" if DEBUG else "Production",
send_default_pii=True,
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR: Path = Path(__file__).resolve().parent.parent
# A list of all the people who get code error notifications. When DEBUG=False and a view raises an exception, Django
ADMINS: list[tuple[str, str]] = [
("Joakim Hellsén", os.getenv("ADMIN_EMAIL", default="")),
]
# The secret key is used for cryptographic signing, and should be set to a unique, unpredictable value.
SECRET_KEY: str = os.getenv("SECRET_KEY", default="")
# A list of strings representing the host/domain names that this Django site can serve.
ALLOWED_HOSTS: list[str] = [
"ttvdrops.lovinator.space",
".localhost",
"127.0.0.1",
]
# The time zone that Django will use to display datetimes in templates and to interpret datetimes entered in forms
TIME_ZONE = "Europe/Stockholm"
# If datetimes will be timezone-aware by default. If True, Django will use timezone-aware datetimes internally.
USE_TZ = True
# Decides which translation is served to all users.
LANGUAGE_CODE = "en-us"
# Default decimal separator used when formatting decimal numbers.
DECIMAL_SEPARATOR = ","
# Use a space as the thousand separator instead of a comma
THOUSAND_SEPARATOR = " "
# Use gmail for sending emails
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
EMAIL_HOST_PASSWORD: str = os.getenv(key="EMAIL_HOST_PASSWORD", default="")
EMAIL_SUBJECT_PREFIX = "[Panso] "
EMAIL_USE_LOCALTIME = True
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL: str = os.getenv(
key="EMAIL_HOST_USER",
default="webmaster@localhost",
)
SERVER_EMAIL: str = os.getenv(key="EMAIL_HOST_USER", default="webmaster@localhost")
INSTALLED_APPS: list[str] = [
# First-party apps
"twitch_drop_notifier.apps.TwitchDropNotifierConfig",
# Third-party apps
"whitenoise.runserver_nostatic",
# Django apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE: list[str] = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "core.urls"
# A list containing the settings for all template engines to be used with Django.
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"loaders": [
(
"django.template.loaders.cached.Loader",
[
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
),
],
},
},
]
WSGI_APPLICATION = "core.wsgi.application"
# A dictionary containing the settings for how we should connect to our PostgreSQL database.
DATABASES: dict[str, dict[str, str]] = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "ttvdrops",
"USER": os.getenv(key="POSTGRES_USER", default=""),
"PASSWORD": os.getenv(key="POSTGRES_PASSWORD", default=""),
"HOST": os.getenv(key="POSTGRES_HOST", default=""),
"PORT": os.getenv(key="POSTGRES_PORT", default="5432"),
},
}
# URL to use when referring to static files located in STATIC_ROOT.
STATIC_URL = "static/"
# Use a 64-bit integer as a primary key for models that don't have a field with primary_key=True.
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# The absolute path to the directory where 'python manage.py collectstatic' will copy static files for deployment
STATIC_ROOT: Path = BASE_DIR / "staticfiles"
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
# Use WhiteNoise to serve static files. https://whitenoise.readthedocs.io/en/latest/django.html
STORAGES: dict[str, dict[str, str]] = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Use Redis for caching
REDIS_PASSWORD: str = os.getenv(key="REDIS_PASSWORD", default="")
REDIS_HOST: str = os.getenv(key="REDIS_HOST", default="")
REDIS_PORT: str = os.getenv(key="REDIS_PORT", default="6380")
CACHES: dict[str, dict[str, str]] = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/0",
},
}

120
core/templates/base.html Normal file
View File

@ -0,0 +1,120 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="description"
content="{% block description %}
Subscribe to TTVDrop for the latest Twitch drops.
{% endblock description %}">
<meta name="keywords"
content="{% block keywords %}
Twitch, Drops, TTVDrop, Twitch Drops, Twitch Drops Tracker
{% endblock keywords %}">
<meta name="author" content="TheLovinator">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block title %}
<title>Twitch drops</title>
{% endblock title %}
<style>
html {
max-width: 88ch;
padding: calc(1vmin + 0.5rem);
margin-inline: auto;
font-size: clamp(1em, 0.909em + 0.45vmin, 1.25em);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
color-scheme: light dark;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
margin: 0;
}
.title {
text-align: center;
}
.search {
display: flex;
justify-content: center;
margin-top: 1rem;
margin-inline: auto;
}
.leftright {
display: flex;
justify-content: center;
}
.left {
margin-right: auto;
}
.right {
margin-left: auto;
}
textarea {
width: 100%;
height: 10rem;
resize: vertical;
}
.messages {
list-style-type: none;
}
.error {
color: red;
}
.success {
color: green;
}
</style>
</head>
<body>
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<span class="title">
<h1>
<a href="{% url 'core:index' %}">TTVDrop</a>
</h1>
</span>
<nav>
<small>
<div class="leftright">
<div class="left">Hello.</div>
<div class="right">
<a href="/api/v1/docs">API</a> |
<a href="https://github.com/TheLovinator1/twitch-online-notifier">GitHub</a> |
<a href="https://github.com/sponsors/TheLovinator1">Donate</a>
{% if not user.is_authenticated %}| <a href=''>Login</a>{% endif %}
{% if user.is_authenticated %}| <a href=''>{{ user.username }}</a>{% endif %}
</div>
</div>
</small>
</nav>
<hr />
<main>
{% block content %}<!-- default content -->{% endblock %}
</main>
<hr />
<footer>
<small>
<div class="leftright">
<div class="left">TheLovinator#9276 on Discord</div>
<div class="right">No rights reserved.</div>
</div>
</small>
</footer>
</body>
</html>

75
core/templates/index.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="description" content="Twitch Drops">
<meta name="author" content="TheLovinator">
<meta name="keywords" content="Twitch, Drops, Twitch Drops">
<meta name="robots" content="index, follow">
<title>Twitch Drops</title>
<style>
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 30px;
}
</style>
</head>
<body>
<h1>Twitch Drops</h1>
{% for organization, org_data in orgs_data.items %}
<header>
<a href="{{ organization.url }}">
<h2>{{ organization.name }}</h2>
</a>
<a href="">Subscribe to {{ organization.name }}</a>
</header>
<ul>
{% for game, game_data in org_data.games.items %}
<li>
<header>
<a href="https://www.twitch.tv/directory/category/{{ game.slug }}">
<h2>{{ game.display_name }}</h2>
</a>
<a href="{{ game.url }}">Subscribe to {{ game.display_name }}</a>
</header>
<ul>
{% for drop_benefit in game_data.drop_benefits %}
<dl>
<dt>
<header>
<a href="{{ drop_benefit.details_url }}">{{ drop_benefit.name }}</a>
</header>
</dt>
<dd>
<img src="{{ drop_benefit.image_asset_url }}"
alt="{{ drop_benefit.name }}"
height="100"
width="100">
</dd>
{% if drop_benefit.entitlement_limit > 1 %}
<dt>Entitlement Limit</dt>
<dd>
{{ drop_benefit.entitlement_limit|default:"N/A" }}
</dd>
{% endif %}
{% if drop_benefit.is_ios_available %}
<dt>iOS Available</dt>
<dd>
{{ drop_benefit.is_ios_available|yesno:"Yes,No" }}
</dd>
{% endif %}
</dl>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endfor %}
</body>
</html>

0
core/tests/__init__.py Normal file
View File

22
core/tests/test_views.py Normal file
View File

@ -0,0 +1,22 @@
from typing import TYPE_CHECKING
import pytest
from django.test import Client, RequestFactory
from django.urls import reverse
if TYPE_CHECKING:
from django.http import HttpResponse
@pytest.fixture()
def factory() -> RequestFactory:
"""Factory for creating requests."""
return RequestFactory()
@pytest.mark.django_db()
def test_index_view(client: Client) -> None:
"""Test index view."""
url: str = reverse(viewname="core:index")
response: HttpResponse = client.get(url)
assert response.status_code == 200

View File

@ -1,7 +1,11 @@
from django.contrib import admin
from django.urls import include, path
from __future__ import annotations
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("twitch_drop_notifier.urls")),
from django.urls import URLPattern, path
from . import views
app_name: str = "core"
urlpatterns: list[URLPattern] = [
path(route="", view=views.index, name="index"),
]

69
core/views.py Normal file
View File

@ -0,0 +1,69 @@
from typing import TYPE_CHECKING
from django.http import HttpRequest, HttpResponse
from django.template.response import TemplateResponse
from twitch.models import (
DropBenefit,
DropCampaign,
Game,
Organization,
TimeBasedDrop,
)
if TYPE_CHECKING:
from django.db.models.manager import BaseManager
def index(request: HttpRequest) -> HttpResponse:
"""/ index page.
Args:
request: The request.
Returns:
HttpResponse: Returns the index page.
"""
organizations: BaseManager[Organization] = Organization.objects.all()
# Organize the data
orgs_data = {}
for org in organizations:
orgs_data[org] = {"games": {}, "drop_campaigns": []}
# Populate the games under each organization through DropBenefit and DropCampaign
for org in organizations:
drop_benefits: BaseManager[DropBenefit] = DropBenefit.objects.filter(
owner_organization=org,
)
games: set[Game] = {benefit.game for benefit in drop_benefits}
for game in games:
if game not in orgs_data[org]["games"]:
orgs_data[org]["games"][game] = {
"drop_benefits": [],
"time_based_drops": [],
}
for benefit in drop_benefits:
orgs_data[org]["games"][benefit.game]["drop_benefits"].append(benefit)
time_based_drops = TimeBasedDrop.objects.filter(
benefits__in=drop_benefits,
).distinct()
for drop in time_based_drops:
for benefit in drop.benefits.all():
if benefit.game in orgs_data[org]["games"]:
orgs_data[org]["games"][benefit.game]["time_based_drops"].append(
drop,
)
drop_campaigns = DropCampaign.objects.filter(owner=org)
for campaign in drop_campaigns:
orgs_data[org]["drop_campaigns"].append(campaign)
return TemplateResponse(
request=request,
template="index.html",
context={"orgs_data": orgs_data},
)

View File

@ -1,8 +0,0 @@
import os
from django.core.handlers.wsgi import WSGIHandler
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application: WSGIHandler = get_wsgi_application()