feat: Implementierung von Spielergebnissen, Heimspiel-Status und Zeitzonen-Fix

This commit is contained in:
Matthias Nagel 2025-10-02 16:25:20 +02:00
parent 450d3597d2
commit ec07bfc53b
23 changed files with 384 additions and 8 deletions

View File

@ -0,0 +1,14 @@
import pytz
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = 'Europe/Berlin'
if tzname:
timezone.activate(pytz.timezone(tzname))
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'baseball_organisator.middleware.TimezoneMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',

View File

@ -1,5 +1,5 @@
from django import forms
from .models import Event, Training, Game
from .models import Event, Training, Game, GameResult
from clubs.models import Team
class EventForm(forms.ModelForm):
@ -32,3 +32,23 @@ class OpenGameForm(forms.Form):
club = kwargs.pop('club')
super().__init__(*args, **kwargs)
self.fields['teams'].queryset = Team.objects.filter(club=club)
class GameResultForm(forms.ModelForm):
class Meta:
model = GameResult
fields = []
def __init__(self, *args, **kwargs):
game = kwargs.pop('game')
super().__init__(*args, **kwargs)
if game.is_home_game:
home_team = game.team.name
guest_team = game.opponent
else:
home_team = game.opponent
guest_team = game.team.name
for i in range(1, game.number_of_innings + 1):
self.fields[f'inning_{i}_home'] = forms.IntegerField(label=f'Inning {i} ({home_team})', required=False)
self.fields[f'inning_{i}_guest'] = forms.IntegerField(label=f'Inning {i} ({guest_team})', required=False)

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.6 on 2025-10-02 12:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendars', '0004_game_is_home_game'),
]
operations = [
migrations.AddField(
model_name='game',
name='number_of_innings',
field=models.PositiveIntegerField(default=9),
),
migrations.AlterField(
model_name='gameresult',
name='inning_results',
field=models.JSONField(default=dict),
),
]

View File

@ -28,12 +28,12 @@ class Game(Event):
season = models.CharField(max_length=255, blank=True)
min_players = models.PositiveIntegerField(default=9)
is_home_game = models.BooleanField(default=True)
number_of_innings = models.PositiveIntegerField(default=9)
opened_for_teams = models.ManyToManyField('clubs.Team', related_name='opened_games', blank=True)
class GameResult(models.Model):
game = models.OneToOneField(Game, on_delete=models.CASCADE, related_name='result')
# A simple way to store inning results as a string. A more complex solution could use a JSONField or separate Inning model.
inning_results = models.CharField(max_length=255, help_text="Comma-separated scores per inning, e.g., '1-0,0-2,3-1'")
inning_results = models.JSONField(default=dict)
def __str__(self):
return f"Result for {self.game}"

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h2>Record Results for {{ game.title }}</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<table class="table">
<thead>
<tr>
<th>Team</th>
{% for item in form_fields_by_inning %}
<th>{{ item.inning }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td>
{% if game.is_home_game %}
{{ game.team.name }} (Home)
{% else %}
{{ game.opponent }} (Home)
{% endif %}
</td>
{% for item in form_fields_by_inning %}
<td>{{ item.home }}</td>
{% endfor %}
</tr>
<tr>
<td>
{% if not game.is_home_game %}
{{ game.team.name }} (Guest)
{% else %}
{{ game.opponent }} (Guest)
{% endif %}
</td>
{% for item in form_fields_by_inning %}
<td>{{ item.guest }}</td>
{% endfor %}
</tr>
</tbody>
</table>
<button type="submit" class="btn btn-primary">Save Results</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -10,4 +10,5 @@ urlpatterns = [
path('event/<int:pk>/delete/', views.EventDeleteView.as_view(), name='event-delete'),
path('participation/<int:child_id>/<int:event_id>/<str:status>/', views.manage_participation, name='manage-participation'),
path('game/<int:game_id>/open/', views.open_game, name='open-game'),
path('game/<int:game_id>/results/', views.record_results, name='record-results'),
]

View File

@ -3,8 +3,8 @@ from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required
from django.urls import reverse_lazy
from .models import Event, Training, Game, EventParticipation
from .forms import EventForm, TrainingForm, GameForm, OpenGameForm
from .models import Event, Training, Game, EventParticipation, GameResult
from .forms import EventForm, TrainingForm, GameForm, OpenGameForm, GameResultForm
from accounts.models import CustomUser # Import CustomUser for manage_participation view
from django.utils import timezone
import datetime
@ -142,4 +142,39 @@ def open_game(request, game_id):
else:
form = OpenGameForm(club=club)
return render(request, 'calendars/open_game.html', {'form': form, 'game': game})
return render(request, 'calendars/open_game.html', {'form': form, 'game': game})
@login_required
def record_results(request, game_id):
game = get_object_or_404(Game, id=game_id)
game_result, created = GameResult.objects.get_or_create(game=game)
if request.method == 'POST':
form = GameResultForm(request.POST, game=game, instance=game_result)
if form.is_valid():
inning_results = {}
for i in range(1, game.number_of_innings + 1):
inning_results[f'inning_{i}'] = {
'home': form.cleaned_data.get(f'inning_{i}_home'),
'guest': form.cleaned_data.get(f'inning_{i}_guest'),
}
game_result.inning_results = inning_results
game_result.save()
return redirect('dashboard')
else:
initial_data = {}
if game_result.inning_results:
for inning, scores in game_result.inning_results.items():
initial_data[f'{inning}_home'] = scores.get('home')
initial_data[f'{inning}_guest'] = scores.get('guest')
form = GameResultForm(game=game, instance=game_result, initial=initial_data)
form_fields_by_inning = []
for i in range(1, game.number_of_innings + 1):
form_fields_by_inning.append({
'inning': i,
'home': form[f'inning_{i}_home'],
'guest': form[f'inning_{i}_guest'],
})
return render(request, 'calendars/record_results.html', {'form': form, 'game': game, 'form_fields_by_inning': form_fields_by_inning})

View File

@ -53,7 +53,10 @@
{% if user == item.event.team.head_coach or user in item.event.team.assistant_coaches.all %}
<a href="{% url 'event-update' item.event.pk %}" class="btn btn-warning btn-sm">Edit</a>
<a href="{% url 'event-delete' item.event.pk %}" class="btn btn-danger btn-sm">Delete</a>
{% if item.event.game %}
<a href="{% url 'record-results' item.event.game.id %}" class="btn btn-success btn-sm record-results-btn" style="display: none;" data-start-time="{{ item.local_start_time_iso }}">Record Results</a>
{% endif %}
{% if item.event.game and item.days_until_event >= 0 and item.days_until_event < 7 and item.accepted_count < item.required_players and item.event.team.club.teams.count > 1 %}
<a href="{% url 'open-game' item.event.game.id %}" class="btn btn-info btn-sm">Open Game</a>
{% endif %}
@ -137,4 +140,30 @@
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}
{% block javascript %}
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM fully loaded and parsed');
const recordButtons = document.querySelectorAll('.record-results-btn');
console.log('Found buttons:', recordButtons.length);
const now = new Date();
console.log('Current client time:', now);
recordButtons.forEach(button => {
const startTimeString = button.dataset.startTime;
console.log('Button start time string:', startTimeString);
const startTime = new Date(startTimeString);
console.log('Button start time object:', startTime);
if (now > startTime) {
console.log('Showing button for event starting at', startTime);
button.style.display = 'inline-block';
} else {
console.log('Hiding button for event starting at', startTime);
}
});
});
</script>
{% endblock %}

