Skip to content

Commit

Permalink
Manage mandates in admin area (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
tschumpr authored Jul 17, 2024
2 parents 6839a74 + 1f5a271 commit f3b980c
Show file tree
Hide file tree
Showing 33 changed files with 1,217 additions and 93 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add Cypress test support.
- Added localization.
- Added separate administration area and user navigation menu to switch between delivery, administration and STAC browser.
- Added grid to manage mandates in administration area.

### Changed

Expand Down
4 changes: 3 additions & 1 deletion src/Geopilot.Api/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public List<Mandate> MandatesWithIncludes
get
{
return Mandates
.Include(d => d.Deliveries.Where(delivery => !delivery.Deleted))
.Include(m => m.Organisations)
.ThenInclude(o => o.Users)
.Include(m => m.Deliveries.Where(delivery => !delivery.Deleted))
.ThenInclude(d => d.Assets)
.AsNoTracking()
.ToList();
Expand Down
116 changes: 109 additions & 7 deletions src/Geopilot.Api/Controllers/MandateController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using Geopilot.Api.Authorization;
using Geopilot.Api.DTOs;
using Geopilot.Api.Models;
using Geopilot.Api.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;
using Swashbuckle.AspNetCore.Annotations;
using System.Globalization;

namespace Geopilot.Api.Controllers;

/// <summary>
/// Controller for listing mandates.
/// Controller for mandates.
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
Expand Down Expand Up @@ -50,16 +53,20 @@ public async Task<IActionResult> Get(
if (user == null)
return Unauthorized();

var mandates = context.Mandates
.Where(m => m.Organisations.SelectMany(o => o.Users).Any(u => u.Id == user.Id));
var mandates = context.MandatesWithIncludes.AsQueryable();

if (!user.IsAdmin)
{
mandates = mandates.Where(m => m.Organisations.SelectMany(o => o.Users).Any(u => u.Id == user.Id));
}

if (jobId != default)
{
var job = validationService.GetJob(jobId);
if (job is null)
{
logger.LogTrace("Validation job with id <{JobId}> was not found.", jobId);
return Ok(Array.Empty<Mandate>());
return Ok(Array.Empty<MandateDto>());
}

logger.LogTrace("Filtering mandates for job with id <{JobId}>", jobId);
Expand All @@ -68,9 +75,104 @@ public async Task<IActionResult> Get(
.Where(m => m.FileTypes.Contains(".*") || m.FileTypes.Contains(extension));
}

var result = await mandates.ToListAsync();
var result = mandates.Select(MandateDto.FromMandate).ToList();

logger.LogInformation($"Getting mandates with for job with id <{jobId}> resulted in <{result.Count}> matching mandates.");
return Ok(result);
}

/// <summary>
/// Asynchronously creates the <paramref name="mandateDto"/> specified.
/// </summary>
/// <param name="mandateDto">The mandate to create.</param>
[HttpPost]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status201Created, "The mandate was created successfully.")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "The mandate could not be created due to invalid input.")]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to create a mandate.")]
[SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ", typeof(ProblemDetails), new[] { "application/json" })]
public async Task<IActionResult> Create(MandateDto mandateDto)
{
try
{
if (mandateDto == null)
return BadRequest();

var mandate = await TransformToMandate(mandateDto);

var entityEntry = await context.AddAsync(mandate).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);

var result = entityEntry.Entity;
var location = new Uri(string.Format(CultureInfo.InvariantCulture, $"/api/v1/mandate/{result.Id}"), UriKind.Relative);
return Created(location, MandateDto.FromMandate(result));
}
catch (Exception e)
{
logger.LogError(e, $"An error occured while creating the mandate.");
return Problem(e.Message);
}
}

