feat(players): add origin competition/team model and filtering
This commit is contained in:
@ -11,6 +11,7 @@ from apps.competitions.models import Competition, Season
|
|||||||
from apps.ingestion.models import IngestionRun
|
from apps.ingestion.models import IngestionRun
|
||||||
from apps.ingestion.services.runs import finish_ingestion_run, log_ingestion_error, start_ingestion_run
|
from apps.ingestion.services.runs import finish_ingestion_run, log_ingestion_error, start_ingestion_run
|
||||||
from apps.players.models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
from apps.players.models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
||||||
|
from apps.players.services.origin import refresh_player_origin
|
||||||
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
|
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
|
||||||
from apps.providers.registry import get_provider
|
from apps.providers.registry import get_provider
|
||||||
from apps.providers.services.mappings import upsert_external_mapping
|
from apps.providers.services.mappings import upsert_external_mapping
|
||||||
@ -358,6 +359,7 @@ def _sync_player_stats(provider_namespace: str, payloads: list[dict], run: Inges
|
|||||||
|
|
||||||
|
|
||||||
def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: IngestionRun, summary: SyncSummary):
|
def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: IngestionRun, summary: SyncSummary):
|
||||||
|
touched_player_ids: set[int] = set()
|
||||||
for payload in payloads:
|
for payload in payloads:
|
||||||
summary.processed += 1
|
summary.processed += 1
|
||||||
external_id = payload.get("external_id", "")
|
external_id = payload.get("external_id", "")
|
||||||
@ -380,6 +382,7 @@ def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: Ing
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
touched_player_ids.add(player.id)
|
||||||
_, created = PlayerCareerEntry.objects.update_or_create(
|
_, created = PlayerCareerEntry.objects.update_or_create(
|
||||||
player=player,
|
player=player,
|
||||||
team=team,
|
team=team,
|
||||||
@ -399,6 +402,10 @@ def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: Ing
|
|||||||
else:
|
else:
|
||||||
summary.updated += 1
|
summary.updated += 1
|
||||||
|
|
||||||
|
if touched_player_ids:
|
||||||
|
for player in Player.objects.filter(id__in=touched_player_ids):
|
||||||
|
refresh_player_origin(player)
|
||||||
|
|
||||||
|
|
||||||
def run_sync_job(
|
def run_sync_job(
|
||||||
*,
|
*,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from .models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
from .models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
||||||
|
from .services.origin import refresh_player_origins
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Nationality)
|
@admin.register(Nationality)
|
||||||
@ -39,11 +41,26 @@ class PlayerAdmin(admin.ModelAdmin):
|
|||||||
"nationality",
|
"nationality",
|
||||||
"nominal_position",
|
"nominal_position",
|
||||||
"inferred_role",
|
"inferred_role",
|
||||||
|
"origin_competition",
|
||||||
|
"origin_team",
|
||||||
"is_active",
|
"is_active",
|
||||||
)
|
)
|
||||||
list_filter = ("is_active", "nationality", "nominal_position", "inferred_role")
|
list_filter = (
|
||||||
|
"is_active",
|
||||||
|
"nationality",
|
||||||
|
"nominal_position",
|
||||||
|
"inferred_role",
|
||||||
|
"origin_competition",
|
||||||
|
"origin_team",
|
||||||
|
)
|
||||||
search_fields = ("full_name", "first_name", "last_name")
|
search_fields = ("full_name", "first_name", "last_name")
|
||||||
inlines = (PlayerAliasInline, PlayerCareerEntryInline)
|
inlines = (PlayerAliasInline, PlayerCareerEntryInline)
|
||||||
|
actions = ("recompute_origin_fields",)
|
||||||
|
|
||||||
|
@admin.action(description="Recompute origin fields")
|
||||||
|
def recompute_origin_fields(self, request, queryset):
|
||||||
|
updated = refresh_player_origins(queryset)
|
||||||
|
self.message_user(request, f"Updated origin fields for {updated} player(s).", level=messages.SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(PlayerAlias)
|
@admin.register(PlayerAlias)
|
||||||
|
|||||||
@ -25,8 +25,10 @@ class PlayerSearchForm(forms.Form):
|
|||||||
nominal_position = forms.ModelChoiceField(queryset=Position.objects.none(), required=False)
|
nominal_position = forms.ModelChoiceField(queryset=Position.objects.none(), required=False)
|
||||||
inferred_role = forms.ModelChoiceField(queryset=Role.objects.none(), required=False)
|
inferred_role = forms.ModelChoiceField(queryset=Role.objects.none(), required=False)
|
||||||
competition = forms.ModelChoiceField(queryset=Competition.objects.none(), required=False)
|
competition = forms.ModelChoiceField(queryset=Competition.objects.none(), required=False)
|
||||||
|
origin_competition = forms.ModelChoiceField(queryset=Competition.objects.none(), required=False)
|
||||||
nationality = forms.ModelChoiceField(queryset=Nationality.objects.none(), required=False)
|
nationality = forms.ModelChoiceField(queryset=Nationality.objects.none(), required=False)
|
||||||
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False)
|
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False)
|
||||||
|
origin_team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False)
|
||||||
season = forms.ModelChoiceField(queryset=Season.objects.none(), required=False)
|
season = forms.ModelChoiceField(queryset=Season.objects.none(), required=False)
|
||||||
|
|
||||||
age_min = forms.IntegerField(required=False, min_value=0, max_value=60, label="Min age")
|
age_min = forms.IntegerField(required=False, min_value=0, max_value=60, label="Min age")
|
||||||
@ -86,8 +88,10 @@ class PlayerSearchForm(forms.Form):
|
|||||||
self.fields["nominal_position"].queryset = Position.objects.order_by("code")
|
self.fields["nominal_position"].queryset = Position.objects.order_by("code")
|
||||||
self.fields["inferred_role"].queryset = Role.objects.order_by("name")
|
self.fields["inferred_role"].queryset = Role.objects.order_by("name")
|
||||||
self.fields["competition"].queryset = Competition.objects.order_by("name")
|
self.fields["competition"].queryset = Competition.objects.order_by("name")
|
||||||
|
self.fields["origin_competition"].queryset = Competition.objects.order_by("name")
|
||||||
self.fields["nationality"].queryset = Nationality.objects.order_by("name")
|
self.fields["nationality"].queryset = Nationality.objects.order_by("name")
|
||||||
self.fields["team"].queryset = Team.objects.order_by("name")
|
self.fields["team"].queryset = Team.objects.order_by("name")
|
||||||
|
self.fields["origin_team"].queryset = Team.objects.order_by("name")
|
||||||
self.fields["season"].queryset = Season.objects.order_by("-start_date")
|
self.fields["season"].queryset = Season.objects.order_by("-start_date")
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-03-10 11:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('competitions', '0002_initial'),
|
||||||
|
('players', '0002_initial'),
|
||||||
|
('teams', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='player',
|
||||||
|
name='origin_competition',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_players', to='competitions.competition'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='player',
|
||||||
|
name='origin_team',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_players', to='teams.team'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='player',
|
||||||
|
index=models.Index(fields=['origin_competition'], name='players_pla_origin__1a711b_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='player',
|
||||||
|
index=models.Index(fields=['origin_team'], name='players_pla_origin__b33403_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
35
apps/players/migrations/0004_backfill_player_origins.py
Normal file
35
apps/players/migrations/0004_backfill_player_origins.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import F, Q
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_player_origins(apps, schema_editor):
|
||||||
|
Player = apps.get_model("players", "Player")
|
||||||
|
PlayerCareerEntry = apps.get_model("players", "PlayerCareerEntry")
|
||||||
|
|
||||||
|
for player in Player.objects.all().iterator():
|
||||||
|
entry = (
|
||||||
|
PlayerCareerEntry.objects.filter(player=player)
|
||||||
|
.filter(Q(competition__isnull=False) | Q(team__isnull=False))
|
||||||
|
.order_by(
|
||||||
|
F("start_date").asc(nulls_last=True),
|
||||||
|
F("season__start_date").asc(nulls_last=True),
|
||||||
|
"id",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if entry is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
player.origin_competition_id = entry.competition_id
|
||||||
|
player.origin_team_id = entry.team_id
|
||||||
|
player.save(update_fields=["origin_competition", "origin_team"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("players", "0003_player_origin_competition_player_origin_team_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(backfill_player_origins, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -80,6 +80,20 @@ class Player(TimeStampedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
related_name="role_players",
|
related_name="role_players",
|
||||||
)
|
)
|
||||||
|
origin_competition = models.ForeignKey(
|
||||||
|
"competitions.Competition",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="origin_players",
|
||||||
|
)
|
||||||
|
origin_team = models.ForeignKey(
|
||||||
|
"teams.Team",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="origin_players",
|
||||||
|
)
|
||||||
height_cm = models.PositiveSmallIntegerField(blank=True, null=True)
|
height_cm = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||||
weight_kg = models.PositiveSmallIntegerField(blank=True, null=True)
|
weight_kg = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||||
wingspan_cm = models.PositiveSmallIntegerField(blank=True, null=True)
|
wingspan_cm = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||||
@ -105,6 +119,8 @@ class Player(TimeStampedModel):
|
|||||||
models.Index(fields=["nationality"]),
|
models.Index(fields=["nationality"]),
|
||||||
models.Index(fields=["nominal_position"]),
|
models.Index(fields=["nominal_position"]),
|
||||||
models.Index(fields=["inferred_role"]),
|
models.Index(fields=["inferred_role"]),
|
||||||
|
models.Index(fields=["origin_competition"]),
|
||||||
|
models.Index(fields=["origin_team"]),
|
||||||
models.Index(fields=["is_active"]),
|
models.Index(fields=["is_active"]),
|
||||||
models.Index(fields=["height_cm"]),
|
models.Index(fields=["height_cm"]),
|
||||||
]
|
]
|
||||||
|
|||||||
46
apps/players/services/origin.py
Normal file
46
apps/players/services/origin.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from django.db.models import F, Q, QuerySet
|
||||||
|
|
||||||
|
from apps.players.models import Player, PlayerCareerEntry
|
||||||
|
|
||||||
|
|
||||||
|
def get_origin_career_entry(player: Player) -> PlayerCareerEntry | None:
|
||||||
|
"""Earliest meaningful career entry, ordered by start_date then season start date."""
|
||||||
|
return (
|
||||||
|
PlayerCareerEntry.objects.select_related("competition", "team", "season")
|
||||||
|
.filter(player=player)
|
||||||
|
.filter(Q(competition__isnull=False) | Q(team__isnull=False))
|
||||||
|
.order_by(
|
||||||
|
F("start_date").asc(nulls_last=True),
|
||||||
|
F("season__start_date").asc(nulls_last=True),
|
||||||
|
"id",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_player_origin(player: Player, *, save: bool = True) -> bool:
|
||||||
|
"""Update origin fields from earliest meaningful career entry."""
|
||||||
|
entry = get_origin_career_entry(player)
|
||||||
|
origin_competition = entry.competition if entry else None
|
||||||
|
origin_team = entry.team if entry else None
|
||||||
|
|
||||||
|
changed = (
|
||||||
|
player.origin_competition_id != (origin_competition.id if origin_competition else None)
|
||||||
|
or player.origin_team_id != (origin_team.id if origin_team else None)
|
||||||
|
)
|
||||||
|
if changed:
|
||||||
|
player.origin_competition = origin_competition
|
||||||
|
player.origin_team = origin_team
|
||||||
|
if save:
|
||||||
|
player.save(update_fields=["origin_competition", "origin_team", "updated_at"])
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_player_origins(queryset: QuerySet[Player] | None = None) -> int:
|
||||||
|
"""Backfill/recompute origin fields for players in queryset."""
|
||||||
|
players = queryset if queryset is not None else Player.objects.all()
|
||||||
|
updated = 0
|
||||||
|
for player in players.iterator():
|
||||||
|
if refresh_player_origin(player):
|
||||||
|
updated += 1
|
||||||
|
return updated
|
||||||
@ -47,6 +47,10 @@ def filter_players(queryset, data: dict):
|
|||||||
queryset = queryset.filter(inferred_role=data["inferred_role"])
|
queryset = queryset.filter(inferred_role=data["inferred_role"])
|
||||||
if data.get("nationality"):
|
if data.get("nationality"):
|
||||||
queryset = queryset.filter(nationality=data["nationality"])
|
queryset = queryset.filter(nationality=data["nationality"])
|
||||||
|
if data.get("origin_competition"):
|
||||||
|
queryset = queryset.filter(origin_competition=data["origin_competition"])
|
||||||
|
if data.get("origin_team"):
|
||||||
|
queryset = queryset.filter(origin_team=data["origin_team"])
|
||||||
|
|
||||||
if data.get("team"):
|
if data.get("team"):
|
||||||
queryset = queryset.filter(player_seasons__team=data["team"])
|
queryset = queryset.filter(player_seasons__team=data["team"])
|
||||||
@ -185,4 +189,6 @@ def base_player_queryset():
|
|||||||
"nationality",
|
"nationality",
|
||||||
"nominal_position",
|
"nominal_position",
|
||||||
"inferred_role",
|
"inferred_role",
|
||||||
|
"origin_competition",
|
||||||
|
"origin_team",
|
||||||
).prefetch_related("aliases")
|
).prefetch_related("aliases")
|
||||||
|
|||||||
@ -87,6 +87,8 @@ class PlayerDetailView(DetailView):
|
|||||||
"nationality",
|
"nationality",
|
||||||
"nominal_position",
|
"nominal_position",
|
||||||
"inferred_role",
|
"inferred_role",
|
||||||
|
"origin_competition",
|
||||||
|
"origin_team",
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"aliases",
|
"aliases",
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<h2>Summary</h2>
|
<h2>Summary</h2>
|
||||||
<p><strong>Nationality:</strong> {{ player.nationality.name|default:"-" }}</p>
|
<p><strong>Nationality:</strong> {{ player.nationality.name|default:"-" }}</p>
|
||||||
|
<p><strong>Origin competition:</strong> {{ player.origin_competition.name|default:"-" }}</p>
|
||||||
|
<p><strong>Origin team:</strong> {{ player.origin_team.name|default:"-" }}</p>
|
||||||
<p><strong>Birth date:</strong> {{ player.birth_date|date:"Y-m-d"|default:"-" }}</p>
|
<p><strong>Birth date:</strong> {{ player.birth_date|date:"Y-m-d"|default:"-" }}</p>
|
||||||
<p><strong>Age:</strong> {{ age|default:"-" }}</p>
|
<p><strong>Age:</strong> {{ age|default:"-" }}</p>
|
||||||
<p><strong>Height:</strong> {{ player.height_cm|default:"-" }} cm</p>
|
<p><strong>Height:</strong> {{ player.height_cm|default:"-" }} cm</p>
|
||||||
|
|||||||
@ -42,6 +42,8 @@
|
|||||||
<div><label for="id_competition">Competition</label>{{ search_form.competition }}</div>
|
<div><label for="id_competition">Competition</label>{{ search_form.competition }}</div>
|
||||||
<div><label for="id_team">Team</label>{{ search_form.team }}</div>
|
<div><label for="id_team">Team</label>{{ search_form.team }}</div>
|
||||||
<div><label for="id_season">Season</label>{{ search_form.season }}</div>
|
<div><label for="id_season">Season</label>{{ search_form.season }}</div>
|
||||||
|
<div><label for="id_origin_competition">Origin competition</label>{{ search_form.origin_competition }}</div>
|
||||||
|
<div><label for="id_origin_team">Origin team</label>{{ search_form.origin_team }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
<th>Player</th>
|
<th>Player</th>
|
||||||
<th>Nationality</th>
|
<th>Nationality</th>
|
||||||
<th>Pos / Role</th>
|
<th>Pos / Role</th>
|
||||||
|
<th>Origin</th>
|
||||||
<th>Height / Weight</th>
|
<th>Height / Weight</th>
|
||||||
<th>Games</th>
|
<th>Games</th>
|
||||||
<th>MPG</th>
|
<th>MPG</th>
|
||||||
@ -39,6 +40,10 @@
|
|||||||
{{ player.nominal_position.code|default:"-" }}
|
{{ player.nominal_position.code|default:"-" }}
|
||||||
/ {{ player.inferred_role.name|default:"-" }}
|
/ {{ player.inferred_role.name|default:"-" }}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ player.origin_competition.name|default:"-" }}
|
||||||
|
{% if player.origin_team %}<br><span class="muted-text">{{ player.origin_team.name }}</span>{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ player.height_cm|default:"-" }} / {{ player.weight_kg|default:"-" }}</td>
|
<td>{{ player.height_cm|default:"-" }} / {{ player.weight_kg|default:"-" }}</td>
|
||||||
<td>{{ player.games_played_value|floatformat:0 }}</td>
|
<td>{{ player.games_played_value|floatformat:0 }}</td>
|
||||||
<td>{{ player.mpg_value|floatformat:1 }}</td>
|
<td>{{ player.mpg_value|floatformat:1 }}</td>
|
||||||
|
|||||||
@ -25,6 +25,7 @@ def test_run_full_sync_creates_domain_objects(settings):
|
|||||||
assert Player.objects.count() >= 1
|
assert Player.objects.count() >= 1
|
||||||
assert PlayerSeason.objects.count() >= 1
|
assert PlayerSeason.objects.count() >= 1
|
||||||
assert PlayerSeasonStats.objects.count() >= 1
|
assert PlayerSeasonStats.objects.count() >= 1
|
||||||
|
assert Player.objects.filter(origin_competition__isnull=False).exists()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
190
tests/test_player_origin.py
Normal file
190
tests/test_player_origin.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.competitions.models import Competition, Season
|
||||||
|
from apps.players.models import Nationality, Player, PlayerCareerEntry, Position, Role
|
||||||
|
from apps.players.services.origin import refresh_player_origin, refresh_player_origins
|
||||||
|
from apps.teams.models import Team
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_origin_derivation_uses_earliest_meaningful_career_entry():
|
||||||
|
nationality = Nationality.objects.create(name="Italy", iso2_code="IT", iso3_code="ITA")
|
||||||
|
position = Position.objects.create(code="PG", name="Point Guard")
|
||||||
|
role = Role.objects.create(code="playmaker", name="Playmaker")
|
||||||
|
|
||||||
|
player = Player.objects.create(
|
||||||
|
first_name="Marco",
|
||||||
|
last_name="Rossi",
|
||||||
|
full_name="Marco Rossi",
|
||||||
|
birth_date=date(2000, 1, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
)
|
||||||
|
|
||||||
|
comp_early = Competition.objects.create(
|
||||||
|
name="Lega 2",
|
||||||
|
slug="lega-2",
|
||||||
|
competition_type=Competition.CompetitionType.LEAGUE,
|
||||||
|
gender=Competition.Gender.MEN,
|
||||||
|
)
|
||||||
|
comp_late = Competition.objects.create(
|
||||||
|
name="Lega 1",
|
||||||
|
slug="lega-1",
|
||||||
|
competition_type=Competition.CompetitionType.LEAGUE,
|
||||||
|
gender=Competition.Gender.MEN,
|
||||||
|
)
|
||||||
|
team_early = Team.objects.create(name="Bologna B", slug="bologna-b", country=nationality)
|
||||||
|
team_late = Team.objects.create(name="Bologna A", slug="bologna-a", country=nationality)
|
||||||
|
|
||||||
|
season_early = Season.objects.create(label="2017-2018", start_date=date(2017, 9, 1), end_date=date(2018, 6, 30))
|
||||||
|
season_late = Season.objects.create(label="2019-2020", start_date=date(2019, 9, 1), end_date=date(2020, 6, 30))
|
||||||
|
|
||||||
|
PlayerCareerEntry.objects.create(
|
||||||
|
player=player,
|
||||||
|
team=team_late,
|
||||||
|
competition=comp_late,
|
||||||
|
season=season_late,
|
||||||
|
start_date=date(2019, 9, 15),
|
||||||
|
)
|
||||||
|
PlayerCareerEntry.objects.create(
|
||||||
|
player=player,
|
||||||
|
team=team_early,
|
||||||
|
competition=comp_early,
|
||||||
|
season=season_early,
|
||||||
|
start_date=date(2017, 9, 15),
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = refresh_player_origin(player)
|
||||||
|
|
||||||
|
assert changed is True
|
||||||
|
player.refresh_from_db()
|
||||||
|
assert player.origin_competition == comp_early
|
||||||
|
assert player.origin_team == team_early
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_origin_unknown_when_no_meaningful_career_entries():
|
||||||
|
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="Perez",
|
||||||
|
full_name="Juan Perez",
|
||||||
|
birth_date=date(2001, 5, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = refresh_player_origin(player)
|
||||||
|
|
||||||
|
assert changed is False
|
||||||
|
player.refresh_from_db()
|
||||||
|
assert player.origin_competition is None
|
||||||
|
assert player.origin_team is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_player_search_filters_by_origin_competition(client):
|
||||||
|
nationality = Nationality.objects.create(name="France", iso2_code="FR", iso3_code="FRA")
|
||||||
|
position = Position.objects.create(code="SG", name="Shooting Guard")
|
||||||
|
role = Role.objects.create(code="scorer", name="Scorer")
|
||||||
|
|
||||||
|
origin_a = Competition.objects.create(
|
||||||
|
name="LNB Pro A",
|
||||||
|
slug="lnb-pro-a-origin",
|
||||||
|
competition_type=Competition.CompetitionType.LEAGUE,
|
||||||
|
gender=Competition.Gender.MEN,
|
||||||
|
)
|
||||||
|
origin_b = Competition.objects.create(
|
||||||
|
name="LNB Pro B",
|
||||||
|
slug="lnb-pro-b-origin",
|
||||||
|
competition_type=Competition.CompetitionType.LEAGUE,
|
||||||
|
gender=Competition.Gender.MEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
p1 = Player.objects.create(
|
||||||
|
first_name="A",
|
||||||
|
last_name="One",
|
||||||
|
full_name="A One",
|
||||||
|
birth_date=date(2000, 1, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
origin_competition=origin_a,
|
||||||
|
)
|
||||||
|
Player.objects.create(
|
||||||
|
first_name="B",
|
||||||
|
last_name="Two",
|
||||||
|
full_name="B Two",
|
||||||
|
birth_date=date(2000, 1, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
origin_competition=origin_b,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(reverse("players:index"), data={"origin_competition": origin_a.id})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
players = list(response.context["players"])
|
||||||
|
assert len(players) == 1
|
||||||
|
assert players[0].id == p1.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_backfill_refresh_player_origins_updates_existing_players():
|
||||||
|
nationality = Nationality.objects.create(name="Germany", iso2_code="DE", iso3_code="DEU")
|
||||||
|
position = Position.objects.create(code="PF", name="Power Forward")
|
||||||
|
role = Role.objects.create(code="big", name="Big")
|
||||||
|
competition = Competition.objects.create(
|
||||||
|
name="BBL",
|
||||||
|
slug="bbl-origin",
|
||||||
|
competition_type=Competition.CompetitionType.LEAGUE,
|
||||||
|
gender=Competition.Gender.MEN,
|
||||||
|
)
|
||||||
|
team = Team.objects.create(name="Berlin", slug="berlin-origin", country=nationality)
|
||||||
|
season = Season.objects.create(label="2018-2019", start_date=date(2018, 9, 1), end_date=date(2019, 6, 30))
|
||||||
|
|
||||||
|
p1 = Player.objects.create(
|
||||||
|
first_name="F1",
|
||||||
|
last_name="L1",
|
||||||
|
full_name="Player One",
|
||||||
|
birth_date=date(1999, 1, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
)
|
||||||
|
p2 = Player.objects.create(
|
||||||
|
first_name="F2",
|
||||||
|
last_name="L2",
|
||||||
|
full_name="Player Two",
|
||||||
|
birth_date=date(1998, 1, 1),
|
||||||
|
nationality=nationality,
|
||||||
|
nominal_position=position,
|
||||||
|
inferred_role=role,
|
||||||
|
)
|
||||||
|
|
||||||
|
PlayerCareerEntry.objects.create(
|
||||||
|
player=p1,
|
||||||
|
team=team,
|
||||||
|
competition=competition,
|
||||||
|
season=season,
|
||||||
|
start_date=date(2018, 9, 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = refresh_player_origins(Player.objects.filter(id__in=[p1.id, p2.id]))
|
||||||
|
|
||||||
|
assert updated == 1
|
||||||
|
p1.refresh_from_db()
|
||||||
|
p2.refresh_from_db()
|
||||||
|
assert p1.origin_competition == competition
|
||||||
|
assert p1.origin_team == team
|
||||||
|
assert p2.origin_competition is None
|
||||||
|
assert p2.origin_team is None
|
||||||
Reference in New Issue
Block a user