wiki:AdvancedApplicationDevelopment

Version 6 (modified by 211012, 2 weeks ago) ( diff )

--

Напреден апликативен развој

Во последната верзија апликацијата имплементирани се следните сценарија (сите од прототипот и дополнителни):

ID UseCase
1 Прегледува избирачки список
2 Ажурира избирачки список
3 Прегледува изборни единици
4 Ажурира изборни единици
5 Прегледува гласачки места
6 Aжурира гласачки места
7 Креира инстанца од избори
8 Пријавува учество на партија и кандидати
9 Назначува членови на комисија
10 Гласа онлајн
11 Доделува идентификациски код за гласање со физичко присуство
12 Гласа со физичко присуство
13 Прегледува вкупна излезност
14 Прегледува статистики од излезност по критериуми
15 Прегледува вкунпи резултати од избори со кандидат
16 Прегледува вкунпи резултати од избори со кандидатски листи
16 Прегледува статистики од резултати од избори по критериуми
17 Поднесува приговор
18 Одлучува по приговорот
19 Пријавува учество на кандидатски листи

Назначува членови на комисија

Администраторот пристапува на адреса /admin/committee каде му се отвора страница на која се прикажани сите комисии и формулар за внесување податоци за нова комисија. За ова е одговорен следниот контролер:

    @GetMapping("admin/committee")
    public String showCommittee(Model m) {
        m.addAttribute("realizations", electionRealizationService.findAll());
        m.addAttribute("committeeMembers", committeeMemberService.findAll());
        m.addAttribute("pollingStations", addressService.findAllPollingStations());
        m.addAttribute("committees", committeeService.findAll());
        m.addAttribute("committeeForm", new Committee());
        m.addAttribute("replaceTemplate", "add_comitee");
        return "admin";
    }

По внесување на податоците и клик на копчето „Запиши“ се праќа POST барање до контролерот кој ги обработува.

    @PostMapping("/admin/committee")
    public String addCommittee(
            @RequestParam(required = false) Long id,
            @RequestParam Long electionRealization,
            @RequestParam Long pollingStation,
            @RequestParam List<Long> membersId
    ) {
        committeeService.update(id, pollingStation, electionRealization, membersId);
        return "redirect:/admin/committee";
    }

Контролерот повикува функција од committeeService за додавање и ажурирање на комисии каде се креира нова инстанца од објектот Committee, и за истиот се полнат добиентите податоци од формуларот. Дополнително, се додаваат и членовите на комисијата кои се во many-to-many релација со комисијата. Оттаму се повикува функцијата save од committeeRepository кој наследува од JpaRepository. Таа служи за зачувување на новиот објект во базата. За да се обезбеди интегритет и конзистентност на податоците, методот е анотиран со @Transactional, односно функцијата успешно ќе заврши ако и само ако успешно се запишат и комисијата и членовите.

    @Transactional
    @Override
    public Committee update(Long id, Long pollingStationId, Long electionRealizationId, List<Long> membersId) {
        Committee committee;
        if (id != null){
            committee = findById(id);
        } else {
            committee = new Committee();
        }

        committee.setPollingStation(addressService.findPollingStationById(pollingStationId));
        committee.setElectionRealization(electionRealizationService.    findById(electionRealizationId));
        committeeRepository.save(committee);
        membersId.forEach(memberId -> addMemberToCommittee(committee.getId(), memberId));
        return committeeRepository.save(committee);
    }

Истава имплементација, преточена во SQL прашалникот кој се извршува позадински би изгледала вака:

DO $$
    DECLARE
        new_kom_id BIGINT;
    BEGIN
        INSERT INTO komisii (im_id, ri_id) VALUES (1, 1)
        RETURNING kom_id INTO new_kom_id;
    
        INSERT INTO se_clenovi_na (g_id, kom_id) VALUES (20, new_kom_id);
        INSERT INTO se_clenovi_na (g_id, kom_id) VALUES (21, new_kom_id);
        INSERT INTO se_clenovi_na (g_id, kom_id) VALUES (22, new_kom_id);
        INSERT INTO se_clenovi_na (g_id, kom_id) VALUES (23, new_kom_id);
        INSERT INTO se_clenovi_na (g_id, kom_id) VALUES (24, new_kom_id);
    
        COMMIT;
    END $$;

Доделува идентификациски код за гласање со физичко присуство

