Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage mandates in admin area #227

Merged
merged 24 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
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
117 changes: 111 additions & 6 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 @@ -51,15 +54,22 @@ public async Task<IActionResult> Get(
return Unauthorized();

var mandates = context.Mandates
.Where(m => m.Organisations.SelectMany(o => o.Users).Any(u => u.Id == user.Id));
.Include(m => m.Organisations)
.Include(m => m.Deliveries)
.AsNoTracking();
tschumpr marked this conversation as resolved.
Show resolved Hide resolved

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 +78,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