Loading elections/admin.py +7 −1 Changes for elections/admin.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class OptionInline(admin.TabularInline): extra = 1 class ElectionAdmin(admin.ModelAdmin): list_display = ['name', 'start_date', 'end_date', 'is_active_status', 'view_live_results'] list_display = ['name', 'start_date', 'end_date', 'is_active_status', 'view_live_results', 'view_statistics'] ordering = ['-created_at', '-updated_at'] # exclude = ['invalid_votes'] Loading @@ -26,6 +26,12 @@ class ElectionAdmin(admin.ModelAdmin): url = reverse('stats:live_results', args=[obj.id]) return format_html('<a href={}>WYŚWIETL</a>', url) @admin.display(description='Statystyki') def view_statistics(self, obj): if obj.is_active: return format_html('<span style="color: #999;">{}Dostępne po zakończeniu głosowania</span>', '') url = reverse('stats:statistics', args=[obj.id]) return format_html('<a href={}>WYŚWIETL</a>', url) # Register your models here. admin.site.register(Election, ElectionAdmin) No newline at end of file stats/admin.py +46 −1 Changes for stats/admin.py: 46 added lines, 1 removed line. Original line number Diff line number Diff line from django.contrib import admin from django.shortcuts import render from django.shortcuts import render, get_object_or_404 from django.db.models import Count, Q from elections.models import Election from vote.models import Participation from voters.models import Voter, Group def live_results(request, election_id): context = admin.site.each_context(request) Loading @@ -11,3 +14,45 @@ def live_results(request, election_id): }) return render(request, 'stats/live_results.html', context) def statistics(request, election_id): election = get_object_or_404(Election, id=election_id) context = admin.site.each_context(request) total_eligible = Voter.objects.count() total_participated = Participation.objects.filter(election=election).count() overall_attendance = (total_participated / total_eligible * 100) if total_eligible > 0 else 0 stats_raw = Voter.objects.values('group').annotate( total_voters=Count('id'), voted_count=Count( 'participation', filter=Q(participation__election=election) ) ).order_by('group') groups_map = {g.id: g.display_name for g in Group.objects.all()} stats_by_class = [] for item in stats_raw: total = item['total_voters'] voted = item['voted_count'] stats_by_class.append({ 'class_name': groups_map.get(item['group']), 'voted_count': voted, 'total_voters': total, 'percentage': round((voted / total * 100), 2) if total > 0 else 0 }) context.update({ 'request': request, 'election_id': election_id, 'election': election, 'overall_attendance': round(overall_attendance, 2), 'total_participated': total_participated, 'total_eligible': total_eligible, 'stats_by_class': stats_by_class, }) return render(request, 'stats/statistics.html', context) No newline at end of file stats/templates/stats/live_results.html +47 −4 Changes for stats/templates/stats/live_results.html: 47 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -3,7 +3,27 @@ {% block content %} <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script> <div style="height:40vh; width:80vw"><canvas id="chart"></canvas></div> <div style="height:40vh; width:80vw"> <canvas id="chart"></canvas> <div class="table-wrapper"> <table class="results-table" id="data-table"> <thead> <tr> <th>Opcja</th> <th style="text-align: right;">Liczba głosów</th> </tr> </thead> <tbody id="table-body"></tbody> <tfoot> <tr class="total-row"> <td>Suma głosów</td> <td id="total-votes" style="text-align: right;">0</td> </tr> </tfoot> </table> </div> </div> <script> async function getElectionData() { Loading Loading @@ -69,7 +89,29 @@ ); } async function updateChart() { function updateTable(data) { const tbody = document.getElementById('table-body'); const totalCell = document.getElementById('total-votes'); let html = ''; let total = 0; data.labels.forEach((label, index) => { const voteCount = data.votes[index]; total += voteCount; html += ` <tr> <td>${label}</td> <td style="text-align: right;">${voteCount}</td> </tr> `; }); tbody.innerHTML = html; totalCell.innerText = total; } async function updateUI() { const freshData = await getElectionData(); if(freshData){ Loading @@ -80,10 +122,11 @@ resultsChart.data.datasets[0].data = freshData.votes; resultsChart.update(); } updateTable(freshData); } } updateChart(); setInterval(updateChart, 5000); updateUI(); setInterval(updateUI, 5000); </script> {% endblock %} No newline at end of file stats/templates/stats/statistics.html 0 → 100644 +151 −0 Changes for stats/templates/stats/statistics.html: 151 added lines, 0 removed lines. Original line number Diff line number Diff line {% extends 'admin/base_site.html' %} {% block content %} <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script> <div style="padding: 20px;"> <h2>Statystyki: {{ election.name }}</h2> <div style="display: flex; gap: 20px; margin-bottom: 30px;"> <div style="padding: 20px; border-radius: 8px; flex: 1; text-align: center; border: 1px solid #ddd;"> <h3>Wykres</h3> <div style="height: 40vh; width: 100%;"> <canvas id="chart"></canvas> </div> </div> <div style="padding: 20px; border-radius: 8px; flex: 1; text-align: center; border: 1px solid #ddd;"> <h3>Tabela</h3> <div class="table-wrapper"> <table class="results-table" id="data-table"> <thead> <tr> <th>Opcja</th> <th style="text-align: right;">Liczba głosów</th> </tr> </thead> <tbody id="table-body"></tbody> <tfoot> <tr class="total-row"> <td>Suma głosów</td> <td id="total-votes" style="text-align: right;">0</td> </tr> </tfoot> </table> </div> </div> </div> <h3>Frekwencja w klasach</h3> <table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;"> <thead> <tr> <th>Klasa</th> <th>Głosy</th> <th>%</th> </tr> </thead> <tbody> {% for item in stats_by_class %} <tr style="border-bottom: 1px solid #eee;"> <td>{{ item.class_name }}</td> <td>{{ item.voted_count }} / {{ item.total_voters }}</td> <td style="font-weight: bold;">{{ item.percentage }}%</td> </tr> {% endfor %} </tbody> </table> <div style="border-radius: 8px; text-align: center; border: 1px solid #ddd;"> <h3>Ogólna frekwencja</h3> <p style="font-size: 2.5em; font-weight: bold; color: #417690;">{{ overall_attendance }}%</p> <p>{{ total_participated }} / {{ total_eligible }} oddanych głosów</p> </div> </div> <script> async function getElectionData() { const apiUrl = "{{ request.scheme }}://{{ request.get_host }}{% url 'vote:api_internal_results' %}?election_id={{ election_id }}"; try { const response = await fetch(apiUrl); const data = await response.json(); const labels = data.options.map(option => option.name); const votes = data.options.map(option => option.votes); return { name: data.name, labels: labels, votes: votes }; } catch (error) { console.error('Failed to fetch results', error); } } let resultsChart; function initializeChart(data) { const ctx = document.getElementById('chart').getContext('2d'); resultsChart = new Chart( ctx, { type: 'pie', data: { labels: data.labels, datasets: [{ label: 'Liczba głosów', data: data.votes, }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' } } } } ); } function initializeTable(data) { const tbody = document.getElementById('table-body'); const totalCell = document.getElementById('total-votes'); let html = ''; let total = 0; data.labels.forEach((label, index) => { const voteCount = data.votes[index]; total += voteCount; html += ` <tr> <td>${label}</td> <td style="text-align: right;">${voteCount}</td> </tr> `; }); tbody.innerHTML = html; totalCell.innerText = total; } async function displayResults() { const data = await getElectionData(); initializeChart(data); initializeTable(data); } displayResults(); </script> {% endblock %} No newline at end of file stats/urls.py +1 −0 Changes for stats/urls.py: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -6,4 +6,5 @@ app_name = 'stats' urlpatterns = [ path('admin/stats/live_results/<int:election_id>', admin.live_results, name='live_results'), path('admin/stats/statistics/<int:election_id>', admin.statistics, name='statistics'), ] No newline at end of file Loading
elections/admin.py +7 −1 Changes for elections/admin.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class OptionInline(admin.TabularInline): extra = 1 class ElectionAdmin(admin.ModelAdmin): list_display = ['name', 'start_date', 'end_date', 'is_active_status', 'view_live_results'] list_display = ['name', 'start_date', 'end_date', 'is_active_status', 'view_live_results', 'view_statistics'] ordering = ['-created_at', '-updated_at'] # exclude = ['invalid_votes'] Loading @@ -26,6 +26,12 @@ class ElectionAdmin(admin.ModelAdmin): url = reverse('stats:live_results', args=[obj.id]) return format_html('<a href={}>WYŚWIETL</a>', url) @admin.display(description='Statystyki') def view_statistics(self, obj): if obj.is_active: return format_html('<span style="color: #999;">{}Dostępne po zakończeniu głosowania</span>', '') url = reverse('stats:statistics', args=[obj.id]) return format_html('<a href={}>WYŚWIETL</a>', url) # Register your models here. admin.site.register(Election, ElectionAdmin) No newline at end of file
stats/admin.py +46 −1 Changes for stats/admin.py: 46 added lines, 1 removed line. Original line number Diff line number Diff line from django.contrib import admin from django.shortcuts import render from django.shortcuts import render, get_object_or_404 from django.db.models import Count, Q from elections.models import Election from vote.models import Participation from voters.models import Voter, Group def live_results(request, election_id): context = admin.site.each_context(request) Loading @@ -11,3 +14,45 @@ def live_results(request, election_id): }) return render(request, 'stats/live_results.html', context) def statistics(request, election_id): election = get_object_or_404(Election, id=election_id) context = admin.site.each_context(request) total_eligible = Voter.objects.count() total_participated = Participation.objects.filter(election=election).count() overall_attendance = (total_participated / total_eligible * 100) if total_eligible > 0 else 0 stats_raw = Voter.objects.values('group').annotate( total_voters=Count('id'), voted_count=Count( 'participation', filter=Q(participation__election=election) ) ).order_by('group') groups_map = {g.id: g.display_name for g in Group.objects.all()} stats_by_class = [] for item in stats_raw: total = item['total_voters'] voted = item['voted_count'] stats_by_class.append({ 'class_name': groups_map.get(item['group']), 'voted_count': voted, 'total_voters': total, 'percentage': round((voted / total * 100), 2) if total > 0 else 0 }) context.update({ 'request': request, 'election_id': election_id, 'election': election, 'overall_attendance': round(overall_attendance, 2), 'total_participated': total_participated, 'total_eligible': total_eligible, 'stats_by_class': stats_by_class, }) return render(request, 'stats/statistics.html', context) No newline at end of file
stats/templates/stats/live_results.html +47 −4 Changes for stats/templates/stats/live_results.html: 47 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -3,7 +3,27 @@ {% block content %} <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script> <div style="height:40vh; width:80vw"><canvas id="chart"></canvas></div> <div style="height:40vh; width:80vw"> <canvas id="chart"></canvas> <div class="table-wrapper"> <table class="results-table" id="data-table"> <thead> <tr> <th>Opcja</th> <th style="text-align: right;">Liczba głosów</th> </tr> </thead> <tbody id="table-body"></tbody> <tfoot> <tr class="total-row"> <td>Suma głosów</td> <td id="total-votes" style="text-align: right;">0</td> </tr> </tfoot> </table> </div> </div> <script> async function getElectionData() { Loading Loading @@ -69,7 +89,29 @@ ); } async function updateChart() { function updateTable(data) { const tbody = document.getElementById('table-body'); const totalCell = document.getElementById('total-votes'); let html = ''; let total = 0; data.labels.forEach((label, index) => { const voteCount = data.votes[index]; total += voteCount; html += ` <tr> <td>${label}</td> <td style="text-align: right;">${voteCount}</td> </tr> `; }); tbody.innerHTML = html; totalCell.innerText = total; } async function updateUI() { const freshData = await getElectionData(); if(freshData){ Loading @@ -80,10 +122,11 @@ resultsChart.data.datasets[0].data = freshData.votes; resultsChart.update(); } updateTable(freshData); } } updateChart(); setInterval(updateChart, 5000); updateUI(); setInterval(updateUI, 5000); </script> {% endblock %} No newline at end of file
stats/templates/stats/statistics.html 0 → 100644 +151 −0 Changes for stats/templates/stats/statistics.html: 151 added lines, 0 removed lines. Original line number Diff line number Diff line {% extends 'admin/base_site.html' %} {% block content %} <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script> <div style="padding: 20px;"> <h2>Statystyki: {{ election.name }}</h2> <div style="display: flex; gap: 20px; margin-bottom: 30px;"> <div style="padding: 20px; border-radius: 8px; flex: 1; text-align: center; border: 1px solid #ddd;"> <h3>Wykres</h3> <div style="height: 40vh; width: 100%;"> <canvas id="chart"></canvas> </div> </div> <div style="padding: 20px; border-radius: 8px; flex: 1; text-align: center; border: 1px solid #ddd;"> <h3>Tabela</h3> <div class="table-wrapper"> <table class="results-table" id="data-table"> <thead> <tr> <th>Opcja</th> <th style="text-align: right;">Liczba głosów</th> </tr> </thead> <tbody id="table-body"></tbody> <tfoot> <tr class="total-row"> <td>Suma głosów</td> <td id="total-votes" style="text-align: right;">0</td> </tr> </tfoot> </table> </div> </div> </div> <h3>Frekwencja w klasach</h3> <table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;"> <thead> <tr> <th>Klasa</th> <th>Głosy</th> <th>%</th> </tr> </thead> <tbody> {% for item in stats_by_class %} <tr style="border-bottom: 1px solid #eee;"> <td>{{ item.class_name }}</td> <td>{{ item.voted_count }} / {{ item.total_voters }}</td> <td style="font-weight: bold;">{{ item.percentage }}%</td> </tr> {% endfor %} </tbody> </table> <div style="border-radius: 8px; text-align: center; border: 1px solid #ddd;"> <h3>Ogólna frekwencja</h3> <p style="font-size: 2.5em; font-weight: bold; color: #417690;">{{ overall_attendance }}%</p> <p>{{ total_participated }} / {{ total_eligible }} oddanych głosów</p> </div> </div> <script> async function getElectionData() { const apiUrl = "{{ request.scheme }}://{{ request.get_host }}{% url 'vote:api_internal_results' %}?election_id={{ election_id }}"; try { const response = await fetch(apiUrl); const data = await response.json(); const labels = data.options.map(option => option.name); const votes = data.options.map(option => option.votes); return { name: data.name, labels: labels, votes: votes }; } catch (error) { console.error('Failed to fetch results', error); } } let resultsChart; function initializeChart(data) { const ctx = document.getElementById('chart').getContext('2d'); resultsChart = new Chart( ctx, { type: 'pie', data: { labels: data.labels, datasets: [{ label: 'Liczba głosów', data: data.votes, }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' } } } } ); } function initializeTable(data) { const tbody = document.getElementById('table-body'); const totalCell = document.getElementById('total-votes'); let html = ''; let total = 0; data.labels.forEach((label, index) => { const voteCount = data.votes[index]; total += voteCount; html += ` <tr> <td>${label}</td> <td style="text-align: right;">${voteCount}</td> </tr> `; }); tbody.innerHTML = html; totalCell.innerText = total; } async function displayResults() { const data = await getElectionData(); initializeChart(data); initializeTable(data); } displayResults(); </script> {% endblock %} No newline at end of file
stats/urls.py +1 −0 Changes for stats/urls.py: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -6,4 +6,5 @@ app_name = 'stats' urlpatterns = [ path('admin/stats/live_results/<int:election_id>', admin.live_results, name='live_results'), path('admin/stats/statistics/<int:election_id>', admin.statistics, name='statistics'), ] No newline at end of file