Feat: Implementierung des Abstimmungs-Features (Polls)
Fügt ein neues Abstimmungs-Feature hinzu, das es Head Coaches ermöglicht,
Umfragen innerhalb ihres Teams zu erstellen und zu verwalten.
Teammitglieder können Umfragen ansehen und daran teilnehmen.
Wesentliche Änderungen:
- **Neue 'polls'-App:** Enthält Modelle, Formulare, Views und Templates.
- **Modelle und :** Definieren die Struktur für Umfragen
(Frage, Team, Ersteller, Mehrfachauswahl-Option) und die Auswahlmöglichkeiten
(Text, Stimmen).
- **Formulare und :** Für die Erstellung von Umfragen
und deren Auswahlmöglichkeiten.
- **Views:**
- : Zeigt alle für den Benutzer relevanten Umfragen an.
- : Ermöglicht Head Coaches das Erstellen neuer Umfragen
(inkl. Fehlerbehebung bei der Formularinitialisierung).
- : Zeigt Details einer Umfrage an und ermöglicht die
Stimmabgabe.
- : Zeigt die Ergebnisse einer Umfrage an.
- : Funktion für die Stimmabgabe.
- **Templates:** Spezifische Templates für alle Umfrage-Views.
- **URL-Konfiguration:** Neue URLs für die 'polls'-App und Einbindung in die
Haupt-URL-Konfiguration.
- **Navigationslink:** Ein neuer Link 'Polls' in der Hauptnavigation für
authentifizierte Benutzer.
- **Migrationen:** Datenbankmigrationen für die neuen - und -Modelle.
This commit is contained in:
parent
cb08474301
commit
001444e0dd
@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-)p-ei0pchzmkv!72^wr$!_s=9a_*4kuzsy(5_(urc*w(uummf3
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
||||
# Application definition
|
||||
@ -42,6 +42,7 @@ INSTALLED_APPS = [
|
||||
'calendars',
|
||||
'dashboard',
|
||||
'team_stats',
|
||||
'polls',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@ -24,5 +24,6 @@ urlpatterns = [
|
||||
path('clubs/', include('clubs.urls')),
|
||||
path('calendars/', include('calendars.urls')),
|
||||
path('statistics/', include('team_stats.urls')),
|
||||
path('polls/', include('polls.urls')),
|
||||
path('', include('dashboard.urls')),
|
||||
]
|
||||
|
||||
0
polls/__init__.py
Normal file
0
polls/__init__.py
Normal file
3
polls/admin.py
Normal file
3
polls/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
polls/apps.py
Normal file
6
polls/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PollsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'polls'
|
||||
23
polls/forms.py
Normal file
23
polls/forms.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
from .models import Poll, Choice
|
||||
|
||||
class PollForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Poll
|
||||
fields = ['question', 'team', 'multiple_choice']
|
||||
widgets = {
|
||||
'question': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'team': forms.Select(attrs={'class': 'form-control'}),
|
||||
'multiple_choice': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
class ChoiceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Choice
|
||||
fields = ['choice_text']
|
||||
widgets = {
|
||||
'choice_text': forms.TextInput(attrs={'class': 'form-control', 'required': True}),
|
||||
}
|
||||
|
||||
ChoiceFormSet = formset_factory(ChoiceForm, extra=2, max_num=5)
|
||||
38
polls/migrations/0001_initial.py
Normal file
38
polls/migrations/0001_initial.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-21 21:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('clubs', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Poll',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('question', models.CharField(max_length=255)),
|
||||
('multiple_choice', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls_created', to=settings.AUTH_USER_MODEL)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='clubs.team')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Choice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('choice_text', models.CharField(max_length=100)),
|
||||
('votes', models.ManyToManyField(blank=True, related_name='voted_choices', to=settings.AUTH_USER_MODEL)),
|
||||
('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='polls.poll')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
polls/migrations/__init__.py
Normal file
0
polls/migrations/__init__.py
Normal file
21
polls/models.py
Normal file
21
polls/models.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.db import models
|
||||
from clubs.models import Team
|
||||
from accounts.models import CustomUser
|
||||
|
||||
class Poll(models.Model):
|
||||
question = models.CharField(max_length=255)
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='polls')
|
||||
creator = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='polls_created')
|
||||
multiple_choice = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.question
|
||||
|
||||
class Choice(models.Model):
|
||||
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name='choices')
|
||||
choice_text = models.CharField(max_length=100)
|
||||
votes = models.ManyToManyField(CustomUser, related_name='voted_choices', blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.choice_text
|
||||
29
polls/templates/polls/poll_detail.html
Normal file
29
polls/templates/polls/poll_detail.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>{{ poll.question }}</h1>
|
||||
<p class="text-muted">Asked by {{ poll.creator.get_full_name }} for team {{ poll.team.name }}</p>
|
||||
|
||||
<form action="{% url 'polls:vote' poll.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset class="mb-3">
|
||||
<legend>Your Vote</legend>
|
||||
{% for choice in poll.choices.all %}
|
||||
<div class="form-check">
|
||||
{% if poll.multiple_choice %}
|
||||
<input class="form-check-input" type="checkbox" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
|
||||
{% else %}
|
||||
<input class="form-check-input" type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
|
||||
{% endif %}
|
||||
<label class="form-check-label" for="choice{{ forloop.counter }}">
|
||||
{{ choice.choice_text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<button type="submit" class="btn btn-success">Submit Vote</button>
|
||||
<a href="{% url 'polls:poll_results' poll.id %}" class="btn btn-secondary">View Results</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
polls/templates/polls/poll_form.html
Normal file
40
polls/templates/polls/poll_form.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Create a New Poll</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.question.label_tag }}
|
||||
{{ form.question }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.team.label_tag }}
|
||||
{{ form.team }}
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
{{ form.multiple_choice }}
|
||||
{{ form.multiple_choice.label_tag }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Choices</h4>
|
||||
{{ choice_formset.management_form }}
|
||||
<div id="choice-forms">
|
||||
{% for form in choice_formset %}
|
||||
<div class="mb-2">
|
||||
{{ form.choice_text.label_tag }}
|
||||
{{ form.choice_text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Poll</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
31
polls/templates/polls/poll_list.html
Normal file
31
polls/templates/polls/poll_list.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Polls</h1>
|
||||
{% if user.coached_teams.exists %}
|
||||
<a href="{% url 'polls:poll_create' %}" class="btn btn-primary">Create Poll</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if polls %}
|
||||
<div class="list-group">
|
||||
{% for poll in polls %}
|
||||
<a href="{% url 'polls:poll_detail' poll.pk %}" class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ poll.question }}</h5>
|
||||
<small>{{ poll.created_at|date:"d.m.Y" }}</small>
|
||||
</div>
|
||||
<p class="mb-1">For team: {{ poll.team.name }}</p>
|
||||
<small>Created by: {{ poll.creator.get_full_name }}</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
No polls available for your teams yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
22
polls/templates/polls/poll_results.html
Normal file
22
polls/templates/polls/poll_results.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>{{ poll.question }}</h1>
|
||||
<p class="text-muted">Results for the poll asked by {{ poll.creator.get_full_name }}</p>
|
||||
|
||||
<ul class="list-group">
|
||||
{% for choice in poll.choices.all %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
{{ choice.choice_text }}
|
||||
<span class="badge bg-primary rounded-pill">{{ choice.votes.count }} vote(s)</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="{% url 'polls:poll_detail' poll.id %}" class="btn btn-secondary">Back to Vote</a>
|
||||
<a href="{% url 'polls:poll_list' %}" class="btn btn-outline-primary">Back to All Polls</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
polls/tests.py
Normal file
3
polls/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
polls/urls.py
Normal file
12
polls/urls.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'polls'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.PollListView.as_view(), name='poll_list'),
|
||||
path('create/', views.PollCreateView.as_view(), name='poll_create'),
|
||||
path('<int:pk>/', views.PollDetailView.as_view(), name='poll_detail'),
|
||||
path('<int:pk>/results/', views.PollResultsView.as_view(), name='poll_results'),
|
||||
path('<int:poll_id>/vote/', views.vote, name='vote'),
|
||||
]
|
||||
127
polls/views.py
Normal file
127
polls/views.py
Normal file
@ -0,0 +1,127 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.generic import ListView, CreateView, DetailView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.db import transaction
|
||||
from .models import Poll, Choice
|
||||
from .forms import PollForm, ChoiceFormSet
|
||||
from clubs.models import Team
|
||||
from accounts.models import CustomUser
|
||||
|
||||
class PollListView(LoginRequiredMixin, ListView):
|
||||
model = Poll
|
||||
template_name = 'polls/poll_list.html'
|
||||
context_object_name = 'polls'
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
user_teams = set()
|
||||
|
||||
if user.team:
|
||||
user_teams.add(user.team)
|
||||
|
||||
# Add teams where the user is a coach or assistant
|
||||
user_teams.update(user.coached_teams.all())
|
||||
user_teams.update(user.assisted_teams.all())
|
||||
|
||||
# Add teams of children for parents
|
||||
if hasattr(user, 'children'):
|
||||
for child in user.children.all():
|
||||
if child.team:
|
||||
user_teams.add(child.team)
|
||||
|
||||
return Poll.objects.filter(team__in=user_teams).order_by('-created_at')
|
||||
|
||||
class PollCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
|
||||
model = Poll
|
||||
form_class = PollForm
|
||||
template_name = 'polls/poll_form.html'
|
||||
|
||||
def test_func(self):
|
||||
# Only head coaches can create polls
|
||||
return self.request.user.coached_teams.exists()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
data['choice_formset'] = ChoiceFormSet(self.request.POST)
|
||||
else:
|
||||
data['choice_formset'] = ChoiceFormSet()
|
||||
# Filter the team queryset for the form to only show teams the user coaches
|
||||
data['form'].fields['team'].queryset = self.request.user.coached_teams.all()
|
||||
return data
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
choice_formset = context['choice_formset']
|
||||
|
||||
# Set the creator before saving the poll
|
||||
form.instance.creator = self.request.user
|
||||
|
||||
if choice_formset.is_valid():
|
||||
self.object = form.save()
|
||||
|
||||
with transaction.atomic():
|
||||
for choice_form in choice_formset:
|
||||
if choice_form.cleaned_data.get('choice_text'):
|
||||
Choice.objects.create(
|
||||
poll=self.object,
|
||||
choice_text=choice_form.cleaned_data['choice_text']
|
||||
)
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('polls:poll_list')
|
||||
|
||||
class PollDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
|
||||
model = Poll
|
||||
template_name = 'polls/poll_detail.html'
|
||||
|
||||
def test_func(self):
|
||||
# Check if user is part of the team for which the poll is
|
||||
poll = self.get_object()
|
||||
user = self.request.user
|
||||
|
||||
user_teams = set()
|
||||
if user.team:
|
||||
user_teams.add(user.team)
|
||||
user_teams.update(user.coached_teams.all())
|
||||
user_teams.update(user.assisted_teams.all())
|
||||
if hasattr(user, 'children'):
|
||||
for child in user.children.all():
|
||||
if child.team:
|
||||
user_teams.add(child.team)
|
||||
|
||||
return poll.team in user_teams
|
||||
|
||||
def vote(request, poll_id):
|
||||
poll = get_object_or_404(Poll, pk=poll_id)
|
||||
user = request.user
|
||||
|
||||
if request.method == 'POST':
|
||||
if poll.multiple_choice:
|
||||
selected_choice_ids = request.POST.getlist('choice')
|
||||
# First, remove user's previous votes for this poll
|
||||
poll.choices.filter(votes=user).update(votes=None)
|
||||
# Then, add new votes
|
||||
for choice_id in selected_choice_ids:
|
||||
choice = get_object_or_404(Choice, pk=choice_id)
|
||||
choice.votes.add(user)
|
||||
else:
|
||||
selected_choice_id = request.POST.get('choice')
|
||||
if selected_choice_id:
|
||||
# Remove user's vote from all choices in this poll first
|
||||
for choice in poll.choices.all():
|
||||
choice.votes.remove(user)
|
||||
# Add the new vote
|
||||
choice = get_object_or_404(Choice, pk=selected_choice_id)
|
||||
choice.votes.add(user)
|
||||
|
||||
return redirect('polls:poll_results', pk=poll.id)
|
||||
|
||||
class PollResultsView(LoginRequiredMixin, DetailView):
|
||||
model = Poll
|
||||
template_name = 'polls/poll_results.html'
|
||||
context_object_name = 'poll'
|
||||
@ -41,6 +41,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'past_games' %}">Past Games</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'polls:poll_list' %}">Polls</a>
|
||||
</li>
|
||||
{% if user.coached_teams.all %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'player-add' %}">Create New Player</a>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user