View File

@ -46,13 +46,15 @@ def dashboard(request):
player_participations.append({'player': player, 'status': status})
days_until_event = (event.start_time - timezone.now()).days
local_start_time = timezone.localtime(event.start_time)
events_with_participation.append({
'event': event,
'accepted_count': accepted_count,
'required_players': required_players,
'player_participations': player_participations,
'days_until_event': days_until_event
'days_until_event': days_until_event,
'local_start_time_iso': local_start_time.isoformat()
})
# Get children's events

Binary file not shown.

View File

@ -0,0 +1,36 @@
Hiding button for event starting at
DOM fully loaded and parsed 127.0.0.1:8000:720:13
GET
http://127.0.0.1:8000/favicon.ico
[HTTP/1.1 404 Not Found 0ms]
Found buttons: 3 127.0.0.1:8000:722:13
Current client time:
Date Thu Oct 02 2025 15:02:08 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:724:13
Button start time string: 2025-10-02T13:10:00+00:00 127.0.0.1:8000:728:17
Button start time object:
Date Thu Oct 02 2025 15:10:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:730:17
Hiding button for event starting at
Date Thu Oct 02 2025 15:10:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:736:21
Button start time string: 2025-10-05T19:00:00+00:00 127.0.0.1:8000:728:17
Button start time object:
Date Sun Oct 05 2025 21:00:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:730:17
Hiding button for event starting at
Date Sun Oct 05 2025 21:00:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:736:21
Button start time string: 2025-10-06T17:47:00+00:00 127.0.0.1:8000:728:17
Button start time object:
Date Mon Oct 06 2025 19:47:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:730:17
Hiding button for event starting at
Date Mon Oct 06 2025 19:47:00 GMT+0200 (Mitteleuropäische Sommerzeit)
127.0.0.1:8000:736:21
Source-Map-Fehler: request failed with status 404
Ressourcen-Adresse: http://127.0.0.1:8000/static/js/bootstrap.bundle.min.js
Source-Map-Adresse: bootstrap.bundle.min.js.map