/// <summary>
/// Asynchronously updates the <paramref name="mandateDto"/> specified.
/// </summary>
/// <param name="mandateDto">The mandate to update.</param>
[HttpPut]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status200OK, "The mandate was updated successfully.")]
[SwaggerResponse(StatusCodes.Status404NotFound, "The mandate could not be found.")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "The mandate could not be updated due to invalid input.")]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to edit a mandate.")]
[SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ", typeof(ProblemDetails), new[] { "application/json" })]

public async Task<IActionResult> Edit(MandateDto mandateDto)
{
try
{
if (mandateDto == null)
return BadRequest();

var updatedMandate = await TransformToMandate(mandateDto);
var existingMandate = await context.Mandates
.Include(m => m.Organisations)
.Include(m => m.Deliveries)
.FirstOrDefaultAsync(m => m.Id == mandateDto.Id);

if (existingMandate == null)
return NotFound();

context.Entry(existingMandate).CurrentValues.SetValues(updatedMandate);

existingMandate.Organisations.Clear();
foreach (var organisation in updatedMandate.Organisations)
{
if (!existingMandate.Organisations.Contains(organisation))
existingMandate.Organisations.Add(organisation);
}

logger.LogInformation("Getting mandates with for job with id <{JobId}> resulted in <{MatchingMandatesCount}> matching mandates.", jobId, result.Count);
return Ok(mandates);
await context.SaveChangesAsync().ConfigureAwait(false);
return Ok(MandateDto.FromMandate(updatedMandate));
}
catch (Exception e)
{
logger.LogError(e, $"An error occured while updating the mandate.");
return Problem(e.Message);
}
}

private async Task<Mandate> TransformToMandate(MandateDto mandateDto)
{
var organisations = await context.Organisations.Where(o => mandateDto.Organisations.Contains(o.Id)).ToListAsync();
var deliveries = await context.Deliveries.Where(d => mandateDto.Deliveries.Contains(d.Id)).ToListAsync();
return new Mandate
{
Id = mandateDto.Id,
Name = mandateDto.Name,
FileTypes = mandateDto.FileTypes.ToArray(),
SpatialExtent = Geometry.DefaultFactory.CreatePolygon(),
Organisations = organisations,
Deliveries = deliveries,
};
}
}
51 changes: 51 additions & 0 deletions src/Geopilot.Api/Controllers/OrganisationController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Geopilot.Api.Authorization;
using Geopilot.Api.DTOs;
using Geopilot.Api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Annotations;

namespace Geopilot.Api.Controllers;

/// <summary>
/// Controller for organisations.
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrganisationController : ControllerBase
{
private readonly ILogger<OrganisationController> logger;
private readonly Context context;

/// <summary>
/// Initializes a new instance of the <see cref="OrganisationController"/> class.
/// </summary>
/// <param name="logger">Logger for the instance.</param>
/// <param name="context">Database context for getting organisations.</param>
public OrganisationController(ILogger<OrganisationController> logger, Context context)
{
this.logger = logger;
this.context = context;
}

/// <summary>
/// Get a list of organisations.
/// </summary>
[HttpGet]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status200OK, "Returns list of organisations.", typeof(IEnumerable<Organisation>), new[] { "application/json" })]
public IActionResult Get()
{
logger.LogInformation("Getting organisations.");

var organisations = context.Organisations
.Include(o => o.Mandates)
.Include(o => o.Users)
.AsNoTracking()
.Select(OrganisationDto.FromOrganisation)
.ToList();

return Ok(organisations);
}
}
33 changes: 33 additions & 0 deletions src/Geopilot.Api/DTOs/CoordinateDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using NetTopologySuite.Geometries;

namespace Geopilot.Api.DTOs;

/// <summary>
/// A coordinate in a two-dimensional space.
/// </summary>
public class CoordinateDto
{
/// <summary>
/// Create a new <see cref="CoordinateDto"/> from a <see cref="Coordinate"/>.
/// </summary>
/// <param name="coordinate"></param>
/// <returns></returns>
public static CoordinateDto FromCoordinate(Coordinate coordinate)
{
return new CoordinateDto
{
X = coordinate.X,
Y = coordinate.Y,
};
}

/// <summary>
/// The x-coordinate of the coordinate.
/// </summary>
public double X { get; set; }

/// <summary>
/// The y-coordinate of the coordinate.
/// </summary>
public double Y { get; set; }
}
66 changes: 66 additions & 0 deletions src/Geopilot.Api/DTOs/MandateDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Geopilot.Api.Models;

namespace Geopilot.Api.DTOs;

