diff --git a/api/src/Feature.Forms/FetchAll/Endpoint.cs b/api/src/Feature.Forms/FetchAll/Endpoint.cs index 31c70701f..a62caaeda 100644 --- a/api/src/Feature.Forms/FetchAll/Endpoint.cs +++ b/api/src/Feature.Forms/FetchAll/Endpoint.cs @@ -1,6 +1,5 @@ using Feature.Forms.Models; using Microsoft.EntityFrameworkCore; -using Vote.Monitor.Core.Helpers; using Vote.Monitor.Domain; using Vote.Monitor.Domain.Entities.FormAggregate; using Vote.Monitor.Domain.Entities.FormBase; @@ -80,7 +79,9 @@ public override async Task, NotFound>> Execute return TypedResults.Ok(new NgoFormsResponseModel { ElectionRoundId = monitoringNgo.ElectionRoundId, - Version = DeterministicGuid.Create(resultForms.Select(x => x.Id)).ToString(), + // The same key as forms:version, so it changes when a form's content changes, not only the set of forms. + Version = await ObserverFormsVersion.GetAsync(context, req.ElectionRoundId, monitoringNgo.MonitoringNgoId, + monitoringNgo.FormsVersion, ct), Forms = resultForms.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Code).ToList() }); } diff --git a/api/src/Feature.Forms/GetFormsVersion/Endpoint.cs b/api/src/Feature.Forms/GetFormsVersion/Endpoint.cs index a9f3bd9c4..dfacd54a2 100644 --- a/api/src/Feature.Forms/GetFormsVersion/Endpoint.cs +++ b/api/src/Feature.Forms/GetFormsVersion/Endpoint.cs @@ -15,7 +15,7 @@ public override void Configure() Summary(s => { s.Summary = "Gets current version of forms for users monitoring ngo"; - s.Description = "Cache key changes every time any form changes"; + s.Description = "Cache key changes every time any form the observer receives changes, coalition forms included"; }); } @@ -28,7 +28,7 @@ public override async Task, NotFound>> Exec .Where(x => x.ElectionRoundId == req.ElectionRoundId) .Where(x => x.Status == MonitoringObserverStatus.Active) .Where(x => x.MonitoringNgo.ElectionRoundId == req.ElectionRoundId) - .Select(x => new { x.MonitoringNgo.FormsVersion, x.MonitoringNgo.ElectionRoundId }) + .Select(x => new { x.MonitoringNgoId, x.MonitoringNgo.FormsVersion, x.MonitoringNgo.ElectionRoundId }) .FirstOrDefaultAsync(ct); if (monitoringNgo is null) @@ -38,7 +38,9 @@ public override async Task, NotFound>> Exec return TypedResults.Ok(new FormVersionResponseModel { - ElectionRoundId = monitoringNgo.ElectionRoundId, CacheKey = monitoringNgo.FormsVersion.ToString() + ElectionRoundId = monitoringNgo.ElectionRoundId, + CacheKey = await ObserverFormsVersion.GetAsync(context, req.ElectionRoundId, monitoringNgo.MonitoringNgoId, + monitoringNgo.FormsVersion, ct) }); } } diff --git a/api/src/Feature.Forms/ObserverFormsVersion.cs b/api/src/Feature.Forms/ObserverFormsVersion.cs new file mode 100644 index 000000000..9e2156a2f --- /dev/null +++ b/api/src/Feature.Forms/ObserverFormsVersion.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using Vote.Monitor.Core.Helpers; +using Vote.Monitor.Domain; +using Vote.Monitor.Domain.Entities.FormBase; + +namespace Feature.Forms; + +/// +/// The cache key of the forms an observer's monitoring NGO hands out (forms:version and forms:fetchAll). +/// An NGO's own FormsVersion changes whenever one of its forms changes, but in a coalition its observers also +/// receive forms owned by other NGOs (): +/// the key therefore also follows those owners' versions and which forms are shared, so editing a published shared +/// form or granting access to one gives every receiving NGO a new key. +/// +public static class ObserverFormsVersion +{ + public static async Task GetAsync(VoteMonitorContext context, Guid electionRoundId, Guid monitoringNgoId, + Guid ownFormsVersion, CancellationToken ct) + { + var shared = await context.CoalitionFormAccess + .Where(x => x.MonitoringNgoId == monitoringNgoId && x.Coalition.ElectionRoundId == electionRoundId) + .Where(x => x.Form.FormType != FormType.CitizenReporting) + .Select(x => new { x.FormId, x.Form.MonitoringNgo.FormsVersion }) + .AsNoTracking() + .ToListAsync(ct); + + // Outside a coalition the key stays the NGO's own version, as before. + if (shared.Count == 0) + { + return ownFormsVersion.ToString(); + } + + var parts = shared + .Select(x => x.FormsVersion) + .Distinct() + .Order() + .Select(version => $"owner:{version}") + .Concat(shared.Select(x => x.FormId).Distinct().Order().Select(id => $"form:{id}")) + .Prepend($"own:{ownFormsVersion}"); + + return DeterministicGuid.Create(string.Join('|', parts)).ToString(); + } +}