Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace Feature.CitizenReports.GetSubmissionsAggregated;

[Obsolete("Use :aggregated-submissionsV2 instead.")]
public class Endpoint(
VoteMonitorContext context,
IAuthorizationService authorizationService,
Expand All @@ -21,6 +22,7 @@ public override void Configure()
{
s.Summary =
"Gets aggregated citizen report form submissions aggregated with all the notes and attachments";
s.Description = "Obsolete: use :aggregated-submissionsV2 instead.";
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ await authorizationService.AuthorizeAsync(User,
}

var filter = new FilterSqlCompiler(V2ReportFilterFields.CitizenReports, parameterPrefixer: FilterSqlCompiler.FunctionParameter)
.Build(req.Filter);
.Build(req.ToFilterRule());

const string sql = """
SELECT * FROM "GetCitizenReportsAggregate"(@electionRoundId, @ngoId, @formId, @filter, @filterParameters::jsonb)
""";

CitizenReportFormSubmissionsAggregate? aggregate;
CitizenReportFormSubmissionsAggregateV2? aggregate;
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
{
aggregate = await dbConnection.QuerySingleOrDefaultAsync<CitizenReportFormSubmissionsAggregate>(new CommandDefinition(sql, new
aggregate = await dbConnection.QuerySingleOrDefaultAsync<CitizenReportFormSubmissionsAggregateV2>(new CommandDefinition(sql, new
{
electionRoundId = req.ElectionRoundId,
ngoId = req.NgoId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Vote.Monitor.Core.RulesEngine;
using Vote.Monitor.Core.RulesEngine.Rules;
using Vote.Monitor.Core.Security;
using Vote.Monitor.Domain.Entities.CitizenReportAggregate;

namespace Feature.CitizenReports.GetSubmissionsAggregatedV2;

Expand All @@ -10,4 +13,33 @@ public class Request : BaseFilterConditionsRequest
public Guid NgoId { get; set; }

public Guid FormId { get; set; }

[QueryParam] public string? Level1Filter { get; set; }
[QueryParam] public string? Level2Filter { get; set; }
[QueryParam] public string? Level3Filter { get; set; }
[QueryParam] public string? Level4Filter { get; set; }
[QueryParam] public string? Level5Filter { get; set; }
[QueryParam] public Guid? LocationId { get; set; }
[QueryParam] public bool? HasFlaggedAnswers { get; set; }
[QueryParam] public CitizenReportFollowUpStatus? FollowUpStatus { get; set; }
[QueryParam] public bool? HasNotes { get; set; }
[QueryParam] public bool? HasAttachments { get; set; }
[QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; }

/// <summary>
/// <see cref="BaseFilterConditionsRequest.Filter"/> combined with the query parameters.
/// </summary>
public FilterRule? ToFilterRule() => FilterRules.And(
Filter,
FilterRules.Eq("level1", Level1Filter),
FilterRules.Eq("level2", Level2Filter),
FilterRules.Eq("level3", Level3Filter),
FilterRules.Eq("level4", Level4Filter),
FilterRules.Eq("level5", Level5Filter),
FilterRules.Eq("locationId", LocationId),
FilterRules.Eq("hasFlaggedAnswers", HasFlaggedAnswers),
FilterRules.Eq("followUpStatus", FollowUpStatus?.Name),
FilterRules.Eq("hasNotes", HasNotes),
FilterRules.Eq("hasAttachments", HasAttachments),
FilterRules.Eq("questionsAnswered", QuestionsAnswered?.Name));
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Feature.CitizenReports.GetSubmissionsAggregatedV2;

public class Response
{
public CitizenReportFormSubmissionsAggregate SubmissionsAggregate { get; set; } = null!;
public CitizenReportFormSubmissionsAggregateV2 SubmissionsAggregate { get; set; } = null!;
}
4 changes: 2 additions & 2 deletions api/src/Feature.Form.Submissions/GetAggregatedV2/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
SELECT * FROM "GetFormSubmissionsAggregate"(@electionRoundId, @ngoId, @dataSource, @formId, @filter, @filterParameters::jsonb)
""";

FormSubmissionsAggregate? aggregate;
FormSubmissionsAggregateV2? aggregate;
using (var dbConnection = await connectionFactory.GetOpenConnectionAsync(ct))
{
aggregate = await dbConnection.QuerySingleOrDefaultAsync<FormSubmissionsAggregate>(new CommandDefinition(sql, new
aggregate = await dbConnection.QuerySingleOrDefaultAsync<FormSubmissionsAggregateV2>(new CommandDefinition(sql, new
{
electionRoundId = req.ElectionRoundId,
ngoId = req.NgoId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Feature.Form.Submissions.GetAggregatedV2;

public class Response
{
public FormSubmissionsAggregate SubmissionsAggregate { get; set; } = null!;
public FormSubmissionsAggregateV2 SubmissionsAggregate { get; set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Feature.IncidentReports.GetSubmissionsAggregated;

[Obsolete("Use :aggregated-submissionsV2 instead.")]
public class Endpoint(
IAuthorizationService authorizationService,
VoteMonitorContext context,
Expand All @@ -19,6 +20,7 @@ public override void Configure()
Summary(s =>
{
s.Summary = "Gets aggregated incident report form submissions with all the notes and attachments";
s.Description = "Obsolete: use :aggregated-submissionsV2 instead.";
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
}

var filter = new FilterSqlCompiler(V2ReportFilterFields.IncidentReports, parameterPrefixer: FilterSqlCompiler.FunctionParameter)
.Build(req.Filter);
.Build(req.ToFilterRule());

const string sql = """
SELECT * FROM "GetIncidentReportsAggregate"(@electionRoundId, @ngoId, @dataSource, @formId, @filter, @filterParameters::jsonb)
""";

FormSubmissionsAggregate? aggregate;
FormSubmissionsAggregateV2? aggregate;
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
{
aggregate = await dbConnection.QuerySingleOrDefaultAsync<FormSubmissionsAggregate>(new CommandDefinition(sql, new
aggregate = await dbConnection.QuerySingleOrDefaultAsync<FormSubmissionsAggregateV2>(new CommandDefinition(sql, new
{
electionRoundId = req.ElectionRoundId,
ngoId = req.NgoId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Vote.Monitor.Core.RulesEngine;
using Vote.Monitor.Core.RulesEngine.Rules;
using Vote.Monitor.Core.Security;
using Vote.Monitor.Domain.Entities.IncidentReportAggregate;

namespace Feature.IncidentReports.GetSubmissionsAggregatedV2;

Expand All @@ -10,4 +13,39 @@ public class Request : BaseFilterConditionsRequest
public Guid NgoId { get; set; }

public Guid FormId { get; set; }

[QueryParam] public string? Level1Filter { get; set; }
[QueryParam] public string? Level2Filter { get; set; }
[QueryParam] public string? Level3Filter { get; set; }
[QueryParam] public string? Level4Filter { get; set; }
[QueryParam] public string? Level5Filter { get; set; }
[QueryParam] public string? PollingStationNumberFilter { get; set; }
[QueryParam] public Guid? PollingStationId { get; set; }
[QueryParam] public bool? HasFlaggedAnswers { get; set; }
[QueryParam] public IncidentReportFollowUpStatus? FollowUpStatusFilter { get; set; }
[QueryParam] public IncidentReportLocationType? LocationTypeFilter { get; set; }
[QueryParam] public bool? HasNotes { get; set; }
[QueryParam] public bool? HasAttachments { get; set; }
[QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; }
[QueryParam] public bool? IsCompletedFilter { get; set; }

/// <summary>
/// <see cref="BaseFilterConditionsRequest.Filter"/> combined with the query parameters.
/// </summary>
public FilterRule? ToFilterRule() => FilterRules.And(
Filter,
FilterRules.Eq("level1", Level1Filter),
FilterRules.Eq("level2", Level2Filter),
FilterRules.Eq("level3", Level3Filter),
FilterRules.Eq("level4", Level4Filter),
FilterRules.Eq("level5", Level5Filter),
FilterRules.Eq("pollingStationNumber", PollingStationNumberFilter),
FilterRules.Eq("pollingStationId", PollingStationId),
FilterRules.Eq("hasFlaggedAnswers", HasFlaggedAnswers),
FilterRules.Eq("followUpStatus", FollowUpStatusFilter?.Name),
FilterRules.Eq("locationType", LocationTypeFilter?.Name),
FilterRules.Eq("hasNotes", HasNotes),
FilterRules.Eq("hasAttachments", HasAttachments),
FilterRules.Eq("questionsAnswered", QuestionsAnswered?.Name),
FilterRules.Eq("isCompleted", IsCompletedFilter));
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace Feature.IncidentReports.GetSubmissionsAggregatedV2;

public class Response
{
public FormSubmissionsAggregate SubmissionsAggregate { get; set; } = null!;
public FormSubmissionsAggregateV2 SubmissionsAggregate { get; set; } = null!;
}
35 changes: 35 additions & 0 deletions api/src/Module.Answers/Aggregators/AggregateLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Module.Answers.Aggregators;

/// <summary>
/// A Level1..Level5 path of the aggregated submissions, computed by "AggregateAnswersByLocation".
/// The question aggregates hold their breakdown per location in <see cref="BaseAnswerAggregate.LocationAggregates"/>.
/// </summary>
public class AggregateLocation
{
public int Id { get; init; }

/// <summary>
/// 1..5: the deepest level of the path.
/// </summary>
public int Level { get; init; }

/// <summary>
/// The location one level up with the same path; not set for Level1 locations.
/// </summary>
public int? ParentId { get; init; }

public string? Level1 { get; init; }
public string? Level2 { get; init; }
public string? Level3 { get; init; }
public string? Level4 { get; init; }
public string? Level5 { get; init; }

public int SubmissionCount { get; init; }
public int TotalNumberOfQuestionsAnswered { get; init; }
public int TotalNumberOfFlaggedAnswers { get; init; }

/// <summary>
/// Number of distinct polling stations of the location's submissions. Not set for citizen reports.
/// </summary>
public int? PollingStationsCount { get; init; }
}
29 changes: 28 additions & 1 deletion api/src/Module.Answers/Aggregators/BaseAnswerAggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,36 @@ public abstract class BaseAnswerAggregate

[JsonInclude] public Guid QuestionId { get; private set; }
public int DisplayOrder { get; set; }
[JsonInclude] public BaseQuestion Question { get; private set; } = null!;
/// <summary>
/// Not set on the aggregates of a location breakdown (see <see cref="LocationAggregates"/>).
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public BaseQuestion Question { get; private set; } = null!;
[JsonInclude] public int AnswersAggregated { get; private set; }

/// <summary>
/// Ids of the question's notes in the aggregate's Notes. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<int>? NoteIds { get; private set; }

/// <summary>
/// Ids of the question's attachments in the aggregate's Attachments. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<int>? AttachmentIds { get; private set; }

/// <summary>
/// The question's aggregate per location id (see <see cref="AggregateLocation"/>), without the question itself and
/// referencing only the texts, notes and attachments of the location's submissions. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<int, BaseAnswerAggregate>? LocationAggregates { get; private set; }

/// <summary>
/// Used when deserializing an aggregate computed by the database.
/// </summary>
Expand Down
32 changes: 29 additions & 3 deletions api/src/Module.Answers/Aggregators/MultiSelectAnswerAggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,39 @@ public IReadOnlyDictionary<Guid, int> AnswersHistogram
get => _answersHistogram.AsReadOnly();
private set => _answersHistogram = new Dictionary<Guid, int>(value);
}

/// <summary>
/// Number of distinct submissions that picked each option. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<Guid, int>? SubmissionsHistogram { get; private set; }

/// <summary>
/// Number of distinct polling stations where each option was picked. Only set when computed by the database,
/// and not for citizen reports.
/// </summary>
[JsonInclude]
public IReadOnlyCollection<TextResponse> FreeTexts
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<Guid, int>? PollingStationsHistogram { get; private set; }
/// <summary>
/// Not set when the free texts are referenced by id (<see cref="FreeTextIds"/>).
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyCollection<TextResponse>? FreeTexts
{
get => _freeTexts.ToList().AsReadOnly();
private set => _freeTexts = value.ToList();
get => FreeTextIds is null ? _freeTexts.ToList().AsReadOnly() : null;
private set => _freeTexts = value?.ToList() ?? [];
}

/// <summary>
/// Ids of the texts in the aggregate's Texts. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<int>? FreeTextIds { get; private set; }

[JsonConstructor]
private MultiSelectAnswerAggregate()
{
Expand Down
15 changes: 15 additions & 0 deletions api/src/Module.Answers/Aggregators/RatingAnswerAggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ public IReadOnlyDictionary<int, int> AnswersHistogram
private set => _answersHistogram = new Dictionary<int, int>(value);
}

/// <summary>
/// Number of distinct submissions that picked each rating value. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<int, int>? SubmissionsHistogram { get; private set; }

/// <summary>
/// Number of distinct polling stations where each rating value was picked. Only set when computed by the database,
/// and not for citizen reports.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<int, int>? PollingStationsHistogram { get; private set; }

[JsonInclude] public int Min { get; private set; } = int.MaxValue;
[JsonInclude] public int Max { get; private set; } = int.MinValue;
[JsonInclude] public decimal Average { get; private set; }
Expand Down
32 changes: 29 additions & 3 deletions api/src/Module.Answers/Aggregators/SingleSelectAnswerAggregate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,39 @@ public IReadOnlyDictionary<Guid, int> AnswersHistogram
get => _answersHistogram.AsReadOnly();
private set => _answersHistogram = new Dictionary<Guid, int>(value);
}

/// <summary>
/// Number of distinct submissions that picked each option. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<Guid, int>? SubmissionsHistogram { get; private set; }

/// <summary>
/// Number of distinct polling stations where each option was picked. Only set when computed by the database,
/// and not for citizen reports.
/// </summary>
[JsonInclude]
public IReadOnlyList<TextResponse> FreeTexts
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<Guid, int>? PollingStationsHistogram { get; private set; }
/// <summary>
/// Not set when the free texts are referenced by id (<see cref="FreeTextIds"/>).
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<TextResponse>? FreeTexts
{
get => _freeTexts.ToList().AsReadOnly();
private set => _freeTexts = value.ToList();
get => FreeTextIds is null ? _freeTexts.ToList().AsReadOnly() : null;
private set => _freeTexts = value?.ToList() ?? [];
}

/// <summary>
/// Ids of the texts in the aggregate's Texts. Only set when computed by the database.
/// </summary>
[JsonInclude]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<int>? FreeTextIds { get; private set; }

[JsonConstructor]
private SingleSelectAnswerAggregate()
{
Expand Down
Loading
Loading