Skip to content

Commit

Permalink
Allow privileged users to manage GeoLocations list. (#91)
Browse files Browse the repository at this point in the history
* Use fixed format for logger.

* Implement endpoints to CREATE and DELETE geolocations.

* Improve README file.
  • Loading branch information
Zifah authored Aug 15, 2024
1 parent 2d2b9ea commit 127767e
Show file tree
Hide file tree
Showing 19 changed files with 133 additions and 49 deletions.
29 changes: 27 additions & 2 deletions Api/Controllers/GeoLocationsController.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
using Application.Services;
using Api.Utilities;
using Application.Services;
using Core.Dto.Request;
using Core.Dto.Response;
using Core.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;

namespace Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
[Authorize(Policy = "AdminAndProLexicographers")]
public class GeoLocationsController : ControllerBase
{
private readonly GeoLocationsService _geoLocationsService;
Expand All @@ -23,11 +27,32 @@ public GeoLocationsController(GeoLocationsService geoLocationsService)
/// An <see cref="GeoLocation[]"/> representing the response containing the list of <see cref="GeoLocation"/> objects.
/// </returns>
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(typeof(GeoLocationDto[]), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ListGeoLocations()
{
var result = (await _geoLocationsService.GetAll()).Select(g => new GeoLocationDto(g.Place, g.Region));
var result = (await _geoLocationsService.GetAll()).Select(g => new GeoLocationDto(g.Id, g.Place, g.Region));
return Ok(result);
}

[HttpPost]
[ProducesResponseType(typeof(GeoLocationDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Create(CreateGeoLocationDto geo)
{
var geoLocation = new GeoLocation(geo.Place, geo.Region)
{
CreatedBy = User!.Identity!.Name!
};
await _geoLocationsService.Create(geoLocation);
return StatusCode((int)HttpStatusCode.Created, ResponseHelper.GetResponseDict("Geolocation successfully added"));
}

[HttpDelete("{id}/{place}")]
[ProducesResponseType(typeof(GeoLocationDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(string id, string place)
{
await _geoLocationsService.Delete(id, place);
return Ok(ResponseHelper.GetResponseDict($"Geolocation '{place}' successfully deleted"));
}
}
}
11 changes: 4 additions & 7 deletions Api/ExceptionHandler/GlobalExceptionHandling.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Api.ExceptionHandler
{
using Api.Utilities;
using Application.Exceptions;
using System.Net;
using System.Text.Json;
Expand Down Expand Up @@ -32,21 +33,17 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception
context.Response.ContentType = "application/json";
var response = context.Response;

var errorResponse = new ErrorResponse
{
Success = false
};
Dictionary<string, string> errorResponse;
switch (exception)
{
case ClientException ex:
response.StatusCode = (int)HttpStatusCode.BadRequest;
errorResponse.Message = ex.Message;
errorResponse = ResponseHelper.GetResponseDict(ex.Message);
break;


default:
response.StatusCode = (int)HttpStatusCode.InternalServerError;
errorResponse.Message = "Internal server error!";
errorResponse = ResponseHelper.GetResponseDict("Internal server error!");
break;
}
_logger.LogError(exception, "Unhandled Application Exception");
Expand Down
2 changes: 1 addition & 1 deletion Application/Mappers/NameEntryMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static NameEntryDto MapToDto(this NameEntry nameEntry)
Meaning = nameEntry.Meaning,
ExtendedMeaning = nameEntry.ExtendedMeaning,
Morphology = (CommaSeparatedString)nameEntry.Morphology,
GeoLocation = nameEntry.GeoLocation.Select(ge => new GeoLocationDto(ge.Place, ge.Region)).ToList(),
GeoLocation = nameEntry.GeoLocation.Select(ge => new GeoLocationDto(ge.Id, ge.Place, ge.Region)).ToList(),
FamousPeople = (CommaSeparatedString)nameEntry.FamousPeople,
Media = (CommaSeparatedString)nameEntry.Media,
SubmittedBy = nameEntry.CreatedBy,
Expand Down
2 changes: 1 addition & 1 deletion Application/Mappers/SuggestedNameMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static SuggestedNameDto MapToDto(this SuggestedName request)
Name = request.Name,
Email = request.Email,
Details = request.Details,
GeoLocation = request.GeoLocation.Select(ge => new GeoLocationDto(ge.Place, ge.Region)).ToList(),
GeoLocation = request.GeoLocation.Select(ge => new GeoLocationDto(ge.Id, ge.Place, ge.Region)).ToList(),
};
}
}
35 changes: 32 additions & 3 deletions Application/Services/GeoLocationsService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Core.Entities;
using Application.Exceptions;
using Core.Entities;
using Core.Repositories;
using Application.Exceptions;

namespace Application.Services
{
Expand All @@ -14,7 +14,36 @@ public GeoLocationsService(IGeoLocationsRepository geoLocationsRepository)
}
public async Task<List<GeoLocation>> GetAll()
{
return await _geoLocationsRepository.GetAll();
return await _geoLocationsRepository.GetAll();
}
public async Task Create(GeoLocation geoLocation)
{
var match = await _geoLocationsRepository.FindByPlace(geoLocation.Place);
if (match != null)
{
throw new ClientException("This location already exists.");
}

await _geoLocationsRepository.Create(new GeoLocation
{
Place = geoLocation.Place.Trim().ToUpper(),
Region = geoLocation.Region.Trim().ToUpper(),
CreatedBy = geoLocation.CreatedBy,
});
}

public async Task Delete(string id, string place)
{
if (string.IsNullOrWhiteSpace(place) || string.IsNullOrWhiteSpace(id))
{
throw new ClientException("One or more input parameters are not valid.");
}

var deleteCount = await _geoLocationsRepository.Delete(id, place);
if (deleteCount == 0)
{
throw new ClientException("No matching records were found to delete.");
}
}
}
}
2 changes: 1 addition & 1 deletion Application/Services/NameEntryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task Create(NameEntry entry)
{
existingName.Duplicates.Add(entry);
await UpdateName(existingName);
_logger.LogWarning($"Someone attempted to create a new name over existing name: {name}.");
_logger.LogWarning("Someone attempted to create a new name over existing name: {name}.", name);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion Application/Validation/GeoLocationValidator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Core.Dto.Request;
using Core.Dto.Response;
using Core.Repositories;
using FluentValidation;
using System;
Expand Down
6 changes: 6 additions & 0 deletions Core/Dto/Request/CreateGeoLocationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Core.Dto.Request
{
public record CreateGeoLocationDto(string Place, string Region)
{
}
}
2 changes: 2 additions & 0 deletions Core/Dto/Request/CreateSuggestedNameDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

using Core.Dto.Response;

namespace Core.Dto.Request;

public record CreateSuggestedNameDto
Expand Down
11 changes: 0 additions & 11 deletions Core/Dto/Request/GeoLocationDto.cs

This file was deleted.

3 changes: 2 additions & 1 deletion Core/Dto/Request/NameDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Core.Enums;
using Core.Dto.Response;
using Core.Enums;
using System.ComponentModel.DataAnnotations;

namespace Core.Dto.Request
Expand Down
10 changes: 10 additions & 0 deletions Core/Dto/Response/GeoLocationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Core.Dto.Response
{
public record GeoLocationDto(string Id, string Place, string Region)
{
public override string ToString()
{
return Place;
}
}
}
4 changes: 1 addition & 3 deletions Core/Dto/Response/SuggestedNameDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Core.Dto.Request;

namespace Core.Dto.Response;
namespace Core.Dto.Response;

public record SuggestedNameDto
{
Expand Down
7 changes: 2 additions & 5 deletions Core/Repositories/IGeoLocationsRepository.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using Core.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Repositories
{
Expand All @@ -13,5 +8,7 @@ public interface IGeoLocationsRepository

Task<GeoLocation> FindByPlace(string place);
Task<GeoLocation> FindByPlaceAndRegion(string region, string place);
Task Create(GeoLocation geoLocation);
Task<int> Delete(string id, string place);
}
}
47 changes: 38 additions & 9 deletions Infrastructure.MongoDB/Repositories/GeoLocationsRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Infrastructure.MongoDB.Repositories
{
public class GeoLocationsRepository : IGeoLocationsRepository
public class GeoLocationsRepository : MongoDBRepository, IGeoLocationsRepository
{
private readonly IMongoCollection<GeoLocation> _geoLocationsCollection;

Expand All @@ -18,25 +18,47 @@ public GeoLocationsRepository(IMongoDatabase database)
InitGeoLocation();
}
}

public async Task<GeoLocation> FindByPlace(string place)
{
var filter = Builders<GeoLocation>.Filter.Eq("Place", place);
return await _geoLocationsCollection.Find(filter).SingleOrDefaultAsync();
var filter = Builders<GeoLocation>.Filter.Eq( ge => ge.Place, place);
var options = SetCollationPrimary<FindOptions>(new FindOptions());
return await _geoLocationsCollection.Find(filter, options).SingleOrDefaultAsync();
}

public async Task<GeoLocation> FindByPlaceAndRegion(string region, string place)
{
var filter = Builders<GeoLocation>.Filter.And(
Builders<GeoLocation>.Filter.Eq("Region", region.ToUpper()),
Builders<GeoLocation>.Filter.Eq("Place", place.ToUpper())
Builders<GeoLocation>.Filter.Eq(ge => ge.Region, region),
Builders<GeoLocation>.Filter.Eq(ge => ge.Place, place)
);
return await _geoLocationsCollection.Find(filter).FirstOrDefaultAsync();
var options = SetCollationPrimary<FindOptions>(new FindOptions());
return await _geoLocationsCollection.Find(filter, options).FirstOrDefaultAsync();
}

public async Task<List<GeoLocation>> GetAll()
public async Task<List<GeoLocation>> GetAll() => await _geoLocationsCollection
.Find(Builders<GeoLocation>.Filter.Empty)
.Sort(Builders<GeoLocation>.Sort.Ascending(g => g.Place))
.ToListAsync();

public async Task Create(GeoLocation geoLocation)
{
return await _geoLocationsCollection.Find(FilterDefinition<GeoLocation>.Empty).ToListAsync();
geoLocation.Id = ObjectId.GenerateNewId().ToString();
await _geoLocationsCollection.InsertOneAsync(geoLocation);
}

public async Task<int> Delete(string id, string place)
{
var filterBuilder = Builders<GeoLocation>.Filter;

var filter = filterBuilder.And(
filterBuilder.Eq(g => g.Id, id),
filterBuilder.Eq(g => g.Place, place)
);

var options = SetCollationPrimary<DeleteOptions>(new DeleteOptions());
var deleteResult = await _geoLocationsCollection.DeleteOneAsync(filter, options);
return (int)deleteResult.DeletedCount;
}

private void InitGeoLocation()
Expand Down Expand Up @@ -65,6 +87,12 @@ private void InitGeoLocation()
Region = "OYO"
},
new()
{
Id = ObjectId.GenerateNewId().ToString(),
Place = "OSUN",
Region = "OYO"
},
new()
{
Id = ObjectId.GenerateNewId().ToString(),
Place = "OGUN",
Expand Down Expand Up @@ -186,5 +214,6 @@ private void InitGeoLocation()
}
});
}

}
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ To run the Website and API locally, follow these steps:
5. **Access the Application:**
- Once the Docker containers are up and running, you can access the Website in your browser. The website should launch automatically as soon as all the containers are ready.
- If the Website does not launch automatically, it will be running at `http://localhost:{port}` (the actual port will be displayed in the output window of Visual Studio: "Container Tools").
- The API will also be running locally and accessible via a different URL. You can see its documentation and test the endpoints at `http://localhost:51515/swagger`.
- The API will also be running locally and accessible via a different URL. You can see its documentation and test the endpoints at `http://localhost:51515/swagger`.
- You can login to the API with any username from [the database initialization script](./mongo-init.js) and password: **Password@135**.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion Website/Pages/SubmitName.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Application.Services;
using Core.Dto.Request;
using Core.Dto.Response;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Website.Pages.Shared;
Expand Down
3 changes: 1 addition & 2 deletions Website/Services/ApiService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Core.Dto.Request;
using Core.Dto.Response;
using Core.Dto.Response;
using Microsoft.Extensions.Options;
using System.Text.Json;
using Website.Config;
Expand Down
1 change: 1 addition & 0 deletions YorubaNameDictionary.sln
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6626E809-1AC8-46E8-BBAF-1862FBC64D3D}"
ProjectSection(SolutionItems) = preProject
mongo-init.js = mongo-init.js
README.md = README.md
EndProjectSection
EndProject
Global
Expand Down

0 comments on commit 127767e

Please sign in to comment.