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.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.services.origin import refresh_player_origin
|
||||
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
|
||||
from apps.providers.registry import get_provider
|
||||
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):
|
||||
touched_player_ids: set[int] = set()
|
||||
for payload in payloads:
|
||||
summary.processed += 1
|
||||
external_id = payload.get("external_id", "")
|
||||
@ -380,6 +382,7 @@ def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: Ing
|
||||
)
|
||||
continue
|
||||
|
||||
touched_player_ids.add(player.id)
|
||||
_, created = PlayerCareerEntry.objects.update_or_create(
|
||||
player=player,
|
||||
team=team,
|
||||
@ -399,6 +402,10 @@ def _sync_player_careers(provider_namespace: str, payloads: list[dict], run: Ing
|
||||
else:
|
||||
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(
|
||||
*,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
|
||||
from .models import Nationality, Player, PlayerAlias, PlayerCareerEntry, Position, Role
|
||||
from .services.origin import refresh_player_origins
|
||||
|
||||
|
||||
@admin.register(Nationality)
|
||||
@ -39,11 +41,26 @@ class PlayerAdmin(admin.ModelAdmin):
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
"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")
|
||||
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)
|
||||
|
||||
@ -25,8 +25,10 @@ class PlayerSearchForm(forms.Form):
|
||||
nominal_position = forms.ModelChoiceField(queryset=Position.objects.none(), required=False)
|
||||
inferred_role = forms.ModelChoiceField(queryset=Role.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)
|
||||
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)
|
||||
|
||||
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["inferred_role"].queryset = Role.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["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")
|
||||
|
||||
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,
|
||||
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)
|
||||
weight_kg = 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=["nominal_position"]),
|
||||
models.Index(fields=["inferred_role"]),
|
||||
models.Index(fields=["origin_competition"]),
|
||||
models.Index(fields=["origin_team"]),
|
||||
models.Index(fields=["is_active"]),
|
||||
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"])
|
||||
if data.get("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"):
|
||||
queryset = queryset.filter(player_seasons__team=data["team"])
|
||||
@ -185,4 +189,6 @@ def base_player_queryset():
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
).prefetch_related("aliases")
|
||||
|
||||
@ -87,6 +87,8 @@ class PlayerDetailView(DetailView):
|
||||
"nationality",
|
||||
"nominal_position",
|
||||
"inferred_role",
|
||||
"origin_competition",
|
||||
"origin_team",
|
||||
)
|
||||
.prefetch_related(
|
||||
"aliases",
|
||||
|
||||
Reference in New Issue
Block a user