Skip to content

Commit 435ebc7

Browse files
authored
Fix listing election rounds (#893)
1 parent 6865106 commit 435ebc7

File tree

8 files changed

+171
-89
lines changed

8 files changed

+171
-89
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using Dapper;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Vote.Monitor.Core.Converters;
24

35
namespace Vote.Monitor.Api.Feature.ElectionRound;
46

57
public static class ElectionRoundFeatureInstaller
68
{
79
public static IServiceCollection AddElectionRoundFeature(this IServiceCollection services)
810
{
11+
SqlMapper.AddTypeHandler(typeof(MonitoringNgoModel[]), new JsonToObjectConverter<MonitoringNgoModel[]>());
12+
913
return services;
1014
}
1115
}

api/src/Vote.Monitor.Api.Feature.ElectionRound/ElectionRoundModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static MonitoringNgoModel FromEntity(Ngo ngo)
5656
public string Name { get; private set; }
5757
public NgoStatus Status { get; private set; }
5858

59+
[JsonConstructor]
5960
private MonitoringNgoModel(Guid id, string name, NgoStatus status)
6061
{
6162
Name = name;
Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,139 @@
1-
using Vote.Monitor.Core.Models;
1+
using Dapper;
2+
using Vote.Monitor.Core.Models;
3+
using Vote.Monitor.Domain.ConnectionFactory;
4+
using Vote.Monitor.Domain.Specifications;
25

36
namespace Vote.Monitor.Api.Feature.ElectionRound.List;
47

5-
public class Endpoint(IReadRepository<ElectionRoundAggregate> repository)
6-
: Endpoint<Request, Results<Ok<PagedResponse<ElectionRoundModel>>, ProblemDetails>>
8+
public class Endpoint(INpgsqlConnectionFactory dbConnectionFactory)
9+
: Endpoint<Request, Results<Ok<PagedResponse<Response>>, ProblemDetails>>
710
{
811
public override void Configure()
912
{
1013
Get("/api/election-rounds");
1114
Policies(PolicyNames.PlatformAdminsOnly);
1215
}
1316

14-
public override async Task<Results<Ok<PagedResponse<ElectionRoundModel>>, ProblemDetails>> ExecuteAsync(Request req, CancellationToken ct)
17+
public override async Task<Results<Ok<PagedResponse<Response>>, ProblemDetails>> ExecuteAsync(Request req,
18+
CancellationToken ct)
1519
{
16-
var specification = new ListElectionRoundsSpecification(req);
17-
var electionRounds = await repository.ListAsync(specification, ct);
18-
var electionRoundsCount = await repository.CountAsync(specification, ct);
20+
var sql = """
21+
SELECT COUNT(*)
22+
FROM "ElectionRounds" ER
23+
WHERE (@searchText IS NULL
24+
OR ER."Title" ILIKE @searchText
25+
OR ER."EnglishTitle" ILIKE @searchText
26+
OR ER."Id"::TEXT ILIKE @searchText)
27+
AND (@electionRoundStatus IS NULL OR ER."Status" = @electionRoundStatus)
28+
AND (@countryId IS NULL OR ER."CountryId" = @countryId);
1929
20-
return TypedResults.Ok(new PagedResponse<ElectionRoundModel>(electionRounds, electionRoundsCount, req.PageNumber, req.PageSize));
30+
WITH FilteredElections as (SELECT ER."Id",
31+
ER."Title",
32+
ER."EnglishTitle",
33+
ER."StartDate",
34+
ER."Status",
35+
ER."CreatedOn",
36+
ER."LastModifiedOn",
37+
ER."CountryId",
38+
"C"."Iso2" AS "CountryIso2",
39+
"C"."Iso3" AS "CountryIso3",
40+
"C"."Name" AS "CountryName",
41+
"C"."FullName" AS "CountryFullName",
42+
"C"."NumericCode" AS "CountryNumericCode",
43+
COALESCE((select jsonb_agg(jsonb_build_object('Id', MN."Id", 'Name', n."Name", 'Status', n."Status"))
44+
FROM "MonitoringNgos" MN
45+
INNER join "Ngos" n on n."Id" = mn."NgoId"
46+
where MN."ElectionRoundId" = ER."Id"), '[]'::JSONB) AS "MonitoringNgos"
47+
FROM "ElectionRounds" ER
48+
INNER JOIN "Countries" "C" ON ER."CountryId" = "C"."Id"
49+
WHERE (@searchText IS NULL
50+
OR ER."Title" ILIKE @searchText
51+
OR ER."EnglishTitle" ILIKE @searchText
52+
OR ER."Id"::TEXT ILIKE @searchText)
53+
AND (@electionRoundStatus IS NULL OR ER."Status" = @electionRoundStatus)
54+
AND (@countryId IS NULL OR ER."CountryId" = @countryId))
55+
56+
select *
57+
from FilteredElections
58+
ORDER BY CASE WHEN @sortExpression = 'Title ASC' THEN "Title" END ASC,
59+
CASE WHEN @sortExpression = 'Title DESC' THEN "Title" END DESC,
60+
CASE WHEN @sortExpression = 'EnglishTitle ASC' THEN "EnglishTitle" END ASC,
61+
CASE WHEN @sortExpression = 'EnglishTitle DESC' THEN "EnglishTitle" END DESC,
62+
CASE WHEN @sortExpression = 'StartDate ASC' THEN "StartDate" END ASC,
63+
CASE WHEN @sortExpression = 'StartDate DESC' THEN "StartDate" END DESC,
64+
CASE WHEN @sortExpression = 'Status ASC' THEN "Status" END ASC,
65+
CASE WHEN @sortExpression = 'Status DESC' THEN "Status" END DESC,
66+
CASE WHEN @sortExpression = 'CountryName ASC' THEN "CountryName" END ASC,
67+
CASE WHEN @sortExpression = 'CountryName DESC' THEN "CountryName" END DESC,
68+
CASE WHEN @sortExpression = 'MonitoringNgos ASC' THEN JSONB_ARRAY_LENGTH("MonitoringNgos") END ASC,
69+
CASE WHEN @sortExpression = 'MonitoringNgos DESC' THEN JSONB_ARRAY_LENGTH("MonitoringNgos") END DESC
70+
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY;
71+
""";
72+
73+
var queryArgs = new
74+
{
75+
searchText = $"%{req.SearchText?.Trim() ?? string.Empty}%",
76+
offset = PaginationHelper.CalculateSkip(req.PageSize, req.PageNumber),
77+
pageSize = req.PageSize,
78+
electionroundstatus = req.ElectionRoundStatus?.ToString(),
79+
countryId = req.CountryId,
80+
sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting)
81+
};
82+
83+
int totalRowCount = 0;
84+
List<Response> entries;
85+
86+
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
87+
{
88+
using var multi = await dbConnection.QueryMultipleAsync(sql, queryArgs);
89+
totalRowCount = multi.Read<int>().Single();
90+
entries = multi.Read<Response>().ToList();
91+
}
92+
93+
return TypedResults.Ok(
94+
new PagedResponse<Response>(entries, totalRowCount, req.PageNumber, req.PageSize));
95+
}
96+
97+
private static string GetSortExpression(string? sortColumnName, bool isAscendingSorting)
98+
{
99+
if (string.IsNullOrWhiteSpace(sortColumnName))
100+
{
101+
return $"{nameof(List.Response.StartDate)} DESC";
102+
}
103+
104+
var sortOrder = isAscendingSorting ? "ASC" : "DESC";
105+
106+
107+
if (string.Equals(sortColumnName, nameof(List.Response.Title),
108+
StringComparison.InvariantCultureIgnoreCase))
109+
{
110+
return $"{nameof(List.Response.Title)} {sortOrder}";
111+
}
112+
113+
if (string.Equals(sortColumnName, nameof(List.Response.EnglishTitle),
114+
StringComparison.InvariantCultureIgnoreCase))
115+
{
116+
return $"{nameof(List.Response.EnglishTitle)} {sortOrder}";
117+
}
118+
119+
if (string.Equals(sortColumnName, nameof(List.Response.StartDate),
120+
StringComparison.InvariantCultureIgnoreCase))
121+
{
122+
return $"{nameof(List.Response.StartDate)} {sortOrder}";
123+
}
124+
125+
if (string.Equals(sortColumnName, nameof(List.Response.CountryName),
126+
StringComparison.InvariantCultureIgnoreCase))
127+
{
128+
return $"{nameof(List.Response.CountryName)} {sortOrder}";
129+
}
130+
131+
if (string.Equals(sortColumnName, nameof(List.Response.MonitoringNgos),
132+
StringComparison.InvariantCultureIgnoreCase))
133+
{
134+
return $"{nameof(List.Response.MonitoringNgos)} {sortOrder}";
135+
}
136+
137+
return $"{nameof(List.Response.StartDate)} DESC";
21138
}
22139
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Vote.Monitor.Api.Feature.ElectionRound.List;
2+
3+
public record Response
4+
{
5+
public Guid Id { get; init; }
6+
public Guid CountryId { get; init; }
7+
8+
public string CountryIso2 { get; init; } = string.Empty;
9+
10+
public string CountryIso3 { get; init; } = string.Empty;
11+
12+
public string CountryNumericCode { get; init; } = string.Empty;
13+
14+
public string CountryName { get; init; } = string.Empty;
15+
16+
public string CountryFullName { get; init; } = string.Empty;
17+
18+
public string Title { get; init; } = string.Empty;
19+
public string EnglishTitle { get; init; } = string.Empty;
20+
public DateOnly StartDate { get; init; }
21+
22+
public ElectionRoundStatus Status { get; init; } = ElectionRoundStatus.NotStarted;
23+
24+
public DateTime CreatedOn { get; init; }
25+
public DateTime? LastModifiedOn { get; init; }
26+
27+
public MonitoringNgoModel[] MonitoringNgos { get; init; } = [];
28+
}

api/tests/Vote.Monitor.Api.Feature.ElectionRound.UnitTests/Endpoints/ListEndpointTests.cs

Lines changed: 0 additions & 74 deletions
This file was deleted.

api/tests/Vote.Monitor.Api.IntegrationTests/Features/FormSubmissions/UpdateStatusTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void CoalitionAdminsCanUpdateStatusForFormSubmissionButNotForMembersObser
6868
}
6969

7070
[Test]
71-
public void CoalitionMemberAdminsCanUpdateStatusForFormSubmissionButNotForOhterMembersObservers()
71+
public void CoalitionMemberAdminsCanUpdateStatusForFormSubmissionButNotForOtherMembersObservers()
7272
{
7373
// Arrange
7474
var scenarioData = ScenarioBuilder.New(CreateClient)
@@ -95,8 +95,7 @@ public void CoalitionMemberAdminsCanUpdateStatusForFormSubmissionButNotForOhterM
9595
var electionRoundId = scenarioData.ElectionRoundId;
9696
var aliceSubmissionId = scenarioData.ElectionRound.Coalition.GetSubmissionId("Common", ScenarioObserver.Alice, ScenarioPollingStation.Cluj);
9797
var bobSubmissionId = scenarioData.ElectionRound.Coalition.GetSubmissionId("Common", ScenarioObserver.Bob, ScenarioPollingStation.Iasi);
98-
99-
98+
10099
// Act
101100
scenarioData.NgoByName(ScenarioNgos.Beta).Admin
102101
.PutWithoutResponse(

web/src/features/election-rounds/components/Dashboard/columns-defs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ export const electionRoundColDefs: ColumnDef<ElectionRoundModel>[] = [
5252
}) => <ElectionRoundStatusBadge status={status} />,
5353
}),
5454
columnHelper.display({
55-
id: 'numberOfNgosMonitoring',
55+
id: 'monitoringNgos',
5656
enableSorting: true,
5757
header: ({ column }) => <DataTableColumnHeader title='NGOs' column={column} />,
58-
cell: ({ row }) => row.original.numberOfNgosMonitoring,
58+
cell: ({ row }) => row.original.monitoringNgos.length,
5959
}),
6060
{
6161
id: 'actions',

web/src/features/election-rounds/models/types.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { ElectionRoundStatus } from '@/common/types';
2+
import { NGOStatus } from '@/features/ngos/models/NGO';
3+
4+
export interface MonitoringNgoModel {
5+
id: string;
6+
name: string;
7+
status: NGOStatus;
8+
}
29

310
export interface ElectionRoundModel {
411
id: string;
@@ -10,5 +17,5 @@ export interface ElectionRoundModel {
1017
status: ElectionRoundStatus;
1118
createdOn: string;
1219
lastModifiedOn: string;
13-
numberOfNgosMonitoring: number;
20+
monitoringNgos: MonitoringNgoModel[];
1421
}

0 commit comments

Comments
 (0)