/// <summary>
/// A contract between the system owner and an organisation for data delivery.
/// The mandate describes where and in what format data should be delivered.
/// </summary>
public class MandateDto
{
/// <summary>
/// Create a new <see cref="MandateDto"/> from a <see cref="Mandate"/>.
/// </summary>
public static MandateDto FromMandate(Mandate mandate)
{
var wkt = mandate.SpatialExtent.AsText();
var spatialExtent = new List<CoordinateDto>();
if (mandate.SpatialExtent.Coordinates.Length == 5)
{
spatialExtent.Add(CoordinateDto.FromCoordinate(mandate.SpatialExtent.Coordinates[0]));
spatialExtent.Add(CoordinateDto.FromCoordinate(mandate.SpatialExtent.Coordinates[2]));
}

return new MandateDto
{
Id = mandate.Id,
Name = mandate.Name,
FileTypes = mandate.FileTypes,
SpatialExtent = spatialExtent,
Organisations = mandate.Organisations.Select(o => o.Id).ToList(),
Deliveries = mandate.Deliveries.Select(d => d.Id).ToList(),
};
}

/// <summary>
/// The unique identifier for the mandate.
/// </summary>
public int Id { get; set; }

/// <summary>
/// The display name of the mandate.
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// List of file types that are allowed to be delivered. Include the period "." and support wildcards "*".
/// </summary>
#pragma warning disable CA1819 // Properties should not return arrays
public string[] FileTypes { get; set; } = Array.Empty<string>();
#pragma warning restore CA1819 // Properties should not return arrays

/// <summary>
/// The minimum and maximum coordinates of the spatial extent of the mandate.
/// </summary>
public List<CoordinateDto> SpatialExtent { get; set; } = new List<CoordinateDto>();

/// <summary>
/// IDs of the organisations allowed to deliver data fulfilling the mandate.
/// </summary>
public List<int> Organisations { get; set; } = new List<int>();

/// <summary>
/// IDs of the data deliveries that have been declared fulfilling the mandate.
/// </summary>
public List<int> Deliveries { get; set; } = new List<int>();
}
43 changes: 43 additions & 0 deletions src/Geopilot.Api/DTOs/OrganisationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Geopilot.Api.Models;

namespace Geopilot.Api.DTOs;

/// <summary>
/// A company or group of users that may have a mandate for delivering data to the system owner.
/// </summary>
public class OrganisationDto
{
/// <summary>
/// Create a new <see cref="OrganisationDto"/> from a <see cref="Organisation"/>.
/// </summary>
public static OrganisationDto FromOrganisation(Organisation organisation)
{
return new OrganisationDto
{
Id = organisation.Id,
Name = organisation.Name,
Users = organisation.Users.Select(u => u.Id).ToList(),
Mandates = organisation.Mandates.Select(m => m.Id).ToList(),
};
}

/// <summary>
/// The unique identifier for the organisation.
/// </summary>
public int Id { get; set; }

/// <summary>
/// The display name of the organisation.
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// IDs of the users that are members of the organisation.
/// </summary>
public List<int> Users { get; set; } = new List<int>();

/// <summary>
/// IDs of the mandates the organisation has for delivering data to the system owner.
/// </summary>
public List<int> Mandates { get; set; } = new List<int>();
}
12 changes: 12 additions & 0 deletions src/Geopilot.Api/Models/IIdentifiable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Geopilot.Api.Models;

/// <summary>
/// An object that can be identified by a numeric ID.
/// </summary>
public interface IIdentifiable
{
/// <summary>
/// Gets or sets the entity's id.
/// </summary>
public int Id { get; set; }
}
2 changes: 1 addition & 1 deletion src/Geopilot.Api/Models/Mandate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Geopilot.Api.Models;
/// A contract between the system owner and an organisation for data delivery.
/// The mandate describes where and in what format data should be delivered.
/// </summary>
public class Mandate
public class Mandate : IIdentifiable
{
/// <summary>
/// The unique identifier for the mandate.
Expand Down
2 changes: 1 addition & 1 deletion src/Geopilot.Api/Models/Organisation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// A company or group of users that may have a mandate for delivering data to the system owner.
/// </summary>
public class Organisation
public class Organisation : IIdentifiable
{
/// <summary>
/// The unique identifier for the organisation.
Expand Down
Loading

0 comments on commit f3b980c

Please sign in to comment.