Skip to content

Commit

Permalink
Manage organisations in admin area (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
tschumpr authored Jul 17, 2024
2 parents aa024a1 + ef2acc7 commit df0d07b
Show file tree
Hide file tree
Showing 20 changed files with 490 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- 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.
- Added grid to manage organisations in administration area.

### Changed

Expand Down
35 changes: 25 additions & 10 deletions src/Geopilot.Api/Context.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Geopilot.Api.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;

namespace Geopilot.Api;

Expand Down Expand Up @@ -27,6 +28,19 @@ public Context(DbContextOptions<Context> options)
/// </summary>
public DbSet<Organisation> Organisations { get; set; }

/// <summary>
/// Gets the <see cref="Organisation"/> entity with all includes.
/// </summary>
public IQueryable<Organisation> OrganisationsWithIncludes
{
get
{
return Organisations
.Include(o => o.Users)
.Include(o => o.Mandates);
}
}

/// <summary>
/// Set of all <see cref="Delivery"/>.
/// </summary>
Expand All @@ -35,18 +49,15 @@ public Context(DbContextOptions<Context> options)
/// <summary>
/// Gets the <see cref="Delivery"/> entity with all includes.
/// </summary>
public List<Delivery> DeliveriesWithIncludes
public IQueryable<Delivery> DeliveriesWithIncludes
{
get
{
return Deliveries
.Where(d => !d.Deleted)
.Include(d => d.Mandate)
.Include(d => d.Assets)
.Include(d => d.DeclaringUser)
.Include(d => d.PrecursorDelivery)
.AsNoTracking()
.ToList();
.Include(d => d.PrecursorDelivery);
}
}

Expand All @@ -58,22 +69,26 @@ public List<Delivery> DeliveriesWithIncludes
/// <summary>
/// Gets the <see cref="Mandate"/> entity with all includes.
/// </summary>
public List<Mandate> MandatesWithIncludes
public IQueryable<Mandate> MandatesWithIncludes
{
get
{
return Mandates
.Include(m => m.Organisations)
.ThenInclude(o => o.Users)
.Include(m => m.Deliveries.Where(delivery => !delivery.Deleted))
.ThenInclude(d => d.Assets)
.AsNoTracking()
.ToList();
.Include(m => m.Deliveries)
.ThenInclude(d => d.Assets);
}
}

/// <summary>
/// Set of all <see cref="Asset"/>.
/// </summary>
public DbSet<Asset> Assets { get; set; }

/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Delivery>().HasQueryFilter(d => !d.Deleted);
}
}
1 change: 1 addition & 0 deletions src/Geopilot.Api/Controllers/DeliveryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public async Task<IActionResult> Get([FromQuery] int? mandateId = null)
return NotFound();

var result = context.DeliveriesWithIncludes
.AsNoTracking()
.Where(d => userMandatesIds.Contains(d.Mandate.Id));

if (mandateId.HasValue)
Expand Down
22 changes: 16 additions & 6 deletions src/Geopilot.Api/Controllers/MandateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<IActionResult> Get(
if (user == null)
return Unauthorized();

var mandates = context.MandatesWithIncludes.AsQueryable();
var mandates = context.MandatesWithIncludes.AsNoTracking();

if (!user.IsAdmin)
{
Expand Down Expand Up @@ -103,7 +103,12 @@ public async Task<IActionResult> Create(MandateDto mandateDto)
var entityEntry = await context.AddAsync(mandate).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);

var result = entityEntry.Entity;
var result = await context.MandatesWithIncludes
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == entityEntry.Entity.Id);
if (result == default)
return Problem("Unable to retrieve created mandate.");

var location = new Uri(string.Format(CultureInfo.InvariantCulture, $"/api/v1/mandate/{result.Id}"), UriKind.Relative);
return Created(location, MandateDto.FromMandate(result));
}
Expand Down Expand Up @@ -134,9 +139,7 @@ public async Task<IActionResult> Edit(MandateDto mandateDto)
return BadRequest();

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

if (existingMandate == null)
Expand All @@ -152,7 +155,14 @@ public async Task<IActionResult> Edit(MandateDto mandateDto)
}

