Datenmodell funktioniert und Verwaltung über Admin-Ansicht, service angelegt.

This commit is contained in:
2026-07-28 01:44:02 +02:00
parent 6a6de6882b
commit c62c593b1c
9 changed files with 262 additions and 40 deletions
+62
View File
@@ -0,0 +1,62 @@
# Generated by Django 6.0.7 on 2026-07-27 22:56
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('role', models.CharField(choices=[('admin', 'Administrator'), ('player', 'Spieler')], default='player', max_length=10, verbose_name='Rolle')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Player',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100, verbose_name='Vorname')),
('last_name', models.CharField(max_length=100, verbose_name='Nachname')),
('club', models.CharField(blank=True, default='', max_length=150, verbose_name='Verein/Klasse')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='player_profile', to=settings.AUTH_USER_MODEL, verbose_name='Benutzeraccount')),
],
options={
'verbose_name': 'Spieler',
'verbose_name_plural': 'Spieler',
'ordering': ['last_name', 'first_name'],
},
),
]
View File
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Löscht alle compilierten Python-Zwischendateien im Projekt
find . -type d -name __pycache__ -exec rm -r {} +
find . -name "*.pyc" -delete
+16
View File
@@ -68,5 +68,21 @@ USE_TZ = True
STATIC_URL = 'static/' STATIC_URL = 'static/'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+2 -5
View File
@@ -4,9 +4,8 @@ from .models import Tournament, Registration, Round, Match
class RegistrationInline(admin.TabularInline): class RegistrationInline(admin.TabularInline):
model = Registration model = Registration
fk_name = 'tournament' # 👈 Wichtig: Explizite Rückverknüpfung zum Elternmodell
extra = 0 extra = 0
# autocomplete_fields entfernt, um Startkonflikte zu vermeiden.
# Normale Select-Boxen sind für <= 50 Spieler besser!
@admin.register(Tournament) @admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin): class TournamentAdmin(admin.ModelAdmin):
@@ -21,13 +20,12 @@ class TournamentAdmin(admin.ModelAdmin):
class MatchInline(admin.TabularInline): class MatchInline(admin.TabularInline):
model = Match model = Match
fk_name = 'round_detail' # 👈 Wichtig: Explizite Rückverknüpfung zur Runde
extra = 0 extra = 0
# autocomplete_fields hier ebenfalls entfernt
@admin.register(Round) @admin.register(Round)
class RoundAdmin(admin.ModelAdmin): class RoundAdmin(admin.ModelAdmin):
list_display = ('tournament', 'round_number') list_display = ('tournament', 'round_number')
list_filter = ('tournament',)
inlines = [MatchInline] inlines = [MatchInline]
@@ -35,4 +33,3 @@ class RoundAdmin(admin.ModelAdmin):
class MatchAdmin(admin.ModelAdmin): class MatchAdmin(admin.ModelAdmin):
list_display = ('round_detail', 'player_white', 'player_black', 'result') list_display = ('round_detail', 'player_white', 'player_black', 'result')
list_filter = ('result', 'round_detail__tournament') list_filter = ('result', 'round_detail__tournament')
+65
View File
@@ -0,0 +1,65 @@
# Generated by Django 6.0.7 on 2026-07-27 22:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Tournament',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Turniername')),
('date', models.DateField(verbose_name='Datum')),
('description', models.TextField(blank=True, default='', verbose_name='Beschreibung')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='Round',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('round_number', models.PositiveIntegerField(verbose_name='Rundennummer')),
('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rounds', to='tournaments.tournament')),
],
options={
'ordering': ['round_number'],
'unique_together': {('tournament', 'round_number')},
},
),
migrations.CreateModel(
name='Match',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('result', models.CharField(choices=[('pending', 'Ausstehend'), ('white_win', 'Weiß gewinnt'), ('black_win', 'Schwarz gewinnt'), ('draw', 'Unentschieden')], default='pending', max_length=10)),
('player_black', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='matches_as_black', to='accounts.player', verbose_name='Schwarzer Spieler')),
('player_white', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='matches_as_white', to='accounts.player', verbose_name='Weißer Spieler')),
('round_detail', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='tournaments.round')),
],
options={
'unique_together': {('round_detail', 'player_white', 'player_black')},
},
),
migrations.CreateModel(
name='Registration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.player')),
('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='tournaments.tournament')),
],
options={
'unique_together': {('tournament', 'player')},
},
),
]
View File
+20 -35
View File
@@ -3,10 +3,7 @@ from django.utils.translation import gettext_lazy as _
class Tournament(models.Model): class Tournament(models.Model):
""" """Stellt ein komplettes Turnier dar."""
Stellt ein komplettes Turnier dar.
Hier werden alle Runden und Spieler registriert.
"""
name = models.CharField(_('Turniername'), max_length=200) name = models.CharField(_('Turniername'), max_length=200)
date = models.DateField(_('Datum')) date = models.DateField(_('Datum'))
description = models.TextField(_('Beschreibung'), blank=True, default='') description = models.TextField(_('Beschreibung'), blank=True, default='')
@@ -20,34 +17,27 @@ class Tournament(models.Model):
class Registration(models.Model): class Registration(models.Model):
""" """Verknüpfungstabelle zwischen Turnier und Spielern."""
Verknüpfungstabelle zwischen einem Tournament und den daran teilnehmenden Playern.
Ermöglicht es, dass ein Spieler an mehreren Turnieren teilnehmen kann.
"""
tournament = models.ForeignKey( tournament = models.ForeignKey(
Tournament, Tournament,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='registrations' related_name='registrations'
) )
# FIX: App-Label hinzugefügt ('accounts.Player')
player = models.ForeignKey( player = models.ForeignKey(
'Player', # String-Referenz vermeiden Import-Konflikte (Circular Dependency) 'accounts.Player',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
class Meta: class Meta:
unique_together = ('tournament', 'player') unique_together = ('tournament', 'player')
verbose_name = 'Anmeldung'
verbose_name_plural = 'Anmeldungen'
class Round(models.Model): class Round(models.Model):
""" """Eine einzelne Runde im Turnier."""
Eine einzelne Runde im Turnier.
Jede Runde gehört eindeutig zu einem Tournament.
"""
tournament = models.ForeignKey( tournament = models.ForeignKey(
Tournament, Tournament,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='rounds' related_name='rounds'
) )
round_number = models.PositiveIntegerField(_('Rundennummer')) round_number = models.PositiveIntegerField(_('Rundennummer'))
@@ -61,10 +51,7 @@ class Round(models.Model):
class Match(models.Model): class Match(models.Model):
""" """Einzelnes Spiel (Paarung) innerhalb einer Runde."""
Einzelnes Spiel (Paarung) innerhalb einer Runde.
Hier wird festgehalten, wer als Weiß und Schwarz spielt und das Ergebnis.
"""
class ResultStatus(models.TextChoices): class ResultStatus(models.TextChoices):
PENDING = 'pending', _('Ausstehend') PENDING = 'pending', _('Ausstehend')
@@ -73,35 +60,33 @@ class Match(models.Model):
DRAW = 'draw', _('Unentschieden') DRAW = 'draw', _('Unentschieden')
round_detail = models.ForeignKey( round_detail = models.ForeignKey(
Round, Round,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='matches' related_name='matches'
) )
# FIX: App-Label explizit gesetzt ('accounts.Player')
player_white = models.ForeignKey( player_white = models.ForeignKey(
'Player', 'accounts.Player',
on_delete=models.PROTECT, # Spiel darf nicht gelöscht werden wenn Spieler existiert (Datensicherung) on_delete=models.PROTECT,
related_name='matches_as_white', related_name='matches_as_white',
verbose_name=_('Weißer Spieler') verbose_name=_('Weißer Spieler')
) )
player_black = models.ForeignKey( player_black = models.ForeignKey(
'Player', 'accounts.Player',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='matches_as_black', related_name='matches_as_black',
verbose_name=_('Schwarzer Spieler') verbose_name=_('Schwarzer Spieler')
) )
result = models.CharField( result = models.CharField(
max_length=10, max_length=10,
choices=ResultStatus.choices, choices=ResultStatus.choices,
default=ResultStatus.PENDING default=ResultStatus.PENDING
) )
class Meta: class Meta:
# Verhindert technisch doppelte Einträge (gleiche Paarung in gleicher Runde)
unique_together = ('round_detail', 'player_white', 'player_black') unique_together = ('round_detail', 'player_white', 'player_black')
def __str__(self): def __str__(self):
return f"{self.player_white} vs {self.player_black} ({self.round_detail})" return f"{self.player_white} vs {self.player_black} ({self.round_detail})"
+93
View File
@@ -0,0 +1,93 @@
from .models import Tournament, Match
class SwissSystemService:
"""
Zentrales Service für Turnierberechnungen im Schweizer System.
Verantwortlich für Ranglisten, Wertung und Paarungen.
"""
@staticmethod
def get_standings(tournament_id):
"""
Berechnet die aktuelle Rangliste eines Turniers.
Kriterien (in Reihenfolge):
1. Anzahl gewonnener Punkte (Win=1, Draw=0.5)
2. Buchholz-Wertung (Summe der Punktzahlen der Gegner)
"""
try:
tournament = Tournament.objects.get(id=tournament_id)
except Tournament.DoesNotExist:
return []
# Alle Spieler in diesem Turnier abrufen
players_dict = {}
# 1. Basis-Daten für alle Spieler initialisieren
for reg in tournament.registrations.all():
players_dict[reg.player.id] = {
'id': reg.player.id,
'name': f"{reg.player.first_name} {reg.player.last_name}",
'club': reg.player.club,
'points': 0.0,
'matches_played': 0,
'opponents_points': [] # Um Buchholz zu berechnen
}
# 2. Alle abgeschlossenen Spiele durchgehen und Punkte zuteilen
completed_matches = Match.objects.filter(
round_detail__tournament=tournament,
result__in=[Match.ResultStatus.WHITE_WIN, Match.ResultStatus.BLACK_WIN, Match.ResultStatus.DRAW]
)
for match in completed_matches:
if match.result == Match.ResultStatus.DRAW:
p_white = players_dict.get(match.player_white.id)
p_black = players_dict.get(match.player_black.id)
# Punkte addieren (0.5 für Unentschieden)
if p_white:
p_white['points'] += 0.5
p_white['matches_played'] += 1
if p_black:
p_black['points'] += 0.5
p_black['matches_played'] += 1
elif match.result == Match.ResultStatus.WHITE_WIN:
p_white = players_dict.get(match.player_white.id)
if p_white:
p_white['points'] += 1.0
p_white['matches_played'] += 1
elif match.result == Match.ResultStatus.BLACK_WIN:
p_black = players_dict.get(match.player_black.id)
if p_black:
p_black['points'] += 1.0
p_black['matches_played'] += 1
# Buchholz-Hilfe: Merken, gegen wen gespielt wurde (ID des Gegners)
if match.player_white.id in players_dict:
players_dict[match.player_white.id]['opponents_points'].append(match.player_black.id)
if match.player_black.id in players_dict:
players_dict[match.player_black.id]['opponents_points'].append(match.player_white.id)
# 3. Buchholz-Score berechnen
# (Summe der aktuellen Punkte aller Gegner)
for pid, data in players_dict.items():
buchholz = 0.0
for opp_id in data['opponents_points']:
if opp_id in players_dict:
buchholz += players_dict[opp_id]['points']
# Buchholz zum Datensatz hinzufügen (zum Anzeigen und Sortieren)
data['buchholz'] = round(buchholz, 2)
# 4. In eine Liste verwandeln und sortieren
standings_list = list(players_dict.values())
# Sortierung:
# Primär: Punkte (absteigend), Sekundär: Buchholz (absteigend)
standings_list.sort(key=lambda x: (x['points'], x['buchholz']), reverse=True)
return standings_list