Корисниците со улога членови на комисија, можат да се најават на системот и пристапат на адресата /admin/electionRealizationInterface, при што добиваат преглед на избирачкиот список од реализацијата којашто се одржува на деновите на најава, за гласачкото место во кое се одговорни. Дополнително, на крајот на редот достапна е акцијата „Гласај“ со што можат да генерираат идентификациски код за гласање доколку гласачот сака да го оствари правото на глас со физичко присуство. Контролерот кој ја опслужува оваа акција е следен:

    @GetMapping("/admin/electionRealizationInterface")
    public String showElectionRealization(Model m, Principal p, Authentication authentication) {
        UserProfile userProfile = (UserProfile) authentication.getPrincipal();
        m.addAttribute("realization", committeeService.findElectionRealizationByCitizen(userProfile.getCitizen().getId()));
        m.addAttribute("citizens", committeeService.getCitizens(userProfile.getCitizen().getId()));
        m.addAttribute("replaceTemplate","election_realization_interface");
        return "index";
    }

За да се избегнат било какви манипулации и приказ на информации кои не треба да се достапни за најавениот корисник, неговиот идентитет се утврдува директно на серверска страна, од најавениот корисник. Дополнително во сервисите се процесираат реализацијата и избирачкиот список кој треба да го гледа корисникот.

    @Override
    public List<Citizen> getCitizens(Long committeeId) {
        CommitteeMember committeeMember = committeeMemberRepository.findById(committeeId).orElseThrow(RuntimeException::new);
        Committee committee = committeeRepository.findCommitteeByMembersContainsAndElectionRealization_DateIsAfter(committeeMember, LocalDate.now().minusDays(1));
        PollingStation pollingStation = committee.getPollingStation();
        return citizenRepository.findAllByAddress_PollingStation(pollingStation.getId()).stream().filter(x -> x instanceof Citizen).toList();
    }

Во committeeService-от се пронаѓа членот на комисија кој го претставува корисничкиот профил, комисијата во која е член, како и избирачкиот список за гласачкото место за кое е одговорна. Во програмската имплементација, ова се прави со повик на соодветни методи од Spring Data JPA, а во SQL би изгледало вака:

    select *
    from komisii k
    join se_clenovi_na ck on k.kom_id = ck.kom_id
    join realizacii_na_izbori ri on ri.ri_id = k.ri_id
    where ck.g_id = 1 and ri.ri_id = 1 and ri.ri_datum = CURRENT_DATE
    limit 1

Приказот кој го добива членот на комисијата е прикажан на следната слика. Во делот акции, достапно е времето на гласање за оние корисници кои правото на глас го оствариле онлајн или со физичко присуство, а за остантите достапно е копче за генерирање код за гласање. При клик се повикува следниот контролер:

    @GetMapping("/admin/electionRealizationInterface/{id}")
    public String showElectionRealization(Model m, @PathVariable Long id, Principal p, Authentication authentication) {
        UserProfile userProfile = (UserProfile) authentication.getPrincipal();
        Long realizationId = committeeService.findElectionRealizationByCitizen(userProfile.getCitizen().getId()).getId();
        CommitteeMember committeeMember = committeeMemberService.findById(userProfile.getCitizen().getId());
        if(!committeeService.getSamePollingStation(committeeMember.getId(), id)){
            return "redirect:/admin/electionRealizationInterface";
        }
        return "redirect:/admin/voteCode/" + realizationId + "/" + id;
    }

Како што е и претходно споменато, за да се избегнат манипулации, идентитетот повторно се проверува на серверската страна, односно се проверува дали корисникот кој го испратил барањето и гласачот за кој се генерира код се од исто гласачко место и потоа се пренасочува кон контролерот кој повикува сервис за генерирање на кодот доколку тој не постои или го прикажува веќе генерираниот. Кодот за гласање е од тип UUID, односно шансите за негово повторување или случајно погодување се практично еднакви на нула. Приказот кој го добива членот на комисијата по генерирање код изгледа вака.

Гласа со физичко присуство

