phase2: add modular apps, auth scaffolding, and base template routing
This commit is contained in:
44
README.md
44
README.md
@ -5,26 +5,34 @@ Production-minded basketball scouting and player search platform built with Djan
|
||||
## Stack
|
||||
|
||||
- Python 3.12+
|
||||
- Django + Django Templates + HTMX (no SPA)
|
||||
- Django + Django Templates + HTMX (server-rendered)
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- Celery + Celery Beat
|
||||
- nginx
|
||||
- Docker / Docker Compose
|
||||
|
||||
## Repository Structure
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
.
|
||||
├── apps/
|
||||
│ ├── core/
|
||||
│ ├── users/
|
||||
│ ├── players/
|
||||
│ ├── competitions/
|
||||
│ ├── teams/
|
||||
│ ├── stats/
|
||||
│ ├── scouting/
|
||||
│ ├── providers/
|
||||
│ └── ingestion/
|
||||
├── config/
|
||||
│ └── settings/
|
||||
├── nginx/
|
||||
├── requirements/
|
||||
├── static/
|
||||
├── templates/
|
||||
├── tests/
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
└── entrypoint.sh
|
||||
└── tests/
|
||||
```
|
||||
|
||||
## Setup
|
||||
@ -41,7 +49,7 @@ cp .env.example .env
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
3. Apply migrations (if not using auto migrations):
|
||||
3. Apply migrations (if auto-migrate disabled):
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py migrate
|
||||
@ -59,6 +67,18 @@ docker compose exec web python manage.py createsuperuser
|
||||
- Admin: http://localhost/admin/
|
||||
- Health endpoint: http://localhost/health/
|
||||
|
||||
## Authentication Routes
|
||||
|
||||
- Signup: `/users/signup/`
|
||||
- Login: `/users/login/`
|
||||
- Logout: `/users/logout/`
|
||||
- Dashboard (auth required): `/dashboard/`
|
||||
|
||||
## Tailwind Integration Strategy
|
||||
|
||||
Phase 2 keeps styling minimal and framework-neutral using `static/css/main.css`.
|
||||
For upcoming phases, Tailwind will be integrated as a build step that emits compiled CSS into `static/css/` (e.g., via standalone Tailwind CLI or PostCSS in a dedicated frontend tooling container), while templates stay server-rendered.
|
||||
|
||||
## Development Commands
|
||||
|
||||
Run tests:
|
||||
@ -73,12 +93,6 @@ Run Django shell:
|
||||
docker compose exec web python manage.py shell
|
||||
```
|
||||
|
||||
Collect static files:
|
||||
|
||||
```bash
|
||||
docker compose exec web python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
Create migration:
|
||||
@ -93,10 +107,6 @@ Apply migration:
|
||||
docker compose exec web python manage.py migrate
|
||||
```
|
||||
|
||||
## Ingestion / Sync (Phase Placeholder)
|
||||
|
||||
Data provider ingestion flow will be added in a later phase with adapter-based provider isolation.
|
||||
|
||||
## GitFlow
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for branch model and PR workflow.
|
||||
|
||||
0
apps/competitions/__init__.py
Normal file
0
apps/competitions/__init__.py
Normal file
6
apps/competitions/apps.py
Normal file
6
apps/competitions/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CompetitionsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.competitions"
|
||||
0
apps/competitions/migrations/__init__.py
Normal file
0
apps/competitions/migrations/__init__.py
Normal file
9
apps/competitions/urls.py
Normal file
9
apps/competitions/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import CompetitionsHomeView
|
||||
|
||||
app_name = "competitions"
|
||||
|
||||
urlpatterns = [
|
||||
path("", CompetitionsHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/competitions/views.py
Normal file
5
apps/competitions/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class CompetitionsHomeView(TemplateView):
|
||||
template_name = "competitions/index.html"
|
||||
@ -1,8 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import health, home
|
||||
from .views import DashboardView, HomeView, health
|
||||
|
||||
app_name = "core"
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("", HomeView.as_view(), name="home"),
|
||||
path("dashboard/", DashboardView.as_view(), name="dashboard"),
|
||||
path("health/", health, name="health"),
|
||||
]
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, "home.html")
|
||||
class HomeView(TemplateView):
|
||||
template_name = "core/home.html"
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "core/dashboard.html"
|
||||
|
||||
|
||||
def health(request):
|
||||
|
||||
0
apps/ingestion/__init__.py
Normal file
0
apps/ingestion/__init__.py
Normal file
6
apps/ingestion/apps.py
Normal file
6
apps/ingestion/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IngestionConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.ingestion"
|
||||
0
apps/ingestion/migrations/__init__.py
Normal file
0
apps/ingestion/migrations/__init__.py
Normal file
9
apps/ingestion/urls.py
Normal file
9
apps/ingestion/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import IngestionHomeView
|
||||
|
||||
app_name = "ingestion"
|
||||
|
||||
urlpatterns = [
|
||||
path("", IngestionHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/ingestion/views.py
Normal file
5
apps/ingestion/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class IngestionHomeView(TemplateView):
|
||||
template_name = "ingestion/index.html"
|
||||
0
apps/players/__init__.py
Normal file
0
apps/players/__init__.py
Normal file
6
apps/players/apps.py
Normal file
6
apps/players/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlayersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.players"
|
||||
0
apps/players/migrations/__init__.py
Normal file
0
apps/players/migrations/__init__.py
Normal file
9
apps/players/urls.py
Normal file
9
apps/players/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import PlayersHomeView
|
||||
|
||||
app_name = "players"
|
||||
|
||||
urlpatterns = [
|
||||
path("", PlayersHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/players/views.py
Normal file
5
apps/players/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class PlayersHomeView(TemplateView):
|
||||
template_name = "players/index.html"
|
||||
0
apps/providers/__init__.py
Normal file
0
apps/providers/__init__.py
Normal file
6
apps/providers/apps.py
Normal file
6
apps/providers/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProvidersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.providers"
|
||||
0
apps/providers/migrations/__init__.py
Normal file
0
apps/providers/migrations/__init__.py
Normal file
9
apps/providers/urls.py
Normal file
9
apps/providers/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ProvidersHomeView
|
||||
|
||||
app_name = "providers"
|
||||
|
||||
urlpatterns = [
|
||||
path("", ProvidersHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/providers/views.py
Normal file
5
apps/providers/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class ProvidersHomeView(TemplateView):
|
||||
template_name = "providers/index.html"
|
||||
0
apps/scouting/__init__.py
Normal file
0
apps/scouting/__init__.py
Normal file
6
apps/scouting/apps.py
Normal file
6
apps/scouting/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ScoutingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.scouting"
|
||||
0
apps/scouting/migrations/__init__.py
Normal file
0
apps/scouting/migrations/__init__.py
Normal file
9
apps/scouting/urls.py
Normal file
9
apps/scouting/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ScoutingHomeView
|
||||
|
||||
app_name = "scouting"
|
||||
|
||||
urlpatterns = [
|
||||
path("", ScoutingHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/scouting/views.py
Normal file
5
apps/scouting/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class ScoutingHomeView(TemplateView):
|
||||
template_name = "scouting/index.html"
|
||||
0
apps/stats/__init__.py
Normal file
0
apps/stats/__init__.py
Normal file
6
apps/stats/apps.py
Normal file
6
apps/stats/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StatsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.stats"
|
||||
0
apps/stats/migrations/__init__.py
Normal file
0
apps/stats/migrations/__init__.py
Normal file
9
apps/stats/urls.py
Normal file
9
apps/stats/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import StatsHomeView
|
||||
|
||||
app_name = "stats"
|
||||
|
||||
urlpatterns = [
|
||||
path("", StatsHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/stats/views.py
Normal file
5
apps/stats/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class StatsHomeView(TemplateView):
|
||||
template_name = "stats/index.html"
|
||||
0
apps/teams/__init__.py
Normal file
0
apps/teams/__init__.py
Normal file
6
apps/teams/apps.py
Normal file
6
apps/teams/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TeamsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.teams"
|
||||
0
apps/teams/migrations/__init__.py
Normal file
0
apps/teams/migrations/__init__.py
Normal file
9
apps/teams/urls.py
Normal file
9
apps/teams/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import TeamsHomeView
|
||||
|
||||
app_name = "teams"
|
||||
|
||||
urlpatterns = [
|
||||
path("", TeamsHomeView.as_view(), name="index"),
|
||||
]
|
||||
5
apps/teams/views.py
Normal file
5
apps/teams/views.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class TeamsHomeView(TemplateView):
|
||||
template_name = "teams/index.html"
|
||||
0
apps/users/__init__.py
Normal file
0
apps/users/__init__.py
Normal file
6
apps/users/apps.py
Normal file
6
apps/users/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.users"
|
||||
18
apps/users/forms.py
Normal file
18
apps/users/forms.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class SignupForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "email", "password1", "password2")
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
user.email = self.cleaned_data["email"]
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
0
apps/users/migrations/__init__.py
Normal file
0
apps/users/migrations/__init__.py
Normal file
12
apps/users/urls.py
Normal file
12
apps/users/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ProfileView, SignupView, UserLoginView, UserLogoutView
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path("signup/", SignupView.as_view(), name="signup"),
|
||||
path("login/", UserLoginView.as_view(), name="login"),
|
||||
path("logout/", UserLogoutView.as_view(), name="logout"),
|
||||
path("profile/", ProfileView.as_view(), name="profile"),
|
||||
]
|
||||
31
apps/users/views.py
Normal file
31
apps/users/views.py
Normal file
@ -0,0 +1,31 @@
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import CreateView, TemplateView
|
||||
|
||||
from .forms import SignupForm
|
||||
|
||||
|
||||
class SignupView(CreateView):
|
||||
form_class = SignupForm
|
||||
template_name = "users/signup.html"
|
||||
success_url = reverse_lazy("core:dashboard")
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
login(self.request, self.object)
|
||||
return response
|
||||
|
||||
|
||||
class UserLoginView(LoginView):
|
||||
template_name = "users/login.html"
|
||||
redirect_authenticated_user = True
|
||||
|
||||
|
||||
class UserLogoutView(LogoutView):
|
||||
next_page = reverse_lazy("core:home")
|
||||
|
||||
|
||||
class ProfileView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "users/profile.html"
|
||||
@ -19,7 +19,9 @@ def env_list(key: str, default: str = "") -> list[str]:
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "insecure-development-secret")
|
||||
DEBUG = env_bool("DJANGO_DEBUG", False)
|
||||
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
|
||||
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS", "http://localhost,http://127.0.0.1")
|
||||
CSRF_TRUSTED_ORIGINS = env_list(
|
||||
"DJANGO_CSRF_TRUSTED_ORIGINS", "http://localhost,http://127.0.0.1"
|
||||
)
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
@ -29,6 +31,14 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"apps.core",
|
||||
"apps.users",
|
||||
"apps.players",
|
||||
"apps.competitions",
|
||||
"apps.teams",
|
||||
"apps.stats",
|
||||
"apps.scouting",
|
||||
"apps.providers",
|
||||
"apps.ingestion",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -86,11 +96,17 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
LOGIN_URL = "users:login"
|
||||
LOGIN_REDIRECT_URL = "core:dashboard"
|
||||
LOGOUT_REDIRECT_URL = "core:home"
|
||||
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")
|
||||
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://redis:6379/0")
|
||||
CELERY_ACCEPT_CONTENT = ["json"]
|
||||
|
||||
@ -4,4 +4,12 @@ from django.urls import include, path
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("apps.core.urls")),
|
||||
path("users/", include("apps.users.urls")),
|
||||
path("players/", include("apps.players.urls")),
|
||||
path("competitions/", include("apps.competitions.urls")),
|
||||
path("teams/", include("apps.teams.urls")),
|
||||
path("stats/", include("apps.stats.urls")),
|
||||
path("scouting/", include("apps.scouting.urls")),
|
||||
path("providers/", include("apps.providers.urls")),
|
||||
path("ingestion/", include("apps.ingestion.urls")),
|
||||
]
|
||||
|
||||
115
static/css/main.css
Normal file
115
static/css/main.css
Normal file
@ -0,0 +1,115 @@
|
||||
:root {
|
||||
--bg: #f7f8fa;
|
||||
--surface: #ffffff;
|
||||
--text: #132033;
|
||||
--muted: #4f6278;
|
||||
--line: #dbe2ea;
|
||||
--brand: #1163d9;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(960px, 92vw);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.row-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.row-gap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.brand {
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.button,
|
||||
.link-button {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--brand);
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
background: transparent;
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.stack p {
|
||||
margin: 0 0 0.8rem;
|
||||
}
|
||||
|
||||
.stack input {
|
||||
width: 100%;
|
||||
padding: 0.55rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
background: #e8f3ff;
|
||||
border: 1px solid #b7d6fa;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
39
templates/base.html
Normal file
39
templates/base.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}HoopScout{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container row-between">
|
||||
<a class="brand" href="{% url 'core:home' %}">HoopScout</a>
|
||||
<nav class="row-gap">
|
||||
<a href="{% url 'players:index' %}">Players</a>
|
||||
<a href="{% url 'competitions:index' %}">Competitions</a>
|
||||
<a href="{% url 'teams:index' %}">Teams</a>
|
||||
<a href="{% url 'scouting:index' %}">Scouting</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url 'core:dashboard' %}">Dashboard</a>
|
||||
<form method="post" action="{% url 'users:logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="link-button">Logout</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{% url 'users:login' %}">Login</a>
|
||||
<a href="{% url 'users:signup' %}">Signup</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% include 'partials/messages.html' %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
10
templates/competitions/index.html
Normal file
10
templates/competitions/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Competitions{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Competitions</h1>
|
||||
<p>Competitions module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
11
templates/core/dashboard.html
Normal file
11
templates/core/dashboard.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}HoopScout | Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome, {{ request.user.username }}.</p>
|
||||
<p>This is the authenticated control center for saved searches and watchlists.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
18
templates/core/home.html
Normal file
18
templates/core/home.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}HoopScout | Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Basketball scouting, server-rendered.</h1>
|
||||
<p>Search players and track targets using a Django + HTMX workflow.</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="{% url 'players:index' %}">Browse players</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a class="button ghost" href="{% url 'core:dashboard' %}">Go to dashboard</a>
|
||||
{% else %}
|
||||
<a class="button ghost" href="{% url 'users:signup' %}">Create account</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/ingestion/index.html
Normal file
10
templates/ingestion/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Ingestion{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Ingestion</h1>
|
||||
<p>Ingestion module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
7
templates/partials/messages.html
Normal file
7
templates/partials/messages.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% if messages %}
|
||||
<section class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
10
templates/players/index.html
Normal file
10
templates/players/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Players{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Players</h1>
|
||||
<p>Players module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/providers/index.html
Normal file
10
templates/providers/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Providers{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Providers</h1>
|
||||
<p>Providers module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/scouting/index.html
Normal file
10
templates/scouting/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Scouting{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Scouting</h1>
|
||||
<p>Scouting module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/stats/index.html
Normal file
10
templates/stats/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Stats{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Stats</h1>
|
||||
<p>Stats module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/teams/index.html
Normal file
10
templates/teams/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Teams{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Teams</h1>
|
||||
<p>Teams module scaffolding for upcoming phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
14
templates/users/login.html
Normal file
14
templates/users/login.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}HoopScout | Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel narrow">
|
||||
<h1>Login</h1>
|
||||
<form method="post" class="stack">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="button">Sign in</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
10
templates/users/profile.html
Normal file
10
templates/users/profile.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}HoopScout | Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Profile</h1>
|
||||
<p>Profile management scaffolding for future phases.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
14
templates/users/signup.html
Normal file
14
templates/users/signup.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}HoopScout | Signup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel narrow">
|
||||
<h1>Create account</h1>
|
||||
<form method="post" class="stack">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="button">Create account</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
23
tests/test_auth.py
Normal file
23
tests/test_auth.py
Normal file
@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_signup_creates_user(client):
|
||||
payload = {
|
||||
"username": "scout1",
|
||||
"email": "scout1@example.com",
|
||||
"password1": "StrongPass12345",
|
||||
"password2": "StrongPass12345",
|
||||
}
|
||||
response = client.post(reverse("users:signup"), data=payload)
|
||||
assert response.status_code == 302
|
||||
assert User.objects.filter(username="scout1").exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_requires_authentication(client):
|
||||
response = client.get(reverse("core:dashboard"))
|
||||
assert response.status_code == 302
|
||||
assert reverse("users:login") in response.url
|
||||
@ -4,6 +4,6 @@ from django.urls import reverse
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_health_endpoint(client):
|
||||
response = client.get(reverse("health"))
|
||||
response = client.get(reverse("core:health"))
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "ok"
|
||||
|
||||
Reference in New Issue
Block a user