await context.SaveChangesAsync().ConfigureAwait(false);
return Ok(MandateDto.FromMandate(updatedMandate));

var result = await context.MandatesWithIncludes
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == mandateDto.Id);
if (result == default)
return Problem("Unable to retrieve updated mandate.");

return Ok(MandateDto.FromMandate(result));
}
catch (Exception e)
{
Expand Down
123 changes: 118 additions & 5 deletions src/Geopilot.Api/Controllers/OrganisationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Annotations;
using System.Globalization;

namespace Geopilot.Api.Controllers;

Expand Down Expand Up @@ -35,17 +36,129 @@ public OrganisationController(ILogger<OrganisationController> logger, Context co
[HttpGet]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status200OK, "Returns list of organisations.", typeof(IEnumerable<Organisation>), new[] { "application/json" })]
public IActionResult Get()
public List<OrganisationDto> Get()
{
logger.LogInformation("Getting organisations.");

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

/// <summary>
/// Asynchronously creates the <paramref name="organisationDto"/> specified.
/// </summary>
/// <param name="organisationDto">The organisation to create.</param>
[HttpPost]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status201Created, "The organisation was created successfully.")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "The organisation could not be created due to invalid input.")]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to create an organisation.")]
[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(OrganisationDto organisationDto)
{
try
{
if (organisationDto == null)
return BadRequest();

var organisation = await TransformToOrganisation(organisationDto);

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

var result = await context.OrganisationsWithIncludes
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == entityEntry.Entity.Id);
if (result == default)
return Problem("Unable to retrieve created organisation.");

var location = new Uri(string.Format(CultureInfo.InvariantCulture, $"/api/v1/organisation/{result.Id}"), UriKind.Relative);
return Created(location, OrganisationDto.FromOrganisation(result));
}
catch (Exception e)
{
logger.LogError(e, $"An error occurred while creating the organisation.");
return Problem(e.Message);
}
}

/// <summary>
/// Asynchronously updates the <paramref name="organisationDto"/> specified.
/// </summary>
/// <param name="organisationDto">The organisation to update.</param>
[HttpPut]
[Authorize(Policy = GeopilotPolicies.Admin)]
[SwaggerResponse(StatusCodes.Status200OK, "The organisation was updated successfully.")]
[SwaggerResponse(StatusCodes.Status404NotFound, "The organisation could not be found.")]
[SwaggerResponse(StatusCodes.Status400BadRequest, "The organisation could not be updated due to invalid input.")]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to edit an organisation.")]
[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(OrganisationDto organisationDto)
{
try
{
if (organisationDto == null)
return BadRequest();

var updatedOrganisation = await TransformToOrganisation(organisationDto);
var existingOrganisation = await context.OrganisationsWithIncludes
.FirstOrDefaultAsync(o => o.Id == organisationDto.Id);

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

context.Entry(existingOrganisation).CurrentValues.SetValues(updatedOrganisation);

existingOrganisation.Mandates.Clear();
foreach (var mandate in updatedOrganisation.Mandates)
{
if (!existingOrganisation.Mandates.Contains(mandate))
existingOrganisation.Mandates.Add(mandate);
}

existingOrganisation.Users.Clear();
foreach (var user in updatedOrganisation.Users)
{
if (!existingOrganisation.Users.Contains(user))
existingOrganisation.Users.Add(user);
}

await context.SaveChangesAsync().ConfigureAwait(false);

var result = await context.OrganisationsWithIncludes
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == organisationDto.Id);

if (result == default)
return Problem("Unable to retrieve updated organisation.");

return Ok(OrganisationDto.FromOrganisation(result));
}
catch (Exception e)
{
logger.LogError(e, $"An error occurred while updating the organisation.");
return Problem(e.Message);
}
}

