Trudny / egzaminacyjny · lekcja 26
Projekt 7: System głosowania i ankiet — wyniki na żywo
Czwarta i ostatnia z funkcjonalności nazwanych wprost w podstawie programowej (INF.04.7.3) — system głosowania/ankiet. Ten projekt uczy dwóch nowych umiejętności: AGREGACJI danych (zliczanie głosów pogrupowanych po opcji) oraz PREZENTACJI wyników jako pasków p...
Zacznij tutaj
Po co się tego uczymy?
Wprowadzenie
Teoria
Wymagania projektu:
- Tworzenie ankiety (WYMAGA zalogowania) — pytanie + od 2 do N opcji odpowiedzi;
- Głosowanie — KAŻDY zalogowany użytkownik może oddać JEDEN głos w danej ankiecie (blokada podwójnego głosowania);
- Wyniki jako wykres / paski procentowe — po oddaniu głosu (albo jeśli użytkownik już głosował) widoczne są wyniki: liczba głosów na każdą opcję ORAZ procent, zwizualizowany jako pasek o szerokości proporcjonalnej do procentu;
- Lista aktywnych ankiet oraz historia zakończonych.
Agregacja danych w EF Core — GroupBy i zliczanie. Do tej pory pobierałeś dane wprost (Sklep, lekcja 22) albo liczyłeś proste sumy przez .Count() w DTO (Portal społecznościowy, lekcja 24). Tutaj potrzebujesz zliczyć głosy pogrupowane WEDŁUG opcji odpowiedzi — do tego służy GroupBy:
var wyniki = await context.Glosy
.Where(g => g.AnkietaId == ankietaId)
.GroupBy(g => g.OpcjaId)
.Select(grupa => new WynikOpcjiDto
{
OpcjaId = grupa.Key,
LiczbaGlosow = grupa.Count()
})
.ToListAsync();
GroupBy(g => g.OpcjaId) dzieli WSZYSTKIE głosy danej ankiety na "koszyki" — jeden koszyk na każdą unikalną wartość OpcjaId. grupa.Key to identyfikator koszyka (czyli ID opcji), a grupa.Count() liczy, ile głosów trafiło do TEGO koszyka. EF Core tłumaczy to na SQL-owe GROUP BY z COUNT(*) — CAŁA agregacja dzieje się w bazie danych, nie w pamięci aplikacji.
Blokada podwójnego głosowania — unikalne ograniczenie na parze kolumn. Żeby użytkownik NIE MÓGŁ zagłosować dwa razy w tej samej ankiecie, tabela Glosy ma unikalny indeks na PARZE kolumn (AnkietaId, UzytkownikId) — konfigurowany w OnModelCreating:
modelBuilder.Entity<Glos>()
.HasIndex(g => new { g.AnkietaId, g.UzytkownikId })
.IsUnique();
Dzięki temu baza danych SAMA odrzuci próbę wstawienia drugiego głosu tego samego użytkownika w tej samej ankiecie (nawet gdyby backend miał błąd w logice) — to DODATKOWA warstwa ochrony NIEZALEŻNA od sprawdzania w kodzie C#.
Paski procentowe w Angularze — wiązanie stylu przez [style.width]. Zamiast wykresu z zewnętrznej biblioteki, wynik można pokazać jako prosty pasek <div>, którego szerokość wiążemy DYNAMICZNIE z obliczonym procentem: [style.width.%]="opcja.procent" — Angular sam przelicza to na style="width: 42%" w renderowanym HTML-u.
Zobacz zależności
Schemat
POST /api/ankiety/5/glosuj { opcjaId: 3 }
│
▼
Backend: sprawdź unikalny indeks (AnkietaId, UzytkownikId)
├─ już głosował → 409 Conflict
└─ nowy głos → zapis do tabeli Glosy → 200 OK
│
▼
GET /api/ankiety/5/wyniki
│
▼
context.Glosy.Where(AnkietaId=5).GroupBy(OpcjaId).Select(Count())
│
▼
[ {opcja:"Tak", liczba:12, procent:60}, {opcja:"Nie", liczba:8, procent:40} ]
│
▼
Angular: <div class="pasek" [style.width.%]="opcja.procent">{{opcja.procent}}%</div>
Połącz z praktyką
Przykład z życia
Ankiety na żywo podczas prezentacji (np. Slido, Mentimeter) czy głosowania w konkursach internetowych działają dokładnie na tej zasadzie: jeden głos na użytkownika, wyniki agregowane w czasie rzeczywistym i pokazywane jako paski lub wykres kołowy — pokazuje to, jak duża część "efektu wow" takich narzędzi to w istocie prosty GROUP BY w bazie danych i CSS.