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
@@ -0,0 +1,15 @@
using Vote.Monitor.Domain.Entities.FormBase;

namespace Feature.Forms.Specifications;

public sealed class GetCitizenReportingFormsByIdsSpecification : Specification<FormAggregate>
{
public GetCitizenReportingFormsByIdsSpecification(Guid electionRoundId, Guid ngoId, IEnumerable<Guid> formIds)
{
Query.Where(x =>
x.ElectionRoundId == electionRoundId
&& x.MonitoringNgo.NgoId == ngoId
&& x.FormType == FormType.CitizenReporting
&& formIds.Contains(x.Id));
}
}
16 changes: 16 additions & 0 deletions api/src/Feature.Forms/Specifications/GetFormsByIdsSpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Vote.Monitor.Domain.Entities.FormBase;

namespace Feature.Forms.Specifications;

public sealed class GetFormsByIdsSpecification : Specification<FormAggregate>
{
public GetFormsByIdsSpecification(Guid electionRoundId, Guid ngoId, IEnumerable<Guid> formIds)
{
Query.Where(x =>
x.ElectionRoundId == electionRoundId
&& x.MonitoringNgo.NgoId == ngoId
&& x.FormType != FormType.CitizenReporting
&& x.FormType != FormType.IncidentReporting
&& formIds.Contains(x.Id));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Vote.Monitor.Domain.Entities.FormBase;

namespace Feature.Forms.Specifications;

public sealed class GetIncidentReportingFormsByIdsSpecification : Specification<FormAggregate>
{
public GetIncidentReportingFormsByIdsSpecification(Guid electionRoundId, Guid ngoId, IEnumerable<Guid> formIds)
{
Query.Where(x =>
x.ElectionRoundId == electionRoundId
&& x.MonitoringNgo.NgoId == ngoId
&& x.FormType == FormType.IncidentReporting
&& formIds.Contains(x.Id));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Authorization.Policies;
using Authorization.Policies.Requirements;
using Feature.Forms.Specifications;
using Microsoft.AspNetCore.Authorization;
using Vote.Monitor.Domain.Entities.FormAggregate;
using Vote.Monitor.Domain.Entities.MonitoringNgoAggregate;

namespace Feature.Forms.UpdateCitizenReportingDisplayOrder;

public class Endpoint(
IAuthorizationService authorizationService,
IRepository<MonitoringNgo> monitoringNgoRepository,
IRepository<FormAggregate> formsRepository) : Endpoint<Request, Results<NoContent, NotFound, ProblemDetails>>
{
public override void Configure()
{
Put("/api/election-rounds/{electionRoundId}/citizen-reporting-forms:display-order");
Description(x => x.Accepts<Request>());
DontAutoTag();
Options(x => x.WithTags("citizen-reporting-forms", "forms"));
Policies(PolicyNames.NgoAdminsOnly);
Summary(s =>
{
s.Summary =
"Updates display order for published citizen reporting forms belonging to the citizen reporting monitoring NGO";
});
}

public override async Task<Results<NoContent, NotFound, ProblemDetails>> ExecuteAsync(Request req,
CancellationToken ct)
{
var requirement = new CitizenReportingNgoAdminRequirement(req.ElectionRoundId);
var authorizationResult = await authorizationService.AuthorizeAsync(User, requirement);
if (!authorizationResult.Succeeded)
{
return TypedResults.NotFound();
}

var formIds = req.Forms.Select(x => x.FormId).ToList();
var forms = await formsRepository.ListAsync(
new GetCitizenReportingFormsByIdsSpecification(req.ElectionRoundId, req.NgoId, formIds), ct);

if (forms.Count != formIds.Count)
{
return TypedResults.NotFound();
}

if (forms.Any(x => x.Status != FormStatus.Published))
{
AddError("All forms must be published.");
return new ProblemDetails(ValidationFailures);
}

var displayOrders = req.Forms.ToDictionary(x => x.FormId, x => x.DisplayOrder);
foreach (var form in forms)
{
form.UpdateDisplayOrder(displayOrders[form.Id]);
}

await formsRepository.UpdateRangeAsync(forms, ct);

var monitoringNgo = await monitoringNgoRepository.FirstOrDefaultAsync(
new GetMonitoringNgoSpecification(req.ElectionRoundId, req.NgoId), ct);
monitoringNgo!.UpdateFormVersion();
await monitoringNgoRepository.UpdateAsync(monitoringNgo, ct);

return TypedResults.NoContent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Vote.Monitor.Core.Security;

namespace Feature.Forms.UpdateCitizenReportingDisplayOrder;

public class Request
{
public Guid ElectionRoundId { get; set; }

[FromClaim(ApplicationClaimTypes.NgoId)]
public Guid NgoId { get; set; }

public List<FormDisplayOrderModel> Forms { get; set; } = [];
}

public class FormDisplayOrderModel
{
public Guid FormId { get; set; }
public int DisplayOrder { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Feature.Forms.UpdateCitizenReportingDisplayOrder;

public class Validator : Validator<Request>
{
public Validator()
{
RuleFor(x => x.ElectionRoundId).NotEmpty();
RuleFor(x => x.NgoId).NotEmpty();
RuleFor(x => x.Forms).NotEmpty();
RuleFor(x => x.Forms)
.Must(forms => forms.Select(f => f.FormId).Distinct().Count() == forms.Count)
.WithMessage("Form ids must be unique.");

RuleForEach(x => x.Forms).ChildRules(form =>
{
form.RuleFor(x => x.FormId).NotEmpty();
form.RuleFor(x => x.DisplayOrder).GreaterThanOrEqualTo(0);
});
}
}
68 changes: 68 additions & 0 deletions api/src/Feature.Forms/UpdateDisplayOrder/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Authorization.Policies;
using Authorization.Policies.Requirements;
using Feature.Forms.Specifications;
using Microsoft.AspNetCore.Authorization;
using Vote.Monitor.Domain.Entities.FormAggregate;
using Vote.Monitor.Domain.Entities.MonitoringNgoAggregate;

namespace Feature.Forms.UpdateDisplayOrder;

public class Endpoint(
IAuthorizationService authorizationService,
IRepository<MonitoringNgo> monitoringNgoRepository,
IRepository<FormAggregate> formsRepository) : Endpoint<Request, Results<NoContent, NotFound, ProblemDetails>>
{
public override void Configure()
{
Put("/api/election-rounds/{electionRoundId}/forms:display-order");
Description(x => x.Accepts<Request>());
DontAutoTag();
Options(x => x.WithTags("forms"));
Policies(PolicyNames.NgoAdminsOnly);
Summary(s =>
{
s.Summary = "Updates display order for published forms belonging to the current monitoring NGO";
});
}

public override async Task<Results<NoContent, NotFound, ProblemDetails>> ExecuteAsync(Request req,
CancellationToken ct)
{
var requirement = new MonitoringNgoAdminRequirement(req.ElectionRoundId);
var authorizationResult = await authorizationService.AuthorizeAsync(User, requirement);
if (!authorizationResult.Succeeded)
{
return TypedResults.NotFound();
}

var formIds = req.Forms.Select(x => x.FormId).ToList();
var forms = await formsRepository.ListAsync(
new GetFormsByIdsSpecification(req.ElectionRoundId, req.NgoId, formIds), ct);

if (forms.Count != formIds.Count)
{
return TypedResults.NotFound();
}

if (forms.Any(x => x.Status != FormStatus.Published))
{
AddError("All forms must be published.");
return new ProblemDetails(ValidationFailures);
}

var displayOrders = req.Forms.ToDictionary(x => x.FormId, x => x.DisplayOrder);
foreach (var form in forms)
{
form.UpdateDisplayOrder(displayOrders[form.Id]);
}

await formsRepository.UpdateRangeAsync(forms, ct);

var monitoringNgo = await monitoringNgoRepository.FirstOrDefaultAsync(
new GetMonitoringNgoSpecification(req.ElectionRoundId, req.NgoId), ct);
monitoringNgo!.UpdateFormVersion();
await monitoringNgoRepository.UpdateAsync(monitoringNgo, ct);

return TypedResults.NoContent();
}
}
19 changes: 19 additions & 0 deletions api/src/Feature.Forms/UpdateDisplayOrder/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Vote.Monitor.Core.Security;

namespace Feature.Forms.UpdateDisplayOrder;

public class Request
{
public Guid ElectionRoundId { get; set; }

[FromClaim(ApplicationClaimTypes.NgoId)]
public Guid NgoId { get; set; }

public List<FormDisplayOrderModel> Forms { get; set; } = [];
}

public class FormDisplayOrderModel
{
public Guid FormId { get; set; }
public int DisplayOrder { get; set; }
}
20 changes: 20 additions & 0 deletions api/src/Feature.Forms/UpdateDisplayOrder/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Feature.Forms.UpdateDisplayOrder;

public class Validator : Validator<Request>
{
public Validator()
{
RuleFor(x => x.ElectionRoundId).NotEmpty();
RuleFor(x => x.NgoId).NotEmpty();
RuleFor(x => x.Forms).NotEmpty();
RuleFor(x => x.Forms)
.Must(forms => forms.Select(f => f.FormId).Distinct().Count() == forms.Count)
.WithMessage("Form ids must be unique.");

RuleForEach(x => x.Forms).ChildRules(form =>
{
form.RuleFor(x => x.FormId).NotEmpty();
form.RuleFor(x => x.DisplayOrder).GreaterThanOrEqualTo(0);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Authorization.Policies;
using Authorization.Policies.Requirements;
using Feature.Forms.Specifications;
using Microsoft.AspNetCore.Authorization;
using Vote.Monitor.Domain.Entities.FormAggregate;
using Vote.Monitor.Domain.Entities.MonitoringNgoAggregate;

namespace Feature.Forms.UpdateIncidentReportingDisplayOrder;

public class Endpoint(
IAuthorizationService authorizationService,
IRepository<MonitoringNgo> monitoringNgoRepository,
IRepository<FormAggregate> formsRepository) : Endpoint<Request, Results<NoContent, NotFound, ProblemDetails>>
{
public override void Configure()
{
Put("/api/election-rounds/{electionRoundId}/incident-reporting-forms:display-order");
Description(x => x.Accepts<Request>());
DontAutoTag();
Options(x => x.WithTags("incident-reporting-forms", "forms"));
Policies(PolicyNames.NgoAdminsOnly);
Summary(s =>
{
s.Summary =
"Updates display order for published incident reporting forms belonging to the current monitoring NGO";
});
}

public override async Task<Results<NoContent, NotFound, ProblemDetails>> ExecuteAsync(Request req,
CancellationToken ct)
{
var requirement = new MonitoringNgoAdminRequirement(req.ElectionRoundId);
var authorizationResult = await authorizationService.AuthorizeAsync(User, requirement);
if (!authorizationResult.Succeeded)
{
return TypedResults.NotFound();
}

var formIds = req.Forms.Select(x => x.FormId).ToList();
var forms = await formsRepository.ListAsync(
new GetIncidentReportingFormsByIdsSpecification(req.ElectionRoundId, req.NgoId, formIds), ct);

if (forms.Count != formIds.Count)
{
return TypedResults.NotFound();
}

if (forms.Any(x => x.Status != FormStatus.Published))
{
AddError("All forms must be published.");
return new ProblemDetails(ValidationFailures);
}

var displayOrders = req.Forms.ToDictionary(x => x.FormId, x => x.DisplayOrder);
foreach (var form in forms)
{
form.UpdateDisplayOrder(displayOrders[form.Id]);
}

await formsRepository.UpdateRangeAsync(forms, ct);

var monitoringNgo = await monitoringNgoRepository.FirstOrDefaultAsync(
new GetMonitoringNgoSpecification(req.ElectionRoundId, req.NgoId), ct);
monitoringNgo!.UpdateFormVersion();
await monitoringNgoRepository.UpdateAsync(monitoringNgo, ct);

return TypedResults.NoContent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Vote.Monitor.Core.Security;

namespace Feature.Forms.UpdateIncidentReportingDisplayOrder;

public class Request
{
public Guid ElectionRoundId { get; set; }

[FromClaim(ApplicationClaimTypes.NgoId)]
public Guid NgoId { get; set; }

public List<FormDisplayOrderModel> Forms { get; set; } = [];
}

public class FormDisplayOrderModel
{
public Guid FormId { get; set; }
public int DisplayOrder { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Feature.Forms.UpdateIncidentReportingDisplayOrder;

public class Validator : Validator<Request>
{
public Validator()
{
RuleFor(x => x.ElectionRoundId).NotEmpty();
RuleFor(x => x.NgoId).NotEmpty();
RuleFor(x => x.Forms).NotEmpty();
RuleFor(x => x.Forms)
.Must(forms => forms.Select(f => f.FormId).Distinct().Count() == forms.Count)
.WithMessage("Form ids must be unique.");

RuleForEach(x => x.Forms).ChildRules(form =>
{
form.RuleFor(x => x.FormId).NotEmpty();
form.RuleFor(x => x.DisplayOrder).GreaterThanOrEqualTo(0);
});
}
}
5 changes: 5 additions & 0 deletions api/src/Vote.Monitor.Domain/Entities/FormAggregate/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public class Form : BaseForm
public ElectionRound ElectionRound { get; private set; }
public int DisplayOrder { get; private set; }

public void UpdateDisplayOrder(int displayOrder)
{
DisplayOrder = displayOrder;
}

private Form(
ElectionRound electionRound,
MonitoringNgo monitoringNgo,
Expand Down
Loading
Loading