Loading .gitignore +1 −0 Changes for .gitignore: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,3 +4,4 @@ db.sqlite3 .vscode .env deployment .DS_Store pyproject.toml +4 −0 Changes for pyproject.toml: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ dependencies = [ "django-rq~=4.0.1", "django-tasks-rq~=0.12.0", "gunicorn~=25.2.0", "matplotlib>=3.10.8", "psycopg[binary]~=3.3.3", "tzdata~=2025.3", ] Loading @@ -18,3 +19,6 @@ dependencies = [ dev = [ "honcho~=2.0.0", ] [tool.uv.sources] django-admin-autocomplete = { git = "https://github.com/drorsoft/django_charfield_autocomplete" } stats/admin.py +68 −3 Changes for stats/admin.py: 68 added lines, 3 removed lines. Original line number Diff line number Diff line from django.contrib import admin, messages from django.shortcuts import render, get_object_or_404 from django.db.models import Count, Q from django.contrib.admin.views.decorators import staff_member_required from django.urls import reverse from django.utils.html import format_html from django.utils.safestring import mark_safe from django.urls import path from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, HttpResponse import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import io from elections.models import Election from vote.models import Participation Loading Loading @@ -43,7 +48,9 @@ class ElectionStatsAdmin(admin.ModelAdmin): urls = super().get_urls() custom_urls = [ path('live-results/<int:election_id>', self.admin_site.admin_view(self.live_results_view), name='live_results'), path('statistics/<int:election_id>', self.admin_site.admin_view(self.statistics_view), name='statistics') path('statistics/<int:election_id>', self.admin_site.admin_view(self.statistics_view), name='statistics'), path('chartapi/results/<int:election_id>', self.admin_site.admin_view(self.results_chart_view), name='results_chart'), path('chartapi/attendance/<int:election_id>', self.admin_site.admin_view(self.attendance_chart_view), name='attendance_chart') ] return custom_urls + urls Loading Loading @@ -124,6 +131,64 @@ class ElectionStatsAdmin(admin.ModelAdmin): return render(request, 'admin/statistics.html', context) def results_chart_view(self, request, election_id): election = get_object_or_404(Election, pk=election_id) labels = list(election.option_set.values_list('name', flat=True)) values = list(election.option_set.values_list('votes', flat=True)) if election.allow_invalid: labels.append('GŁOS NIEWAŻNY') values.append(election.invalid_votes) total = sum(values) value_iterator = iter(values) fig, ax = plt.subplots() ax.set_title('Wyniki głosowania') def display_value(p): value = next(value_iterator) return f'{round(100*value/total, 1)}% ({value})' ax.pie(values, labels=labels, autopct=lambda p:display_value(p)) buffer = io.BytesIO() plt.savefig(buffer, format='svg') plt.close(fig) buffer.seek(0) return HttpResponse(buffer.read(), 'image/svg+xml') def attendance_chart_view(self, request, election_id): election = get_object_or_404(Election, pk=election_id) if election.is_active: return HttpResponse('Niedostępne dla trwających głosowań') groups = Group.objects.annotate( total_voters = Count('voter', distinct=True), voted_count = Count('voter__participation', filter=Q(voter__participation__election=election), distinct=True) ) labels = [group.display_name for group in groups] values = [(group.voted_count/group.total_voters if group.total_voters > 0 else 0) * 100 for group in groups] fig, ax = plt.subplots() bar = ax.bar(labels, values, label=labels) ax.bar_label(bar, label_type='center', fmt='%.1f%%') ax.set_xlabel('Klasa') ax.set_ylabel('Frekwencja [w %]') ax.set_title('Frekwencja według klasy') buffer = io.BytesIO() plt.savefig(buffer, format='svg') plt.close(fig) buffer.seek(0) return HttpResponse(buffer.read(), 'image/svg+xml') def has_add_permission(self, request): return False Loading uv.lock +397 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
.gitignore +1 −0 Changes for .gitignore: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,3 +4,4 @@ db.sqlite3 .vscode .env deployment .DS_Store
pyproject.toml +4 −0 Changes for pyproject.toml: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ dependencies = [ "django-rq~=4.0.1", "django-tasks-rq~=0.12.0", "gunicorn~=25.2.0", "matplotlib>=3.10.8", "psycopg[binary]~=3.3.3", "tzdata~=2025.3", ] Loading @@ -18,3 +19,6 @@ dependencies = [ dev = [ "honcho~=2.0.0", ] [tool.uv.sources] django-admin-autocomplete = { git = "https://github.com/drorsoft/django_charfield_autocomplete" }
stats/admin.py +68 −3 Changes for stats/admin.py: 68 added lines, 3 removed lines. Original line number Diff line number Diff line from django.contrib import admin, messages from django.shortcuts import render, get_object_or_404 from django.db.models import Count, Q from django.contrib.admin.views.decorators import staff_member_required from django.urls import reverse from django.utils.html import format_html from django.utils.safestring import mark_safe from django.urls import path from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect, HttpResponse import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import io from elections.models import Election from vote.models import Participation Loading Loading @@ -43,7 +48,9 @@ class ElectionStatsAdmin(admin.ModelAdmin): urls = super().get_urls() custom_urls = [ path('live-results/<int:election_id>', self.admin_site.admin_view(self.live_results_view), name='live_results'), path('statistics/<int:election_id>', self.admin_site.admin_view(self.statistics_view), name='statistics') path('statistics/<int:election_id>', self.admin_site.admin_view(self.statistics_view), name='statistics'), path('chartapi/results/<int:election_id>', self.admin_site.admin_view(self.results_chart_view), name='results_chart'), path('chartapi/attendance/<int:election_id>', self.admin_site.admin_view(self.attendance_chart_view), name='attendance_chart') ] return custom_urls + urls Loading Loading @@ -124,6 +131,64 @@ class ElectionStatsAdmin(admin.ModelAdmin): return render(request, 'admin/statistics.html', context) def results_chart_view(self, request, election_id): election = get_object_or_404(Election, pk=election_id) labels = list(election.option_set.values_list('name', flat=True)) values = list(election.option_set.values_list('votes', flat=True)) if election.allow_invalid: labels.append('GŁOS NIEWAŻNY') values.append(election.invalid_votes) total = sum(values) value_iterator = iter(values) fig, ax = plt.subplots() ax.set_title('Wyniki głosowania') def display_value(p): value = next(value_iterator) return f'{round(100*value/total, 1)}% ({value})' ax.pie(values, labels=labels, autopct=lambda p:display_value(p)) buffer = io.BytesIO() plt.savefig(buffer, format='svg') plt.close(fig) buffer.seek(0) return HttpResponse(buffer.read(), 'image/svg+xml') def attendance_chart_view(self, request, election_id): election = get_object_or_404(Election, pk=election_id) if election.is_active: return HttpResponse('Niedostępne dla trwających głosowań') groups = Group.objects.annotate( total_voters = Count('voter', distinct=True), voted_count = Count('voter__participation', filter=Q(voter__participation__election=election), distinct=True) ) labels = [group.display_name for group in groups] values = [(group.voted_count/group.total_voters if group.total_voters > 0 else 0) * 100 for group in groups] fig, ax = plt.subplots() bar = ax.bar(labels, values, label=labels) ax.bar_label(bar, label_type='center', fmt='%.1f%%') ax.set_xlabel('Klasa') ax.set_ylabel('Frekwencja [w %]') ax.set_title('Frekwencja według klasy') buffer = io.BytesIO() plt.savefig(buffer, format='svg') plt.close(fig) buffer.seek(0) return HttpResponse(buffer.read(), 'image/svg+xml') def has_add_permission(self, request): return False Loading