wiki:AdvancedApplicationDevelopment

Version 11 (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";
    }

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

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

На јавната страница /results достапни се податоци за резултатите од тековни и минати реализации со можност за филтрирање по реализации, општини и/или гласачки места. Очекувано, при реално користење оваа функционалност би била најкористена и најоптоварена, а истовремено повлекува големо количество податоци, односно податоци за сите гласови, кандидатури, избирачки места и слично. Со цел да направиме пооптимално решение, оваа го имплементиравме на начин што во базата е креирана функција којашто при креирање реализација креира материјализирани погледи за резултатите по општини и по избирачки места. На овој начин, обезбедуваме при освежување на погледот, соодветно да се освежи само погледот за моменталната реализација, без притоа да трошиме ресурси за агрегација на резултатите од претходни реализации, како и да не го оптоваруваме погледот со дополнителен, голем број редици.

create or replace function public.create_view_for_candidates_realizations()
        returns trigger
        language plpgsql
    as
    $$
    declare
        view_name text;
    begin
    
        view_name := format('results_per_polling_stations_%s', new.ri_id);
        raise notice '%', view_name;
        execute format('create materialized view if not exists %I as
        (
        select ri.ri_id, k.kan_id, gl.im_id, count(distinct gk.gl_id)
        from realizacii_na_izbori ri
                 join realizacii_so_kandidaturi rk on ri.ri_id = rk.ri_id
                 left join kandidaturi k on rk.ri_id = k.ri_id
                 left join glasovi_za_kandidat gk on k.kan_id = gk.kan_id
                 left join kandidati kan on kan.g_id = k.g_id
                 left join gragjani g on k.g_id = g.g_id
                 left join glasovi gl on gk.gl_id = gl.gl_id
        where ri.ri_id = %s
        group by ri.ri_id, k.kan_id, kan.g_id, gl.im_id
            );', view_name, new.ri_id);
    
        view_name := format('results_per_municipalities_%s', new.ri_id);
        raise notice '%', view_name;
        execute format('create materialized view if not exists %I as
        (
            select gk.kan_id, o.o_id, ri.ri_id, count(distinct gk.gl_id)
            from glasovi gl
            join glasovi_za_kandidat gk on gl.gl_id = gk.gl_id
            join izbiracki_mesta im on gl.im_id = im.im_id
            join adresi a on im.a_id = a.a_id
            join opstini o on a.o_id = o.o_id
            join realizacii_na_izbori ri on gl.ri_id = ri.ri_id
            where ri.ri_id = %s
            group by gk.kan_id, o.o_id, ri.ri_id
            );', view_name, new.ri_id);
    
        return new;
    end;
    $$;
    create or replace function public.create_view_for_lists_realizations()
        returns trigger
        language plpgsql
    as
    $$
    declare
        view_name text;
    begin
    
        view_name := format('lists_results_per_polling_stations_%s', new.ri_id);
        raise notice '%', view_name;
        execute format('create materialized view if not exists %I as
        (
            select kl.kl_id, gl.im_id, ri.ri_id, count(distinct gli.gl_id)
            from glasovi gl
                     join glasovi_za_lista gli on gl.gl_id = gli.gl_id
                     join kandidatski_listi kl on gli.kl_id = kl.kl_id
                     join izbiracki_mesta im on gl.im_id = im.im_id
                     join realizacii_na_izbori ri on gl.ri_id = ri.ri_id
            where ri.ri_id = %s
            group by kl.kl_id, gl.im_id, ri.ri_id);', view_name, new.ri_id);
    
        view_name := format('lists_results_per_municipalities_%s', new.ri_id);
        raise notice '%', view_name;
        execute format('create materialized view if not exists %I as
        (
            select kl.kl_id, o.o_id, ri.ri_id, count(distinct gli.gl_id)
            from glasovi gl
                     join glasovi_za_lista gli on gl.gl_id = gli.gl_id
                     join kandidatski_listi kl on gli.kl_id = kl.kl_id
                     join izbiracki_mesta im on gl.im_id = im.im_id
                     join adresi a on im.a_id = a.a_id
                     join opstini o on a.o_id = o.o_id
                     join realizacii_na_izbori ri on gl.ri_id = ri.ri_id
            where ri.ri_id = %s
            group by kl.kl_id, o.o_id, ri.ri_id
            );', view_name, new.ri_id);
    
        return new;
    end;
    $$;

Кога корисникот ќе пристапи до страницата за резултати, наместо во моментот да се изврши прашалник со агрегативни функции, преку Spring Data JPA се повикува друга функција во базата, која врз основа на идентификаторот на реализацијата ги враќа резултатите од соодветниот поглед. Вака добиените резултати, понатаму се мапираат во DTO објект кој служи за нивно прикажување на страница.

    create or replace function public.get_view_for_candidates_realizations(real_id bigint)
        returns table(ri_id bigint, kan_id bigint, im_id bigint, vote_count bigint)
        language plpgsql
    as
    $$
    declare
        view_name text;
        result record;
    begin
    
        view_name := format('results_per_polling_stations_%s', real_id);
        raise notice '%', view_name;
        return query execute format('SELECT * FROM %s', view_name);
    end;
    $$;
    create or replace function public.get_view_for_lists_realizations(real_id bigint)
        returns table(kl_id bigint, ri_id bigint, im_id bigint, vote_count bigint)
        language plpgsql
    as
    $$
    declare
        view_name text;
        result record;
    begin
    
        view_name := format('lists_results_per_polling_stations_%s', real_id);
        raise notice '%', view_name;
        return query execute format('SELECT * FROM %s', view_name);
    end;
    $$;

create or replace function public.get_view_for_candidates_realizations_by_municipalities(real_id bigint)
        returns table(kan_id bigint, o_id bigint, ri_id bigint, vote_count bigint)
        language plpgsql
    as
    $$
    declare
        view_name text;
        result record;
    begin
    
        view_name := format('results_per_municipalities_%s', real_id);
        raise notice '%', view_name;
        return query execute format('SELECT * FROM %s', view_name);
    end;
    $$;
create or replace function public.get_view_for_lists_realizations_by_municipalities(real_id bigint)
        returns table(gl_id bigint, o_id bigint, ri_id bigint, vote_count bigint)
        language plpgsql
    as
    $$
    declare
        view_name text;
        result record;
    begin
    
        view_name := format('lists_results_per_municipalities_%s', real_id);
        raise notice '%', view_name;
        return query execute format('SELECT * FROM %s', view_name);
    end;
    $$;

Притоа, во соодветни приватни функции од ResultsService-от се врши мапирањето на следниот начин и кон view-то се враќа објектот за репрезентација.

    List<TotalListResultsPerPollingStation> listResultsPerPollingStation(Long realizationId, Long pollingStationId) {
        List<Map<String, Object>> results = turnoutRepository.totalListResultsPerPollingStation(realizationId, pollingStationId);
        return results.stream().map(x -> {
            TotalListResultsPerPollingStation totalListResultsPerPollingStation = new TotalListResultsPerPollingStation();
            totalListResultsPerPollingStation.setListName((x.get("list_name").toString()));
            totalListResultsPerPollingStation.setPartyName((String) x.get("participant").toString());
            totalListResultsPerPollingStation.setVoteCount((String) x.get("vote_count").toString());
            totalListResultsPerPollingStation.setPollingStationId((String) x.get("polling_station_id").toString());
            return totalListResultsPerPollingStation;
        }).toList();
    }

Пријавува учество на кандидатски листи

Администраторот со пристап до адресата /candidatesLists добива приказ на листа со постоечки кандидатски листи и можност за креирање на нова кандидатска листа со избор на општина, реализација и изборна единица. Потоа следи одбирање на кандидатите при што бројот е лимитиран на 20, според изборниот законик во Македонија. Креирањето на кандидатската листа е исто така имплементирано во вид на трансакција, односно методот е анотиран со @Transactional.

    @Override
    @Transactional
    public CandidatesList update(Long id, String description, Long partyId, Long candidatesListElectionRealizationId, Long municipalityId, Long electoralUnitId, List<Long> candidatesInList) {
        if(candidatesInList.size() != 20)
        {
            throw new RuntimeException("Candidates list must have 20 candidates!");
        }
        
        CandidatesList list;

        ElectoralUnit electoralUnit = electoralUnitId != null ? electoralUnitService.findById(electoralUnitId) : null;
        Municipality municipality = municipalityId != null ? municipalityService.findById(municipalityId) : null;
        CandidatesListElectionRealization candidatesListElectionRealization = candidatesListElectionRealizationService.findById(candidatesListElectionRealizationId);
        Party party = partyService.findById(partyId);

        if (id != null) {
            list = findById(id);
            list.getCandidates().clear();
            repository.save(list);
        } else {
            throw new IllegalArgumentException("Id must be provided for update operation.");
        }

        candidatesInList.stream()
                .filter(Objects::nonNull)
                .map(candidateService::findById)
                .forEach(x -> list.candidates.add(x));
        list.setDescription(description);
        list.setParty(party);
        list.setMunicipality(municipality);
        list.setElectoralUnit(electoralUnit);
        list.setCandidatesListElectionRealization(candidatesListElectionRealization);
        return repository.save(list);
    }

Attachments (9)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.