Add working prototype
This commit is contained in:
6
core/apps.py
Normal file
6
core/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "core"
|
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
167
core/settings.py
167
core/settings.py
@ -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
120
core/templates/base.html
Normal 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
75
core/templates/index.html
Normal 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
0
core/tests/__init__.py
Normal file
22
core/tests/test_views.py
Normal file
22
core/tests/test_views.py
Normal 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
|
14
core/urls.py
14
core/urls.py
@ -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
69
core/views.py
Normal 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},
|
||||
)
|
@ -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()
|
Reference in New Issue
Block a user