diff --git a/README.md b/README.md index 591868e..d88a340 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ The current application baseline provides: - player scouting search with player, context, and stat filters - matching season/team/competition context on search results - result sorting and pagination -- a shared development shortlist for favorite players -- shared plain-text scouting notes on player detail pages +- login/logout with Django built-in authentication +- user-scoped shortlist favorites +- user-scoped plain-text scouting notes on player detail pages Accepted technical and product-shaping decisions live in: - `docs/ARCHITECTURE.md` @@ -48,9 +49,12 @@ Accepted technical and product-shaping decisions live in: 1. Start the stack with `docker compose --env-file .env -f infra/docker-compose.yml up -d --build`. 2. Apply migrations with `docker compose --env-file .env -f infra/docker-compose.yml exec -T app python manage.py migrate`. 3. Load sample data with `docker compose --env-file .env -f infra/docker-compose.yml exec -T app python manage.py seed_scouting_data`. -4. Visit `http://127.0.0.1:8000/players/` to explore the scouting search MVP. -5. Use player detail pages to manage shortlist entries and shared scouting notes. -6. Use `http://127.0.0.1:8000/favorites/` to review the shared development shortlist. +4. Create a local user with `docker compose --env-file .env -f infra/docker-compose.yml exec -T app python manage.py createsuperuser` if you need a development login. +5. Visit `http://127.0.0.1:8000/players/` to explore the scouting search MVP. +6. Log in at `http://127.0.0.1:8000/accounts/login/` to manage your own shortlist and notes. +7. Use `http://127.0.0.1:8000/favorites/` to review your user-scoped shortlist. + +Legacy shared favorites and notes from the pre-auth MVP are cleared by the early-stage ownership migration so the app can move cleanly to user-scoped data. ## Workflow diff --git a/app/hoopscout/settings.py b/app/hoopscout/settings.py index 857138e..eafb22c 100644 --- a/app/hoopscout/settings.py +++ b/app/hoopscout/settings.py @@ -66,3 +66,7 @@ USE_TZ = True STATIC_URL = "static/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "scouting:player_list" +LOGOUT_REDIRECT_URL = "scouting:player_list" diff --git a/app/hoopscout/urls.py b/app/hoopscout/urls.py index 4ded0db..0009b89 100644 --- a/app/hoopscout/urls.py +++ b/app/hoopscout/urls.py @@ -1,7 +1,10 @@ from django.contrib import admin +from django.contrib.auth import views as auth_views from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/login/", auth_views.LoginView.as_view(template_name="registration/login.html"), name="login"), + path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"), path("", include("scouting.urls")), ] diff --git a/app/scouting/admin.py b/app/scouting/admin.py index 09aa45c..50b0ffa 100644 --- a/app/scouting/admin.py +++ b/app/scouting/admin.py @@ -74,11 +74,11 @@ class PlayerSeasonStatsAdmin(admin.ModelAdmin): @admin.register(FavoritePlayer) class FavoritePlayerAdmin(admin.ModelAdmin): - list_display = ("player", "created_at") - search_fields = ("player__full_name",) + list_display = ("user", "player", "created_at") + search_fields = ("user__username", "player__full_name") @admin.register(PlayerNote) class PlayerNoteAdmin(admin.ModelAdmin): - list_display = ("player", "created_at", "updated_at") - search_fields = ("player__full_name", "body") + list_display = ("user", "player", "created_at", "updated_at") + search_fields = ("user__username", "player__full_name", "body") diff --git a/app/scouting/migrations/0007_user_scoped_favorites_and_notes.py b/app/scouting/migrations/0007_user_scoped_favorites_and_notes.py new file mode 100644 index 0000000..56bc10c --- /dev/null +++ b/app/scouting/migrations/0007_user_scoped_favorites_and_notes.py @@ -0,0 +1,75 @@ +from django.conf import settings +from django.db import migrations, models + + +def clear_shared_shortlist_and_notes(apps, schema_editor): + FavoritePlayer = apps.get_model("scouting", "FavoritePlayer") + PlayerNote = apps.get_model("scouting", "PlayerNote") + FavoritePlayer.objects.all().delete() + PlayerNote.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("scouting", "0006_playernote"), + ] + + operations = [ + migrations.AddField( + model_name="favoriteplayer", + name="user", + field=models.ForeignKey( + null=True, + on_delete=models.CASCADE, + related_name="favorite_players", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="playernote", + name="user", + field=models.ForeignKey( + null=True, + on_delete=models.CASCADE, + related_name="player_notes", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.RunPython(clear_shared_shortlist_and_notes, migrations.RunPython.noop), + migrations.RemoveField( + model_name="favoriteplayer", + name="player", + ), + migrations.AddField( + model_name="favoriteplayer", + name="player", + field=models.ForeignKey( + on_delete=models.CASCADE, + related_name="favorite_entries", + to="scouting.player", + ), + ), + migrations.AlterField( + model_name="favoriteplayer", + name="user", + field=models.ForeignKey( + on_delete=models.CASCADE, + related_name="favorite_players", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="playernote", + name="user", + field=models.ForeignKey( + on_delete=models.CASCADE, + related_name="player_notes", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddConstraint( + model_name="favoriteplayer", + constraint=models.UniqueConstraint(fields=("user", "player"), name="uniq_favorite_player_per_user"), + ), + ] diff --git a/app/scouting/models.py b/app/scouting/models.py index adfff75..0b4659d 100644 --- a/app/scouting/models.py +++ b/app/scouting/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import models @@ -164,25 +165,34 @@ class PlayerSeasonStats(models.Model): class FavoritePlayer(models.Model): - # Phase-2 MVP uses a single shared development shortlist instead of user-scoped - # favorites so the workflow stays useful without introducing auth complexity yet. - player = models.OneToOneField( + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="favorite_players", + ) + player = models.ForeignKey( Player, on_delete=models.CASCADE, - related_name="favorite_entry", + related_name="favorite_entries", ) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at", "player__full_name"] + constraints = [ + models.UniqueConstraint(fields=["user", "player"], name="uniq_favorite_player_per_user"), + ] def __str__(self) -> str: - return f"Favorite: {self.player.full_name}" + return f"Favorite: {self.user} -> {self.player.full_name}" class PlayerNote(models.Model): - # Phase-2 MVP keeps notes shared within the local development environment so - # scouting observations can be used immediately without introducing auth yet. + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="player_notes", + ) player = models.ForeignKey( Player, on_delete=models.CASCADE, @@ -196,4 +206,4 @@ class PlayerNote(models.Model): ordering = ["-created_at", "-id"] def __str__(self) -> str: - return f"Note for {self.player.full_name}" + return f"Note by {self.user} for {self.player.full_name}" diff --git a/app/scouting/templates/registration/login.html b/app/scouting/templates/registration/login.html new file mode 100644 index 0000000..133c416 --- /dev/null +++ b/app/scouting/templates/registration/login.html @@ -0,0 +1,24 @@ + + + + + Log In + + +

Back to search

+

Log In

+ + {% if form.errors %} +

Your username and password did not match. Please try again.

+ {% endif %} + +
+ {% csrf_token %} + {{ form.as_p }} + {% if next %} + + {% endif %} + +
+ + diff --git a/app/scouting/templates/scouting/favorites_list.html b/app/scouting/templates/scouting/favorites_list.html index e26766a..d39bf97 100644 --- a/app/scouting/templates/scouting/favorites_list.html +++ b/app/scouting/templates/scouting/favorites_list.html @@ -7,9 +7,14 @@

Back to search + | Signed in as {{ request.user.username }} + |

+ {% csrf_token %} + +

-

Shared Development Shortlist

-

This MVP shortlist is shared across the local development environment.

+

Your Shortlist

+

This page shows favorites saved only for your account.