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
2 changes: 2 additions & 0 deletions api/src/Feature.PollingStation.Visits/ListMy/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
PS."Level5",
PS."Address",
PS."Number",
PS."Latitude",
PS."Longitude",
T."MonitoringObserverId",
MAX(T."LatestTimestamp") "VisitedAt"
FROM
Expand Down
2 changes: 2 additions & 0 deletions api/src/Feature.PollingStation.Visits/VisitModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public record VisitModel
public string Level5 { get; set; }
public string Address { get; set; }
public string Number { get; set; }
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
}
38 changes: 34 additions & 4 deletions api/src/Feature.PollingStations/Create/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Vote.Monitor.Core.Helpers;
using Vote.Monitor.Core.Helpers;
using Vote.Monitor.Core.Services.Security;
using Vote.Monitor.Core.Services.Time;

Expand All @@ -7,6 +7,7 @@
public class Endpoint(
VoteMonitorContext context,
IRepository<ElectionRoundAggregate> electionRoundRepository,
IRepository<PollingStationAggregate> pollingStationRepository,
ITimeProvider timeProvider,
ICurrentUserProvider userProvider)
: Endpoint<Request, Results<Ok<Response>, NotFound<ProblemDetails>>>
Expand All @@ -29,9 +30,36 @@
return TypedResults.NotFound(new ProblemDetails(ValidationFailures));
}

var userId = userProvider.GetUserId()!.Value;
var pollingStationIds = req.PollingStations
.Where(ps => ps.Id.HasValue)
.Select(ps => ps.Id!.Value)
.ToList();

var groupedPollingStationIds = pollingStationIds.GroupBy(id => id, y => y,
(id, duplicates) => new { id, numberOfDuplicates = duplicates.Count() });

foreach (var groupedPollingStationId in groupedPollingStationIds)
{
if (groupedPollingStationId.numberOfDuplicates > 1)
{
AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
}
}
Comment on lines +41 to +47

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI about 18 hours ago

In general, to fix this kind of issue you replace a foreach loop that uses an if guard (or continue) to filter items with a foreach over a filtered sequence using .Where(...). This makes the filtering explicit at the sequence level instead of inside the loop body.

For this specific code in api/src/Feature.PollingStations/Create/Endpoint.cs, the best fix is to add a .Where clause to groupedPollingStationIds so that the loop iterates only over those groups where numberOfDuplicates > 1. Concretely, change:

foreach (var groupedPollingStationId in groupedPollingStationIds)
{
    if (groupedPollingStationId.numberOfDuplicates > 1)
    {
        AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
    }
}

to:

foreach (var groupedPollingStationId in groupedPollingStationIds.Where(g => g.numberOfDuplicates > 1))
{
    AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
}

This removes the inner if and expresses the same condition via Where. No additional imports are necessary because LINQ extension methods like Where are already being used earlier in the file (req.PollingStations.Where(...)), implying the relevant using System.Linq; is present somewhere in the compilation unit. Only the loop region (lines 41–47) needs to be updated.

Suggested changeset 1
api/src/Feature.PollingStations/Create/Endpoint.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/Feature.PollingStations/Create/Endpoint.cs b/api/src/Feature.PollingStations/Create/Endpoint.cs
--- a/api/src/Feature.PollingStations/Create/Endpoint.cs
+++ b/api/src/Feature.PollingStations/Create/Endpoint.cs
@@ -38,12 +38,9 @@
         var groupedPollingStationIds = pollingStationIds.GroupBy(id => id, y => y,
             (id, duplicates) => new { id, numberOfDuplicates = duplicates.Count() });
 
-        foreach (var groupedPollingStationId in groupedPollingStationIds)
+        foreach (var groupedPollingStationId in groupedPollingStationIds.Where(g => g.numberOfDuplicates > 1))
         {
-            if (groupedPollingStationId.numberOfDuplicates > 1)
-            {
-                AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
-            }
+            AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
         }
 
         ThrowIfAnyErrors();
