Skip to content

Commit b07881d

Browse files
authored
Improve form submissions endpoint (#989)
* Improve form submissions endpoint * fix failing tests
1 parent 9cee2fa commit b07881d

File tree

11 files changed

+254
-3
lines changed

11 files changed

+254
-3
lines changed

api/src/Feature.Form.Submissions/GetById/Endpoint.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ WITH submissions AS
4545
(SELECT "Languages"
4646
FROM "PollingStationInformationForms"
4747
WHERE "ElectionRoundId" = @electionRoundId) AS "Languages",
48+
(SELECT "Id"
49+
FROM "PollingStationInformationForms"
50+
WHERE "ElectionRoundId" = @electionRoundId) AS "FormId",
4851
psi."FollowUpStatus" as "FollowUpStatus",
4952
'[]'::jsonb AS "Attachments",
5053
'[]'::jsonb AS "Notes",
@@ -67,6 +70,7 @@ UNION ALL
6770
f."Questions",
6871
f."DefaultLanguage",
6972
f."Languages",
73+
f."Id" AS "FormId",
7074
fs."FollowUpStatus",
7175
COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt"))
7276
FROM "Attachments" a
@@ -95,6 +99,7 @@ UNION ALL
9599
INNER JOIN "Forms" f ON f."Id" = fs."FormId"
96100
WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId)
97101
SELECT s."SubmissionId",
102+
s."FormId",
98103
s."TimeSubmitted",
99104
s."FormCode",
100105
s."FormType",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using Module.Answers.Models;
2+
using Vote.Monitor.Core.Services.FileStorage.Contracts;
3+
4+
namespace Feature.Form.Submissions.GetByIdV2;
5+
6+
public class Endpoint(
7+
IAuthorizationService authorizationService,
8+
INpgsqlConnectionFactory dbConnectionFactory,
9+
IFileStorageService fileStorageService) : Endpoint<Request, Results<Ok<FormSubmissionView>, NotFound>>
10+
{
11+
public override void Configure()
12+
{
13+
Get("/api/election-rounds/{electionRoundId}/form-submissions/{submissionId}:v2");
14+
DontAutoTag();
15+
Options(x => x.WithTags("form-submissions"));
16+
Summary(s => { s.Summary = "Gets submission by id"; });
17+
18+
Policies(PolicyNames.NgoAdminsOnly);
19+
}
20+
21+
public override async Task<Results<Ok<FormSubmissionView>, NotFound>> ExecuteAsync(Request req,
22+
CancellationToken ct)
23+
{
24+
var authorizationResult =
25+
await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId));
26+
if (!authorizationResult.Succeeded)
27+
{
28+
return TypedResults.NotFound();
29+
}
30+
31+
var sql = """
32+
WITH submissions AS
33+
(SELECT psi."Id" AS "SubmissionId",
34+
'PSI' AS "FormType",
35+
'PSI' AS "FormCode",
36+
psi."PollingStationId",
37+
psi."MonitoringObserverId",
38+
psi."Answers",
39+
(SELECT "Id"
40+
FROM "PollingStationInformationForms"
41+
WHERE "ElectionRoundId" = @electionRoundId) AS "FormId",
42+
psi."FollowUpStatus" as "FollowUpStatus",
43+
'[]'::jsonb AS "Attachments",
44+
'[]'::jsonb AS "Notes",
45+
"LastUpdatedAt" AS "TimeSubmitted",
46+
psi."ArrivalTime",
47+
psi."DepartureTime",
48+
psi."Breaks",
49+
psi."IsCompleted"
50+
FROM "PollingStationInformation" psi
51+
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = psi."MonitoringObserverId"
52+
WHERE psi."Id" = @submissionId and psi."ElectionRoundId" = @electionRoundId
53+
UNION ALL
54+
SELECT
55+
fs."Id" AS "SubmissionId",
56+
f."FormType" AS "FormType",
57+
f."Code" AS "FormCode",
58+
fs."PollingStationId",
59+
fs."MonitoringObserverId",
60+
fs."Answers",
61+
f."Id" AS "FormId",
62+
fs."FollowUpStatus",
63+
COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt"))
64+
FROM "Attachments" a
65+
WHERE
66+
a."ElectionRoundId" = @electionRoundId
67+
AND a."FormId" = fs."FormId"
68+
AND a."MonitoringObserverId" = fs."MonitoringObserverId"
69+
AND a."IsDeleted" = false AND a."IsCompleted" = true
70+
AND fs."PollingStationId" = a."PollingStationId"),'[]'::JSONB) AS "Attachments",
71+
72+
COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'Text', "Text", 'TimeSubmitted', "LastUpdatedAt"))
73+
FROM "Notes" n
74+
WHERE
75+
n."ElectionRoundId" = @electionRoundId
76+
AND n."FormId" = fs."FormId"
77+
AND n."MonitoringObserverId" = fs."MonitoringObserverId"
78+
AND fs."PollingStationId" = n."PollingStationId"), '[]'::JSONB) AS "Notes",
79+
80+
"LastUpdatedAt" AS "TimeSubmitted",
81+
NULL AS "ArrivalTime",
82+
NULL AS "DepartureTime",
83+
'[]'::jsonb AS "Breaks",
84+
fs."IsCompleted"
85+
FROM "FormSubmissions" fs
86+
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = FS."MonitoringObserverId"
87+
INNER JOIN "Forms" f ON f."Id" = fs."FormId"
88+
WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId)
89+
SELECT s."SubmissionId",
90+
s."FormId",
91+
s."TimeSubmitted",
92+
s."FormCode",
93+
s."FormType",
94+
ps."Id" AS "PollingStationId",
95+
ps."Level1",
96+
ps."Level2",
97+
ps."Level3",
98+
ps."Level4",
99+
ps."Level5",
100+
ps."Number",
101+
s."MonitoringObserverId",
102+
AMO."DisplayName" "ObserverName",
103+
AMO."Email",
104+
AMO."PhoneNumber",
105+
AMO."Tags",
106+
AMO."NgoName",
107+
AMO."IsOwnObserver",
108+
s."Attachments",
109+
s."Notes",
110+
s."Answers",
111+
s."FollowUpStatus",
112+
s."ArrivalTime",
113+
s."DepartureTime",
114+
s."Breaks",
115+
s."IsCompleted"
116+
FROM submissions s
117+
INNER JOIN "PollingStations" ps ON ps."Id" = s."PollingStationId"
118+
INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = s."MonitoringObserverId"
119+
""";
120+
121+
var queryArgs = new
122+
{
123+
electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, submissionId = req.SubmissionId
124+
};
125+
126+
FormSubmissionView submission = null;
127+
128+
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
129+
{
130+
submission = await dbConnection.QueryFirstOrDefaultAsync<FormSubmissionView>(sql, queryArgs);
131+
}
132+
133+
if (submission is null)
134+
{
135+
return TypedResults.NotFound();
136+
}
137+
138+
submission = submission with
139+
{
140+
Attachments = await Task.WhenAll(
141+
submission.Attachments.Select(async attachment =>
142+
{
143+
var result =
144+
await fileStorageService.GetPresignedUrlAsync(attachment.FilePath, attachment.UploadedFileName);
145+
return result is GetPresignedUrlResult.Ok(var url, _, var urlValidityInSeconds)
146+
? attachment with { PresignedUrl = url, UrlValidityInSeconds = urlValidityInSeconds }
147+
: attachment;
148+
})
149+
)
150+
};
151+
152+
return TypedResults.Ok(submission);
153+
}
154+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Vote.Monitor.Core.Security;
2+
3+
namespace Feature.Form.Submissions.GetByIdV2;
4+
5+
public class Request
6+
{
7+
public Guid ElectionRoundId { get; set; }
8+
9+
[FromClaim(ApplicationClaimTypes.NgoId)]
10+
public Guid NgoId { get; set; }
11+
public Guid SubmissionId { get; set; }
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Feature.Form.Submissions.GetByIdV2;
2+
3+
public class Validator : Validator<Request>
4+
{
5+
public Validator()
6+
{
7+
RuleFor(x => x.ElectionRoundId).NotEmpty();
8+
RuleFor(x => x.NgoId).NotEmpty();
9+
RuleFor(x => x.SubmissionId).NotEmpty();
10+
}
11+
}

api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ OR mo."PhoneNumber" ILIKE @searchText
125125
WITH polling_station_submissions AS (SELECT psi."Id" AS "SubmissionId",
126126
'PSI' AS "FormType",
127127
'PSI' AS "FormCode",
128+
psif."Id" "FormId",
128129
psi."PollingStationId",
129130
psi."MonitoringObserverId",
130131
psi."NumberOfQuestionsAnswered",
@@ -166,6 +167,7 @@ OR mo."PhoneNumber" ILIKE @searchText
166167
form_submissions AS (SELECT fs."Id" AS "SubmissionId",
167168
f."FormType",
168169
f."Code" AS "FormCode",
170+
f."Id" AS "FormId",
169171
fs."PollingStationId",
170172
fs."MonitoringObserverId",
171173
fs."NumberOfQuestionsAnswered",
@@ -212,6 +214,7 @@ OR mo."PhoneNumber" ILIKE @searchText
212214
OR (@questionsAnswered = 'None' AND fs."NumberOfQuestionsAnswered" = 0)))
213215
SELECT s."SubmissionId",
214216
s."TimeSubmitted",
217+
s."FormId",
215218
s."FormCode",
216219
s."FormType",
217220
s."DefaultLanguage",

api/src/Feature.Form.Submissions/ListEntries/FormSubmissionEntry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public record FormSubmissionEntry
1111
public Guid SubmissionId { get; init; }
1212
public DateTime TimeSubmitted { get; init; }
1313

14+
public Guid FormId { get; init; }
1415
public string FormCode { get; init; } = null!;
1516
public TranslatedString FormName { get; init; } = null!;
1617

api/src/Feature.Forms/Get/Endpoint.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
using Feature.Forms.Specifications;
44
using Microsoft.AspNetCore.Authorization;
55
using Vote.Monitor.Domain.Entities.CoalitionAggregate;
6+
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;
67
using GetCoalitionFormSpecification = Feature.Forms.Specifications.GetCoalitionFormSpecification;
78

89
namespace Feature.Forms.Get;
910

1011
public class Endpoint(
1112
IAuthorizationService authorizationService,
1213
IReadRepository<FormAggregate> formRepository,
13-
IReadRepository<Coalition> coalitionRepository) : Endpoint<Request, Results<Ok<FormFullModel>, NotFound>>
14+
IReadRepository<Coalition> coalitionRepository,
15+
IReadRepository<PollingStationInformationForm> psiFormRepository) : Endpoint<Request, Results<Ok<FormFullModel>, NotFound>>
1416
{
1517
public override void Configure()
1618
{
@@ -27,12 +29,20 @@ public override async Task<Results<Ok<FormFullModel>, NotFound>> ExecuteAsync(Re
2729
{
2830
return TypedResults.NotFound();
2931
}
30-
32+
var psiFormSpecification =
33+
new GetPsiFormById(req.ElectionRoundId, req.Id);
3134
var coalitionFormSpecification =
3235
new GetCoalitionFormSpecification(req.ElectionRoundId, req.NgoId, req.Id);
3336
var ngoFormSpecification =
3437
new GetFormByIdSpecification(req.ElectionRoundId, req.NgoId, req.Id);
3538

39+
var psiForm = await psiFormRepository.FirstOrDefaultAsync(psiFormSpecification, ct);
40+
41+
if (psiForm is not null)
42+
{
43+
return TypedResults.Ok(FormFullModel.FromEntity(psiForm));
44+
}
45+
3646
var form = (await coalitionRepository.FirstOrDefaultAsync(coalitionFormSpecification, ct)) ??
3747
(await formRepository.FirstOrDefaultAsync(ngoFormSpecification, ct));
3848

api/src/Feature.Forms/Models/FormFullModel.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Vote.Monitor.Domain.Entities.FormBase;
44
using Module.Forms.Mappers;
55
using Module.Forms.Models;
6+
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;
67

78
namespace Feature.Forms.Models;
89

@@ -48,4 +49,22 @@ public static FormFullModel FromEntity(FormAggregate form) => form == null
4849
Icon = form.Icon,
4950
DisplayOrder = form.DisplayOrder
5051
};
52+
53+
public static FormFullModel FromEntity(PollingStationInformationForm form) => form == null
54+
? null
55+
: new FormFullModel
56+
{
57+
Id = form.Id,
58+
Code = form.Code,
59+
FormType = form.FormType,
60+
Status = form.Status,
61+
DefaultLanguage = form.DefaultLanguage,
62+
Languages = form.Languages,
63+
Name = form.Name,
64+
Questions = form.Questions.Select(QuestionsMapper.ToModel).ToList(),
65+
NumberOfQuestions = form.NumberOfQuestions,
66+
Description = form.Description,
67+
LanguagesTranslationStatus = form.LanguagesTranslationStatus,
68+
Icon = form.Icon,
69+
};
5170
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate;
2+
3+
namespace Feature.Forms.Specifications;
4+
5+
public sealed class GetPsiFormById : SingleResultSpecification<PollingStationInformationForm>
6+
{
7+
public GetPsiFormById(Guid electionRoundId, Guid id)
8+
{
9+
Query.Where(x => x.ElectionRoundId == electionRoundId && x.Id == id);
10+
}
11+
}

api/src/Module.Answers/Models/FormSubmissionView.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public record FormSubmissionView
99
{
1010
public Guid SubmissionId { get; init; }
1111
public DateTime TimeSubmitted { get; init; }
12+
public Guid FormId { get; init; }
1213
public string FormCode { get; init; }
1314
public string DefaultLanguage { get; init; }
1415
public string[] Languages { get; init; } = [];

0 commit comments

Comments
 (0)