View File

@ -0,0 +1,82 @@
[02/Oct/2025 13:07:49] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:07:52] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:07:53] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:07:53] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:07:53] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:08:01] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:08:01] "GET /static/js/bootstrap.bundle.min.js.map HTTP/1.1" 404 1904
[02/Oct/2025 13:08:13] "GET /static/js/bootstrap.bundle.min.js.map HTTP/1.1" 404 1904
[02/Oct/2025 13:08:15] "GET / HTTP/1.1" 200 27859
[02/Oct/2025 13:08:15] "GET /static/js/bootstrap.bundle.min.js.map HTTP/1.1" 404 1904
/home/mnagel/Projekte/baseball_organisator/baseball_organisator/settings.py changed, reloading.
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Exception in thread django-main-thread:
Traceback (most recent call last):
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/servers/basehttp.py", line 48, in get_internal_wsgi_application
return import_string(app_path)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/module_loading.py", line 30, in import_string
return cached_import(module_path, class_name)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/module_loading.py", line 15, in cached_import
module = import_module(module_path)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/baseball_organisator/wsgi.py", line 16, in <module>
application = get_wsgi_application()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/wsgi.py", line 13, in get_wsgi_application
return WSGIHandler()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/handlers/wsgi.py", line 118, in __init__
self.load_middleware()
~~~~~~~~~~~~~~~~~~~~^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/handlers/base.py", line 40, in load_middleware
middleware = import_string(middleware_path)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/module_loading.py", line 30, in import_string
return cached_import(module_path, class_name)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/module_loading.py", line 15, in cached_import
module = import_module(module_path)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/baseball_organisator/middleware.py", line 1, in <module>
import pytz
ModuleNotFoundError: No module named 'pytz'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/lib64/python3.13/threading.py", line 1043, in _bootstrap_inner
self.run()
~~~~~~~~^^
File "/usr/lib64/python3.13/threading.py", line 994, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
fn(*args, **kwargs)
~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/commands/runserver.py", line 143, in inner_run
handler = self.get_handler(*args, **options)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/contrib/staticfiles/management/commands/runserver.py", line 31, in get_handler
handler = super().get_handler(*args, **options)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/commands/runserver.py", line 73, in get_handler
return get_internal_wsgi_application()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/servers/basehttp.py", line 50, in get_internal_wsgi_application
raise ImproperlyConfigured(
...<2 lines>...
) from err
django.core.exceptions.ImproperlyConfigured: WSGI application 'baseball_organisator.wsgi.application' could not be loaded; Error importing module.

77
docs/traceback/trace9.log Normal file
View File

@ -0,0 +1,77 @@
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
/home/mnagel/Projekte/baseball_organisator/calendars/forms.py changed, reloading.
Watching for file changes with StatReloader
Performing system checks...
Exception in thread django-main-thread:
Traceback (most recent call last):
File "/usr/lib64/python3.13/threading.py", line 1043, in _bootstrap_inner
self.run()
~~~~~~~~^^
File "/usr/lib64/python3.13/threading.py", line 994, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
fn(*args, **kwargs)
~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/commands/runserver.py", line 134, in inner_run
self.check(**check_kwargs)
~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/base.py", line 492, in check
all_issues = checks.run_checks(
app_configs=app_configs,
...<2 lines>...
databases=databases,
)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/registry.py", line 89, in run_checks
new_errors = check(app_configs=app_configs, databases=databases)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/urls.py", line 16, in check_url_config
return check_resolver(resolver)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/urls.py", line 26, in check_resolver
return check_method()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 531, in check
for pattern in self.url_patterns:
^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/functional.py", line 47, in __get__
res = instance.__dict__[self.name] = self.func(instance)
~~~~~~~~~^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 718, in url_patterns
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/functional.py", line 47, in __get__
res = instance.__dict__[self.name] = self.func(instance)
~~~~~~~~~^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 711, in urlconf_module
return import_module(self.urlconf_name)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/baseball_organisator/urls.py", line 25, in <module>
path('calendars/', include('calendars.urls')),
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/conf.py", line 39, in include
urlconf_module = import_module(urlconf_module)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/calendars/urls.py", line 2, in <module>
from . import views
File "/home/mnagel/Projekte/baseball_organisator/calendars/views.py", line 7, in <module>
from .forms import EventForm, TrainingForm, GameForm, OpenGameForm
File "/home/mnagel/Projekte/baseball_organisator/calendars/forms.py", line 1, in <module>
class GameResultForm(forms.ModelForm):
^^^^^
NameError: name 'forms' is not defined. Did you mean: 'format'?

View File

@ -61,5 +61,6 @@
</div>
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
{% block javascript %}{% endblock %}
</body>
</html>