private async Task<Organisation> TransformToOrganisation(OrganisationDto organisationDto)
{
var mandates = await context.Mandates
.Where(m => organisationDto.Mandates.Contains(m.Id))
.ToListAsync();
var users = await context.Users
.Where(u => organisationDto.Users.Contains(u.Id))
.ToListAsync();

return Ok(organisations);
return new Organisation
{
Id = organisationDto.Id,
Name = organisationDto.Name,
Mandates = mandates,
Users = users,
};
}
}
21 changes: 19 additions & 2 deletions src/Geopilot.Api/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Geopilot.Api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Annotations;

Expand Down Expand Up @@ -32,14 +33,30 @@ public UserController(ILogger<UserController> logger, Context context, IOptions<
this.authOptions = authOptions.Value;
}

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

return context.Users
.Include(u => u.Organisations)
.AsNoTracking()
.ToList();
}

/// <summary>
/// Gets the current user information.
/// </summary>
/// <returns>The <see cref="User"/> that is currently logged in.</returns>
[HttpGet]
[HttpGet("self")]
[Authorize(Policy = GeopilotPolicies.User)]
[SwaggerResponse(StatusCodes.Status200OK, "Returns the currently logged in user.", typeof(User), new[] { "application/json" })]
public async Task<User?> GetAsync()
public async Task<User?> GetSelfAsync()
{
var user = await context.GetUserByPrincipalAsync(User);
if (user == null)
Expand Down
1 change: 0 additions & 1 deletion src/Geopilot.Api/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public class User
/// <summary>
/// The unique identifier for the user.
/// </summary>
[JsonIgnore]
public int Id { get; set; }

/// <summary>
Expand Down
12 changes: 9 additions & 3 deletions src/Geopilot.Api/StacServices/StacCollectionsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public Task<StacCollection> GetCollectionByIdAsync(string collectionId, IStacApi
try
{
using var db = contextFactory.CreateDbContext();
var mandate = db.MandatesWithIncludes.FirstOrDefault(m => stacConverter.GetCollectionId(m) == collectionId)
var mandate = db.MandatesWithIncludes
.AsNoTracking()
.AsEnumerable()
.FirstOrDefault(m => stacConverter.GetCollectionId(m) == collectionId)
?? throw new InvalidOperationException($"Collection with id {collectionId} does not exist.");
var collection = stacConverter.ToStacCollection(mandate);
return Task.FromResult(collection);
Expand All @@ -47,9 +50,12 @@ public Task<StacCollection> GetCollectionByIdAsync(string collectionId, IStacApi
public Task<IEnumerable<StacCollection>> GetCollectionsAsync(IStacApiContext stacApiContext, CancellationToken cancellationToken = default)
{
using var db = contextFactory.CreateDbContext();
var collections = db.MandatesWithIncludes.Select(stacConverter.ToStacCollection);
var collections = db.MandatesWithIncludes
.AsNoTracking()
.ToList()
.Select(stacConverter.ToStacCollection);
stacApiContext.Properties.SetProperty(DefaultConventions.MatchedCountPropertiesKey, collections.Count());

return Task.FromResult<IEnumerable<StacCollection>>(collections);
return Task.FromResult(collections);
}
}
4 changes: 3 additions & 1 deletion src/Geopilot.Api/StacServices/StacItemsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public Task<StacItem> GetItemByIdAsync(string featureId, IStacApiContext stacApi
{
using var db = contextFactory.CreateDbContext();
var delivery = db.DeliveriesWithIncludes
.AsNoTracking()
.AsEnumerable()
.FirstOrDefault(d => stacConverter.GetItemId(d) == featureId && (stacConverter.GetCollectionId(d.Mandate) == stacApiContext.Collections.First()))
?? throw new InvalidOperationException($"Item with id {featureId} does not exist.");
var item = stacConverter.ToStacItem(delivery);
Expand All @@ -76,7 +78,7 @@ public Task<IEnumerable<StacItem>> GetItemsAsync(IStacApiContext stacApiContext,

var collectionIds = stacApiContext.Collections?.ToList();
using var db = contextFactory.CreateDbContext();
var mandates = db.MandatesWithIncludes;
var mandates = db.MandatesWithIncludes.AsNoTracking().ToList();

if (collectionIds?.Any() == true)
{
Expand Down
Loading

0 comments on commit df0d07b

Please sign in to comment.