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 }}

+ +
+ {% csrf_token %} +
+ Your Vote + {% for choice in poll.choices.all %} +
+ {% if poll.multiple_choice %} + + {% else %} + + {% endif %} + +
+ {% endfor %} +
+ + View Results +
+
+{% 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 %} +
+

Create a New Poll

+
+ {% csrf_token %} + +
+ {{ form.question.label_tag }} + {{ form.question }} +
+ +
+ {{ form.team.label_tag }} + {{ form.team }} +
+ +
+ {{ form.multiple_choice }} + {{ form.multiple_choice.label_tag }} +
+ +
+ +

Choices

+ {{ choice_formset.management_form }} +
+ {% for form in choice_formset %} +
+ {{ form.choice_text.label_tag }} + {{ form.choice_text }} +
+ {% endfor %} +
+ + +
+
+{% 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 %} +
+ {% for poll in polls %} + +
+
{{ poll.question }}
+ {{ poll.created_at|date:"d.m.Y" }} +
+

For team: {{ poll.team.name }}

+ Created by: {{ poll.creator.get_full_name }} +
+ {% endfor %} +
+ {% else %} + + {% 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 }}

+ + + +
+ Back to Vote + Back to All Polls +
+
+{% 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 @@ + {% if user.coached_teams.all %}