EOF
@@ -38,12 +38,9 @@
var groupedPollingStationIds = pollingStationIds.GroupBy(id => id, y => y,
(id, duplicates) => new { id, numberOfDuplicates = duplicates.Count() });

foreach (var groupedPollingStationId in groupedPollingStationIds)
foreach (var groupedPollingStationId in groupedPollingStationIds.Where(g => g.numberOfDuplicates > 1))
{
if (groupedPollingStationId.numberOfDuplicates > 1)
{
AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
}
AddError(new ValidationFailure(groupedPollingStationId.id.ToString(), "This id is duplicated"));
}

ThrowIfAnyErrors();
Copilot is powered by AI and may make mistakes. Always verify output.

ThrowIfAnyErrors();

var pollingStations = req.PollingStations.Select(ps => PollingStationAggregate.Create(electionRound,
var pollingStationsWithIdsFromAnotherElections = await pollingStationRepository.ListAsync(
new GetPollingStationsByIdsInOtherElectionRoundsSpecification(electionRound.Id, pollingStationIds), ct);

foreach (var ps in pollingStationsWithIdsFromAnotherElections)
{
AddError(new ValidationFailure(ps.Id.ToString(), "This id is for a different PS in another election"));
}

ThrowIfAnyErrors();

var userId = userProvider.GetUserId()!.Value;
var pollingStations = req.PollingStations.Select(ps => PollingStationAggregate.Create(ps.Id, electionRound,
ps.Level1,
ps.Level2,
ps.Level3,
Expand All @@ -41,11 +69,13 @@
ps.Address,
ps.DisplayOrder,
ps.Tags.ToTagsObject(),
ps.Latitude,
ps.Longitude,
timeProvider.UtcNow,
userId))
.ToArray();

await context.BulkInsertAsync(pollingStations, cancellationToken: ct);
await context.BulkInsertOrUpdateAsync(pollingStations, cancellationToken: ct);

electionRound.UpdatePollingStationsVersion();

Expand Down
3 changes: 3 additions & 0 deletions api/src/Feature.PollingStations/Create/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class Request

public class PollingStationRequest
{
public Guid? Id { get; set; }
public string Level1 { get; set; }
public string Level2 { get; set; }
public string Level3 { get; set; }
Expand All @@ -17,5 +18,7 @@ public class PollingStationRequest
public int DisplayOrder { get; set; }
public string Address { get; set; }
public Dictionary<string, string> Tags { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
}
5 changes: 3 additions & 2 deletions api/src/Feature.PollingStations/FetchAll/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public override async Task<Results<Ok<Response>, NotFound>> ExecuteAsync(Request
{
var pollingStations = await context.PollingStations
.Where(x => x.ElectionRoundId == request.ElectionRoundId)
.OrderBy(x => x.DisplayOrder)
.Select(x => new PollingStationModel
{
Id = x.Id,
Expand Down Expand Up @@ -134,7 +133,9 @@ private static List<LocationNode> GetLocationNodes(List<PollingStationModel> pol
Name = ps.Address,
ParentId = parentNode!.Id,
Number = ps.Number,
PollingStationId = ps.Id
PollingStationId = ps.Id,
Latitude = ps.Latitude,
Longitude = ps.Longitude
});
}

Expand Down
6 changes: 6 additions & 0 deletions api/src/Feature.PollingStations/FetchAll/LocationNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ public class LocationNode

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? DisplayOrder { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Latitude { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Longitude { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public override void Configure()
public override async Task HandleAsync(CancellationToken ct)
{
const string template = """
"Level1", "Level2", "Level3", "Level4", "Level5", "Number", "Address", "DisplayOrder"
"Level1", "Level2", "Level3", "Level4", "Level5", "Number", "Address", "DisplayOrder","Latitude", "Longitude", "Tag1", "Tag2", "Tag3"
""";

var stream = GenerateStreamFromString(template);
Expand Down
2 changes: 2 additions & 0 deletions api/src/Feature.PollingStations/Import/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public override async Task<Results<Ok<Response>, NotFound<ProblemDetails>, Probl
x.Address,
x.DisplayOrder,
x.Tags.ToTagsObject(),
x.Latitude,
x.Longitude,
timeProvider.UtcNow,
userProvider.GetUserId()!.Value))
.ToList();
Expand Down
2 changes: 2 additions & 0 deletions api/src/Feature.PollingStations/List/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public override async Task<Results<Ok<PagedResponse<PollingStationModel>>, Probl
Number = x.Number,
Address = x.Address,
DisplayOrder = x.DisplayOrder,
Latitude = x.Latitude,
Longitude = x.Longitude,
Tags = x.Tags.ToDictionary()
}).ToList();

Expand Down
2 changes: 2 additions & 0 deletions api/src/Feature.PollingStations/PollingStationModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public class PollingStationModel
public string Address { get; set; }
public int DisplayOrder { get; set; }
public Dictionary<string, string> Tags { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public class PollingStationImportModel
public string Address { get; set; }

public List<TagImportModel> Tags { get; set; }
}
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ public PollingStationImportModelMapper()

Map(m => m.Address).Name("Address"); // 6
Map(m => m.DisplayOrder).Name("DisplayOrder"); //7
Map(m => m.Tags).Convert(ReadTags); // 8 -> end
Map(m => m.Latitude).Name("Latitude"); //8
Map(m => m.Longitude).Name("Longitude"); //9
Map(m => m.Tags).Convert(ReadTags); // 10 -> end
}

private static List<TagImportModel> ReadTags(ConvertFromStringArgs row)
{
var tags = new List<TagImportModel>();

for (var i = 8; i < row.Row?.HeaderRecord?.Length; i++)
for (var i = 10; i < row.Row?.HeaderRecord?.Length; i++)
{
var name = row.Row.HeaderRecord[i];
var value = row.Row[i];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Feature.PollingStations.Specifications;

public sealed class GetPollingStationsByIdsInOtherElectionRoundsSpecification : Specification<PollingStationAggregate>
{
public GetPollingStationsByIdsInOtherElectionRoundsSpecification(Guid electionRoundId, IEnumerable<Guid> pollingStationIds)
{
Query
.Where(x => x.ElectionRoundId != electionRoundId)
.Where(x => pollingStationIds.Contains(x.Id));
}
}
2 changes: 1 addition & 1 deletion api/src/Feature.PollingStations/Update/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public override async Task<Results<NoContent, NotFound<ProblemDetails>, Conflict
return TypedResults.NotFound(new ProblemDetails(ValidationFailures));
}

pollingStation.UpdateDetails(req.Level1, req.Level2, req.Level3, req.Level4, req.Level5, req.Number, req.Address, req.DisplayOrder, req.Tags.ToTagsObject());
pollingStation.UpdateDetails(req.Level1, req.Level2, req.Level3, req.Level4, req.Level5, req.Number, req.Address, req.DisplayOrder, req.Tags.ToTagsObject(), req.Latitude,req.Longitude);
await repository.UpdateAsync(pollingStation, ct);
electionRound.UpdatePollingStationsVersion();
await electionRoundRepository.UpdateAsync(electionRound, ct);
Expand Down
4 changes: 4 additions & 0 deletions api/src/Feature.PollingStations/Update/Request.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace Feature.PollingStations.Update;

public class Request
{
public Guid ElectionRoundId { get; set; }
Expand All @@ -12,4 +13,7 @@ public class Request
public int DisplayOrder { get; set; }
public string Address { get; set; }
public Dictionary<string, string> Tags { get; set; }

public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ private PollingStation()
{
}
#pragma warning restore CS8618

internal PollingStation(ElectionRound electionRound,

public static PollingStation Create(
Guid? id,
ElectionRound electionRound,
string level1,
string level2,
string level3,
Expand All @@ -17,11 +19,24 @@ internal PollingStation(ElectionRound electionRound,
string number,
string address,
int displayOrder,
JsonDocument tags) : this(Guid.NewGuid(), electionRound, level1, level2, level3, level4, level5, number, address, displayOrder, tags)
JsonDocument tags,
double? latitude,
double? longitude,
DateTime createdOn,
Guid userId)
{
}
var pollingStation = new PollingStation(id, electionRound, level1, level2, level3, level4, level5, number, address,
displayOrder,
tags, latitude, longitude);

public static PollingStation Create(ElectionRound electionRound,
pollingStation.CreatedOn = createdOn;
pollingStation.CreatedBy = userId;

return pollingStation;
}

public static PollingStation Create(
ElectionRound electionRound,
string level1,
string level2,
string level3,
Expand All @@ -31,24 +46,20 @@ public static PollingStation Create(ElectionRound electionRound,
string address,
int displayOrder,
JsonDocument tags,
double? latitude,
double? longitude,
DateTime createdOn,
Guid userId)
{
var pollingStation = new PollingStation(electionRound, level1, level2, level3, level4, level5, number, address, displayOrder,
tags);

pollingStation.CreatedOn = createdOn;
pollingStation.CreatedBy = userId;

return pollingStation;
return Create(null, electionRound, level1, level2, level3, level4, level5, number, address,displayOrder, tags, latitude, longitude, createdOn, userId);
}

public Guid Id { get; private set; }
public ElectionRound ElectionRound { get; private set; }
public Guid ElectionRoundId { get; private set; }

internal PollingStation(
Guid id,
Guid? id,
ElectionRound electionRound,
string level1,
string level2,
Expand All @@ -58,9 +69,11 @@ internal PollingStation(
string number,
string address,
int displayOrder,
JsonDocument tags)
JsonDocument tags,
double? latitude,
double? longitude)
{
Id = id;
Id = id ?? Guid.NewGuid();
ElectionRoundId = electionRound.Id;
ElectionRound = electionRound;
Level1 = level1;
Expand All @@ -72,6 +85,8 @@ internal PollingStation(
Address = address;
DisplayOrder = displayOrder;
Tags = tags;
Latitude = latitude;
Longitude = longitude;
}

public string Level1 { get; private set; }
Expand All @@ -83,6 +98,8 @@ internal PollingStation(

public string Address { get; private set; }
public int DisplayOrder { get; private set; }
public double? Latitude { get; private set; }
public double? Longitude { get; private set; }

public JsonDocument Tags { get; private set; }

Expand All @@ -94,7 +111,9 @@ public void UpdateDetails(string level1,
string number,
string address,
int displayOrder,
JsonDocument tags)
JsonDocument tags,
double? latitude,
double? longitude)
{
Level1 = level1;
Level2 = level2;
Expand All @@ -105,7 +124,10 @@ public void UpdateDetails(string level1,
Address = address;
DisplayOrder = displayOrder;
Tags = tags;
Latitude = latitude;
Longitude = longitude;
}

public void Dispose()
{
Tags.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ public void Configure(EntityTypeBuilder<PollingStation> builder)
builder.Property(p => p.Id).IsRequired();
builder.Property(p => p.Address).HasMaxLength(2024).IsRequired();
builder.Property(p => p.DisplayOrder).IsRequired();
builder.Property(p => p.Tags).IsRequired(false);
builder.Property(p => p.Tags).IsRequired(false).HasDefaultValueSql("'{}'::JSONB");
builder.Property(p => p.Level1).HasMaxLength(256).IsRequired();
builder.Property(p => p.Level2).HasMaxLength(256).IsRequired(false);
builder.Property(p => p.Level3).HasMaxLength(256).IsRequired(false);
builder.Property(p => p.Level4).HasMaxLength(256).IsRequired(false);
builder.Property(p => p.Level5).HasMaxLength(256).IsRequired(false);
builder.Property(p => p.Number).HasMaxLength(256).IsRequired();
builder.Property(p => p.Latitude).IsRequired(false).HasDefaultValue(null);
builder.Property(p => p.Longitude).IsRequired(false).HasDefaultValue(null);

builder
.HasOne(x => x.ElectionRound)
Expand Down
Loading
Loading