phase5: add saved searches, watchlist, and authenticated htmx flows
This commit is contained in:
@ -3,6 +3,7 @@ from datetime import date
|
||||
from django.db.models import Prefetch
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from apps.scouting.models import FavoritePlayer
|
||||
from apps.stats.models import PlayerSeason
|
||||
|
||||
from .forms import PlayerSearchForm
|
||||
@ -56,6 +57,15 @@ class PlayerSearchView(ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["search_form"] = self.get_form()
|
||||
context["favorite_player_ids"] = set()
|
||||
if self.request.user.is_authenticated:
|
||||
player_ids = [player.id for player in context["players"]]
|
||||
context["favorite_player_ids"] = set(
|
||||
FavoritePlayer.objects.filter(
|
||||
user=self.request.user,
|
||||
player_id__in=player_ids,
|
||||
).values_list("player_id", flat=True)
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@ -122,4 +132,10 @@ class PlayerDetailView(DetailView):
|
||||
context["current_assignment"] = current_assignment
|
||||
context["career_entries"] = player.career_entries.all().order_by("-start_date", "-id")
|
||||
context["season_rows"] = season_rows
|
||||
context["is_favorite"] = False
|
||||
if self.request.user.is_authenticated:
|
||||
context["is_favorite"] = FavoritePlayer.objects.filter(
|
||||
user=self.request.user,
|
||||
player=player,
|
||||
).exists()
|
||||
return context
|
||||
|
||||
12
apps/scouting/forms.py
Normal file
12
apps/scouting/forms.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django import forms
|
||||
|
||||
from .models import SavedSearch
|
||||
|
||||
|
||||
class SavedSearchForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SavedSearch
|
||||
fields = ["name", "is_public"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"placeholder": "e.g. EuroLeague guards under 24"}),
|
||||
}
|
||||
38
apps/scouting/services/saved_searches.py
Normal file
38
apps/scouting/services/saved_searches.py
Normal file
@ -0,0 +1,38 @@
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from apps.players.forms import PlayerSearchForm
|
||||
|
||||
IGNORED_QUERY_KEYS = {"page", "csrfmiddlewaretoken"}
|
||||
|
||||
|
||||
def _serialize_value(value):
|
||||
if value is None:
|
||||
return None
|
||||
if hasattr(value, "pk"):
|
||||
return value.pk
|
||||
if isinstance(value, Decimal):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
||||
def extract_filters_from_params(params):
|
||||
payload = params.copy()
|
||||
for key in IGNORED_QUERY_KEYS:
|
||||
payload.pop(key, None)
|
||||
|
||||
form = PlayerSearchForm(payload)
|
||||
if not form.is_valid():
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for key, value in form.cleaned_data.items():
|
||||
serialized = _serialize_value(value)
|
||||
if serialized in (None, ""):
|
||||
continue
|
||||
result[key] = serialized
|
||||
return result
|
||||
|
||||
|
||||
def saved_search_to_querystring(filters: dict) -> str:
|
||||
return urlencode(filters, doseq=True)
|
||||
@ -1,9 +1,25 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ScoutingHomeView
|
||||
from .views import (
|
||||
FavoriteToggleView,
|
||||
SavedSearchCreateView,
|
||||
SavedSearchDeleteView,
|
||||
SavedSearchListView,
|
||||
SavedSearchRunView,
|
||||
SavedSearchUpdateView,
|
||||
ScoutingHomeView,
|
||||
WatchlistView,
|
||||
)
|
||||
|
||||
app_name = "scouting"
|
||||
|
||||
urlpatterns = [
|
||||
path("", ScoutingHomeView.as_view(), name="index"),
|
||||
path("saved-searches/", SavedSearchListView.as_view(), name="saved_search_list"),
|
||||
path("saved-searches/create/", SavedSearchCreateView.as_view(), name="saved_search_create"),
|
||||
path("saved-searches/<int:pk>/run/", SavedSearchRunView.as_view(), name="saved_search_run"),
|
||||
path("saved-searches/<int:pk>/edit/", SavedSearchUpdateView.as_view(), name="saved_search_edit"),
|
||||
path("saved-searches/<int:pk>/delete/", SavedSearchDeleteView.as_view(), name="saved_search_delete"),
|
||||
path("watchlist/", WatchlistView.as_view(), name="watchlist"),
|
||||
path("favorites/toggle/<int:player_id>/", FavoriteToggleView.as_view(), name="favorite_toggle"),
|
||||
]
|
||||
|
||||
@ -1,5 +1,159 @@
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
from django.views.generic import ListView, TemplateView, UpdateView
|
||||
|
||||
from .forms import SavedSearchForm
|
||||
from .models import FavoritePlayer, SavedSearch
|
||||
from .services.saved_searches import extract_filters_from_params, saved_search_to_querystring
|
||||
|
||||
|
||||
class ScoutingHomeView(TemplateView):
|
||||
class ScoutingHomeView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "scouting/index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["saved_searches"] = SavedSearch.objects.filter(user=self.request.user).order_by("-updated_at")
|
||||
context["favorites"] = (
|
||||
FavoritePlayer.objects.filter(user=self.request.user)
|
||||
.select_related("player", "player__nationality", "player__nominal_position", "player__inferred_role")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class SavedSearchCreateView(LoginRequiredMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = SavedSearchForm(request.POST)
|
||||
if not form.is_valid():
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(
|
||||
request,
|
||||
"scouting/partials/save_search_feedback.html",
|
||||
{"ok": False, "message": "Invalid name or visibility values."},
|
||||
)
|
||||
messages.error(request, "Could not save search.")
|
||||
return redirect("players:index")
|
||||
|
||||
filters = extract_filters_from_params(request.POST)
|
||||
if not filters:
|
||||
message = "No valid filters to save from current search."
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(
|
||||
request,
|
||||
"scouting/partials/save_search_feedback.html",
|
||||
{"ok": False, "message": message},
|
||||
)
|
||||
messages.error(request, message)
|
||||
return redirect("players:index")
|
||||
|
||||
saved_search = form.save(commit=False)
|
||||
saved_search.user = request.user
|
||||
saved_search.filters = filters
|
||||
try:
|
||||
saved_search.save()
|
||||
except IntegrityError:
|
||||
message = "A saved search with this name already exists."
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(
|
||||
request,
|
||||
"scouting/partials/save_search_feedback.html",
|
||||
{"ok": False, "message": message},
|
||||
)
|
||||
messages.error(request, message)
|
||||
return redirect("players:index")
|
||||
|
||||
message = f"Saved search '{saved_search.name}' created."
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(
|
||||
request,
|
||||
"scouting/partials/save_search_feedback.html",
|
||||
{"ok": True, "message": message},
|
||||
)
|
||||
|
||||
messages.success(request, message)
|
||||
return redirect("scouting:index")
|
||||
|
||||
|
||||
class SavedSearchUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = SavedSearch
|
||||
form_class = SavedSearchForm
|
||||
template_name = "scouting/saved_search_edit.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return SavedSearch.objects.filter(user=self.request.user)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Saved search updated.")
|
||||
return reverse("scouting:index")
|
||||
|
||||
|
||||
class SavedSearchDeleteView(LoginRequiredMixin, View):
|
||||
def post(self, request, pk, *args, **kwargs):
|
||||
saved_search = get_object_or_404(SavedSearch, pk=pk, user=request.user)
|
||||
saved_search.delete()
|
||||
messages.success(request, "Saved search deleted.")
|
||||
return redirect("scouting:index")
|
||||
|
||||
|
||||
class SavedSearchRunView(LoginRequiredMixin, View):
|
||||
def get(self, request, pk, *args, **kwargs):
|
||||
saved_search = get_object_or_404(SavedSearch, pk=pk, user=request.user)
|
||||
query = saved_search_to_querystring(saved_search.filters)
|
||||
saved_search.last_run_at = timezone.now()
|
||||
saved_search.save(update_fields=["last_run_at"])
|
||||
target = reverse("players:index")
|
||||
return redirect(f"{target}?{query}" if query else target)
|
||||
|
||||
|
||||
class WatchlistView(LoginRequiredMixin, ListView):
|
||||
model = FavoritePlayer
|
||||
context_object_name = "favorites"
|
||||
template_name = "scouting/watchlist.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
FavoritePlayer.objects.filter(user=self.request.user)
|
||||
.select_related("player", "player__nationality", "player__nominal_position", "player__inferred_role")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
|
||||
class FavoriteToggleView(LoginRequiredMixin, View):
|
||||
def post(self, request, player_id, *args, **kwargs):
|
||||
from apps.players.models import Player
|
||||
|
||||
player = get_object_or_404(Player, pk=player_id)
|
||||
next_url = request.POST.get("next") or request.META.get("HTTP_REFERER") or reverse("players:index")
|
||||
|
||||
favorite, created = FavoritePlayer.objects.get_or_create(user=request.user, player=player)
|
||||
if not created:
|
||||
favorite.delete()
|
||||
|
||||
is_favorite = created
|
||||
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return render(
|
||||
request,
|
||||
"scouting/partials/favorite_button.html",
|
||||
{
|
||||
"player": player,
|
||||
"is_favorite": is_favorite,
|
||||
"next_url": next_url,
|
||||
},
|
||||
)
|
||||
|
||||
return redirect(next_url)
|
||||
|
||||
|
||||
class SavedSearchListView(LoginRequiredMixin, ListView):
|
||||
model = SavedSearch
|
||||
context_object_name = "saved_searches"
|
||||
template_name = "scouting/saved_search_list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return SavedSearch.objects.filter(user=self.request.user).order_by("-updated_at")
|
||||
|
||||
@ -114,6 +114,16 @@ main {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #e9f9ef;
|
||||
border-color: #bde8ca;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fff0f0;
|
||||
border-color: #f0b8b8;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@ -205,3 +215,9 @@ th {
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.inline-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
@ -12,8 +12,13 @@
|
||||
· {{ player.inferred_role.name|default:"No inferred role" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-gap">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% include "scouting/partials/favorite_button.html" with player=player is_favorite=is_favorite next_url=request.get_full_path %}
|
||||
{% endif %}
|
||||
<a class="button ghost" href="{% url 'players:index' %}">Back to search</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid mt-16">
|
||||
<div class="detail-card">
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
{% include "scouting/partials/save_search_form.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if players %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@ -21,6 +25,7 @@
|
||||
<th>PPG</th>
|
||||
<th>RPG</th>
|
||||
<th>APG</th>
|
||||
{% if request.user.is_authenticated %}<th>Watchlist</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -40,6 +45,15 @@
|
||||
<td>{{ player.ppg_value|floatformat:1 }}</td>
|
||||
<td>{{ player.rpg_value|floatformat:1 }}</td>
|
||||
<td>{{ player.apg_value|floatformat:1 }}</td>
|
||||
{% if request.user.is_authenticated %}
|
||||
<td>
|
||||
{% if player.id in favorite_player_ids %}
|
||||
{% include "scouting/partials/favorite_button.html" with player=player is_favorite=True next_url=request.get_full_path %}
|
||||
{% else %}
|
||||
{% include "scouting/partials/favorite_button.html" with player=player is_favorite=False next_url=request.get_full_path %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -4,7 +4,25 @@
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Scouting</h1>
|
||||
<p>Scouting module scaffolding for upcoming phases.</p>
|
||||
<div class="row-between wrap-gap">
|
||||
<div>
|
||||
<h1>Scouting Workspace</h1>
|
||||
<p class="muted-text">Manage saved searches and your player watchlist.</p>
|
||||
</div>
|
||||
<div class="row-gap">
|
||||
<a class="button ghost" href="{% url 'scouting:saved_search_list' %}">All saved searches</a>
|
||||
<a class="button ghost" href="{% url 'scouting:watchlist' %}">Watchlist</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<h2>Saved Searches</h2>
|
||||
{% include "scouting/partials/saved_search_table.html" with saved_searches=saved_searches %}
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<h2>Watchlist</h2>
|
||||
{% include "scouting/partials/watchlist_table.html" with favorites=favorites %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
16
templates/scouting/partials/favorite_button.html
Normal file
16
templates/scouting/partials/favorite_button.html
Normal file
@ -0,0 +1,16 @@
|
||||
<form
|
||||
id="favorite-form-{{ player.id }}"
|
||||
method="post"
|
||||
action="{% url 'scouting:favorite_toggle' player.id %}"
|
||||
hx-post="{% url 'scouting:favorite_toggle' player.id %}"
|
||||
hx-target="#favorite-form-{{ player.id }}"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next_url }}">
|
||||
{% if is_favorite %}
|
||||
<button type="submit" class="button ghost">Remove favorite</button>
|
||||
{% else %}
|
||||
<button type="submit" class="button ghost">Add favorite</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
3
templates/scouting/partials/save_search_feedback.html
Normal file
3
templates/scouting/partials/save_search_feedback.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="message {% if ok %}success{% else %}error{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
27
templates/scouting/partials/save_search_form.html
Normal file
27
templates/scouting/partials/save_search_form.html
Normal file
@ -0,0 +1,27 @@
|
||||
<div class="panel mt-16">
|
||||
<h3>Save Current Search</h3>
|
||||
<p class="muted-text">Store current filters and replay them later.</p>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="{% url 'scouting:saved_search_create' %}"
|
||||
class="row-gap"
|
||||
hx-post="{% url 'scouting:saved_search_create' %}"
|
||||
hx-target="#saved-search-feedback"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="text" name="name" placeholder="Search name" required>
|
||||
<label class="inline-label">
|
||||
<input type="checkbox" name="is_public">
|
||||
Public
|
||||
</label>
|
||||
|
||||
{% for key, value in request.GET.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
|
||||
<button class="button" type="submit">Save search</button>
|
||||
</form>
|
||||
<div id="saved-search-feedback" class="mt-16"></div>
|
||||
</div>
|
||||
37
templates/scouting/partials/saved_search_table.html
Normal file
37
templates/scouting/partials/saved_search_table.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% if saved_searches %}
|
||||
<div class="table-wrap mt-16">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visibility</th>
|
||||
<th>Updated</th>
|
||||
<th>Last run</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for saved_search in saved_searches %}
|
||||
<tr>
|
||||
<td>{{ saved_search.name }}</td>
|
||||
<td>{% if saved_search.is_public %}Public{% else %}Private{% endif %}</td>
|
||||
<td>{{ saved_search.updated_at|date:"Y-m-d H:i" }}</td>
|
||||
<td>{{ saved_search.last_run_at|date:"Y-m-d H:i"|default:"-" }}</td>
|
||||
<td>
|
||||
<div class="row-gap">
|
||||
<a class="button ghost" href="{% url 'scouting:saved_search_run' saved_search.pk %}">Run</a>
|
||||
<a class="button ghost" href="{% url 'scouting:saved_search_edit' saved_search.pk %}">Edit</a>
|
||||
<form method="post" action="{% url 'scouting:saved_search_delete' saved_search.pk %}">
|
||||
{% csrf_token %}
|
||||
<button class="button ghost" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="mt-16">No saved searches yet.</p>
|
||||
{% endif %}
|
||||
35
templates/scouting/partials/watchlist_table.html
Normal file
35
templates/scouting/partials/watchlist_table.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% if favorites %}
|
||||
<div class="table-wrap mt-16">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Nationality</th>
|
||||
<th>Position / Role</th>
|
||||
<th>Added</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for favorite in favorites %}
|
||||
<tr>
|
||||
<td><a href="{% url 'players:detail' favorite.player_id %}">{{ favorite.player.full_name }}</a></td>
|
||||
<td>{{ favorite.player.nationality.name|default:"-" }}</td>
|
||||
<td>
|
||||
{{ favorite.player.nominal_position.code|default:"-" }}
|
||||
/ {{ favorite.player.inferred_role.name|default:"-" }}
|
||||
</td>
|
||||
<td>{{ favorite.created_at|date:"Y-m-d" }}</td>
|
||||
<td>
|
||||
<div id="favorite-toggle-{{ favorite.player_id }}">
|
||||
{% include "scouting/partials/favorite_button.html" with player=favorite.player is_favorite=True next_url=request.get_full_path %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="mt-16">No players in your watchlist yet.</p>
|
||||
{% endif %}
|
||||
17
templates/scouting/saved_search_edit.html
Normal file
17
templates/scouting/saved_search_edit.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Edit Saved Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel narrow">
|
||||
<h1>Edit Saved Search</h1>
|
||||
<form method="post" class="stack">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="row-gap">
|
||||
<button type="submit" class="button">Update</button>
|
||||
<a class="button ghost" href="{% url 'scouting:index' %}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
13
templates/scouting/saved_search_list.html
Normal file
13
templates/scouting/saved_search_list.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Saved Searches{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<div class="row-between wrap-gap">
|
||||
<h1>Saved Searches</h1>
|
||||
<a class="button ghost" href="{% url 'scouting:index' %}">Back to scouting</a>
|
||||
</div>
|
||||
{% include "scouting/partials/saved_search_table.html" with saved_searches=saved_searches %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
13
templates/scouting/watchlist.html
Normal file
13
templates/scouting/watchlist.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Watchlist{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<div class="row-between wrap-gap">
|
||||
<h1>Watchlist</h1>
|
||||
<a class="button ghost" href="{% url 'scouting:index' %}">Back to scouting</a>
|
||||
</div>
|
||||
{% include "scouting/partials/watchlist_table.html" with favorites=favorites %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
87
tests/test_scouting_views.py
Normal file
87
tests/test_scouting_views.py
Normal file
@ -0,0 +1,87 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.players.models import Nationality, Player, Position, Role
|
||||
from apps.scouting.models import FavoritePlayer, SavedSearch
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scouting_index_requires_login(client):
|
||||
response = client.get(reverse("scouting:index"))
|
||||
assert response.status_code == 302
|
||||
assert reverse("users:login") in response.url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_saved_search_from_filters(client):
|
||||
user = User.objects.create_user(username="scout", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
nationality = Nationality.objects.create(name="Italy", iso2_code="IT", iso3_code="ITA")
|
||||
position = Position.objects.create(code="PG", name="Point Guard")
|
||||
|
||||
response = client.post(
|
||||
reverse("scouting:saved_search_create"),
|
||||
data={
|
||||
"name": "Italian guards",
|
||||
"is_public": "on",
|
||||
"q": "marco",
|
||||
"nominal_position": str(position.id),
|
||||
"nationality": str(nationality.id),
|
||||
"age_max": "24",
|
||||
"sort": "ppg_desc",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
saved = SavedSearch.objects.get(user=user, name="Italian guards")
|
||||
assert saved.filters["q"] == "marco"
|
||||
assert saved.filters["nominal_position"] == position.id
|
||||
assert saved.filters["nationality"] == nationality.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_saved_search_run_redirects_to_players(client):
|
||||
user = User.objects.create_user(username="scout2", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
saved = SavedSearch.objects.create(
|
||||
user=user,
|
||||
name="Run me",
|
||||
filters={"q": "rossi", "sort": "name_asc"},
|
||||
)
|
||||
|
||||
response = client.get(reverse("scouting:saved_search_run", kwargs={"pk": saved.pk}))
|
||||
assert response.status_code == 302
|
||||
assert response.url.startswith(reverse("players:index"))
|
||||
assert "q=rossi" in response.url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_favorite_toggle_adds_and_removes(client):
|
||||
user = User.objects.create_user(username="scout3", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
nationality = Nationality.objects.create(name="Spain", iso2_code="ES", iso3_code="ESP")
|
||||
position = Position.objects.create(code="SF", name="Small Forward")
|
||||
role = Role.objects.create(code="wing", name="Wing")
|
||||
player = Player.objects.create(
|
||||
first_name="Juan",
|
||||
last_name="Ramos",
|
||||
full_name="Juan Ramos",
|
||||
birth_date=date(2000, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
add_resp = client.post(reverse("scouting:favorite_toggle", kwargs={"player_id": player.id}))
|
||||
assert add_resp.status_code == 302
|
||||
assert FavoritePlayer.objects.filter(user=user, player=player).exists()
|
||||
|
||||
remove_resp = client.post(reverse("scouting:favorite_toggle", kwargs={"player_id": player.id}))
|
||||
assert remove_resp.status_code == 302
|
||||
assert not FavoritePlayer.objects.filter(user=user, player=player).exists()
|
||||
Reference in New Issue
Block a user