Граѓанин кој сака да го оствари правото на глас со физичко присуство на потенцијално поставена машина во гласачко место може да го направи тоа користејќи го кодот добиен од избирачката комисија на страницата /codeVote, при што го гледа следниот интерфејс. Потоа, процесот на гласање продолжува исто како што тоа е опишано во делот од прототипот на апликацијата. При оваа акција за запишување на гласот, повторно е употребен концептот на трансакции бидејќи целта е кога некој гласа истовремено да се забележи и неговата излезност, па така, методот каде се запишува резултатот е анотиран со @Transactional.

    @Override
    @Transactional
    public CandidacyVote voteForCandidate(Long citizenId, UUID voteIdentificationCodeId, Long realizationId, Long id){

        Candidacy candidacy = candidacyService.findById(id);
        VoteIdentificationCode voteIdentificationCode = voteIdentificationCodeService.findById(voteIdentificationCodeId);
        ElectionRealization electionRealization = electionRealizationService.findById(realizationId);
        Citizen citizen = citizenService.findById(citizenId);
        PollingStation pollingStation = citizen.getAddress().getPollingStation();
        CandidacyVote candidacyVote = new CandidacyVote();
        candidacyVote.setVoteTimestamp(LocalDateTime.now());
        candidacyVote.setVoteIdentificationCode(voteIdentificationCode);
        candidacyVote.setElectionRealization(electionRealization);
        candidacyVote.setPollingStation(pollingStation);
        candidacyVote.pollingStation = pollingStation;
        candidacyVote.setCandidacy(candidacy);

        turnoutService.update(null, LocalDateTime.now(), citizenId, realizationId, pollingStation);

        return candidacyVoteRepository.save(candidacyVote);
    }

Соодветно, истата операција во SQL би изгледала вака:

DO
$$
    DECLARE
        new_gl_id BIGINT;
    BEGIN
        INSERT INTO glasovi (im_id, ri_id)
        VALUES (1, 1)
        RETURNING gl_id INTO new_gl_id;

        INSERT INTO glasovi_za_kandidat(gl_id) VALUES (new_gl_id);

        INSERT INTO glasanja (g_id, ri_id, im_id, ug_vreme) VALUES (1, 1, 1, now());

        COMMIT;
    END
$$;

Прегледува статистики од излезност по критериуми

На јавната страница /turnout достапни се податоци за процентот на излезност со можност за филтрирање по реализации, општини и/или гласачки места. Имајќи предвид дека се работи за домен каде што бројот на записи, особено во табелата за гласови и излезност ќе биде огромен, решивме овој дел да го имплементираме со материјализиран поглед кој ќе се обноввува на Х време, во нашиот случај 30 минути. На овој начин, наместо да го оптоваруваме Database Engine-от со агрегативни прашалници на секој пристап, податоците ќе се сервираат како готови од погледот. За таа цел е креиран следниот поглед:

SELECT row_number() OVER (ORDER BY op.o_ime)                                           AS row_num,
       op.o_id,
       op.o_ime,
       op.map_id,
       ri.ri_id,
       ri.ri_datum,
       ri.ri_ime,
       COALESCE((SELECT count(g.g_id)::numeric * 100.0 / count(DISTINCT gr.g_id)::numeric
                 FROM glasanja g
                          JOIN realizacii_na_izbori r ON r.ri_id = g.ri_id
                          LEFT JOIN gragjani gr ON gr.g_id = g.g_id
                          LEFT JOIN adresi a ON a.a_id = gr.a_id
                          LEFT JOIN opstini o ON o.o_id = a.o_id
                 WHERE o.o_id = op.o_id
                   AND r.ri_id = ri.ri_id
                 GROUP BY r.ri_id, r.ri_ime, r.ri_datum, o.o_id, o.o_ime), 0::numeric) AS total
FROM opstini op
         CROSS JOIN realizacii_na_izbori ri

Притоа, кога корисникот ќе изврши филтрирање се повикува следниот контролер:

    @GetMapping("/turnout")
    public String turnOutResults(Model m,
                                 @RequestParam(required = false, defaultValue = "2") Long realizationId,
                                 @RequestParam(required = false) String opshtinaId,
                                 @RequestParam(required = false) Long izbirachkoMestoId) {
        Double turnOut;
        if(opshtinaId != null && izbirachkoMestoId != null){
            turnOut = turnoutService.getTurnOutByRealizationAndMunicipalityAndPollingStation(realizationId, opshtinaId, izbirachkoMestoId);
        }else if(opshtinaId != null){
            turnOut = turnoutService.getTurnOutByRealizationAndMunicipality(realizationId, opshtinaId);
        }else {
            turnOut = turnoutService.turnOutByElectionRealization(realizationId);
        }
        m.addAttribute("replaceTemplate", "turnout_circle");
        m.addAttribute("turnoutPercentage", turnOut);
        m.addAttribute("realizationId", realizationId);
        m.addAttribute("municipalities", municipalityService.findAll());
        m.addAttribute("realizations", candidatesElectionRealizationService.findAll());
        m.addAttribute("pollingStations", addressService.findAllPollingStations());
        m.addAttribute("realization", candidatesElectionRealizationService.findById(realizationId));
        return "index";
    }

И приказот кој го добива корисникот е следниот:

Прегледува вкупни резултати од избори со кандидати/кандидатски листи

Attachments (9)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.