diff --git a/baseball_organisator/settings.py b/baseball_organisator/settings.py
index 7822c66..f8f8767 100644
--- a/baseball_organisator/settings.py
+++ b/baseball_organisator/settings.py
@@ -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 = [
diff --git a/baseball_organisator/urls.py b/baseball_organisator/urls.py
index 709fa64..5fdae4f 100644
--- a/baseball_organisator/urls.py
+++ b/baseball_organisator/urls.py
@@ -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')),
]
diff --git a/polls/__init__.py b/polls/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/polls/admin.py b/polls/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/polls/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/polls/apps.py b/polls/apps.py
new file mode 100644
index 0000000..5a5f94c
--- /dev/null
+++ b/polls/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PollsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'polls'
diff --git a/polls/forms.py b/polls/forms.py
new file mode 100644
index 0000000..1f9b147
--- /dev/null
+++ b/polls/forms.py
@@ -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)
diff --git a/polls/migrations/0001_initial.py b/polls/migrations/0001_initial.py
new file mode 100644
index 0000000..7d01942
--- /dev/null
+++ b/polls/migrations/0001_initial.py
@@ -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')),
+ ],
+ ),
+ ]
diff --git a/polls/migrations/__init__.py b/polls/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/polls/models.py b/polls/models.py
new file mode 100644
index 0000000..8cca582
--- /dev/null
+++ b/polls/models.py
@@ -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
\ No newline at end of file
diff --git a/polls/templates/polls/poll_detail.html b/polls/templates/polls/poll_detail.html
new file mode 100644
index 0000000..d4ca3b1
--- /dev/null
+++ b/polls/templates/polls/poll_detail.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
{{ poll.question }}
+
Asked by {{ poll.creator.get_full_name }} for team {{ poll.team.name }}
+
+
+
+{% endblock %}
diff --git a/polls/templates/polls/poll_form.html b/polls/templates/polls/poll_form.html
new file mode 100644
index 0000000..344551e
--- /dev/null
+++ b/polls/templates/polls/poll_form.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/polls/templates/polls/poll_list.html b/polls/templates/polls/poll_list.html
new file mode 100644
index 0000000..3f6964a
--- /dev/null
+++ b/polls/templates/polls/poll_list.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
Polls
+ {% if user.coached_teams.exists %}
+
Create Poll
+ {% endif %}
+
+
+ {% if polls %}
+
+ {% else %}
+
+ No polls available for your teams yet.
+
+ {% endif %}
+
+{% endblock %}
diff --git a/polls/templates/polls/poll_results.html b/polls/templates/polls/poll_results.html
new file mode 100644
index 0000000..29835e2
--- /dev/null
+++ b/polls/templates/polls/poll_results.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
{{ poll.question }}
+
Results for the poll asked by {{ poll.creator.get_full_name }}
+
+
+ {% for choice in poll.choices.all %}
+ -
+ {{ choice.choice_text }}
+ {{ choice.votes.count }} vote(s)
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/polls/tests.py b/polls/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/polls/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/polls/urls.py b/polls/urls.py
new file mode 100644
index 0000000..3a34808
--- /dev/null
+++ b/polls/urls.py
@@ -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('/', views.PollDetailView.as_view(), name='poll_detail'),
+ path('/results/', views.PollResultsView.as_view(), name='poll_results'),
+ path('/vote/', views.vote, name='vote'),
+]
diff --git a/polls/views.py b/polls/views.py
new file mode 100644
index 0000000..885bbbc
--- /dev/null
+++ b/polls/views.py
@@ -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'
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index fe8738a..a5ecfd7 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -41,6 +41,9 @@
Past Games
+
+ Polls
+
{% if user.coached_teams.all %}
Create New Player