diff --git a/Api/Controllers/GeoLocationsController.cs b/Api/Controllers/GeoLocationsController.cs index 7e44b94..8d4c5a9 100644 --- a/Api/Controllers/GeoLocationsController.cs +++ b/Api/Controllers/GeoLocationsController.cs @@ -1,6 +1,9 @@ -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; @@ -8,6 +11,7 @@ namespace Api.Controllers { [Route("api/v1/[controller]")] [ApiController] + [Authorize(Policy = "AdminAndProLexicographers")] public class GeoLocationsController : ControllerBase { private readonly GeoLocationsService _geoLocationsService; @@ -23,11 +27,32 @@ public GeoLocationsController(GeoLocationsService geoLocationsService) /// An representing the response containing the list of objects. /// [HttpGet] + [AllowAnonymous] [ProducesResponseType(typeof(GeoLocationDto[]), (int)HttpStatusCode.OK)] public async Task 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 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 Delete(string id, string place) + { + await _geoLocationsService.Delete(id, place); + return Ok(ResponseHelper.GetResponseDict($"Geolocation '{place}' successfully deleted")); + } } } diff --git a/Api/Controllers/SuggestedNameController.cs b/Api/Controllers/SuggestedNameController.cs index f3271f8..ffb56af 100644 --- a/Api/Controllers/SuggestedNameController.cs +++ b/Api/Controllers/SuggestedNameController.cs @@ -7,6 +7,7 @@ using System.Net; using Application.Mappers; using Application.Validation; +using FluentValidation; namespace Api.Controllers; @@ -16,9 +17,9 @@ namespace Api.Controllers; public class SuggestedNameController : ControllerBase { private readonly SuggestedNameService _suggestedNameService; - private readonly CreateSuggestedNameValidator _suggestedNameValidator; + private readonly IValidator _suggestedNameValidator; - public SuggestedNameController(SuggestedNameService suggestedNameService, CreateSuggestedNameValidator suggestedNameValidator) + public SuggestedNameController(SuggestedNameService suggestedNameService, IValidator suggestedNameValidator) { _suggestedNameService = suggestedNameService; _suggestedNameValidator = suggestedNameValidator; diff --git a/Api/ExceptionHandler/GlobalExceptionHandling.cs b/Api/ExceptionHandler/GlobalExceptionHandling.cs index 8a9d13f..3f18d1c 100644 --- a/Api/ExceptionHandler/GlobalExceptionHandling.cs +++ b/Api/ExceptionHandler/GlobalExceptionHandling.cs @@ -1,5 +1,6 @@ namespace Api.ExceptionHandler { + using Api.Utilities; using Application.Exceptions; using System.Net; using System.Text.Json; @@ -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 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"); diff --git a/Application/Mappers/NameEntryMapper.cs b/Application/Mappers/NameEntryMapper.cs index 7a00260..57f9200 100644 --- a/Application/Mappers/NameEntryMapper.cs +++ b/Application/Mappers/NameEntryMapper.cs @@ -29,7 +29,7 @@ public static NameEntry MapToEntity(this NameDto request) { return new NameEntry { - Name = request.Name, + Name = request.Name.Trim(), Pronunciation = request.Pronunciation?.Trim(), Meaning = request.Meaning.Trim(), ExtendedMeaning = request.ExtendedMeaning?.Trim(), @@ -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, diff --git a/Application/Mappers/SuggestedNameMapper.cs b/Application/Mappers/SuggestedNameMapper.cs index 8f9422d..b26dc52 100644 --- a/Application/Mappers/SuggestedNameMapper.cs +++ b/Application/Mappers/SuggestedNameMapper.cs @@ -16,9 +16,9 @@ public static SuggestedName MapToEntity(this CreateSuggestedNameDto request) return new SuggestedName { Id = ObjectId.GenerateNewId().ToString(), - Name = request.Name, - Email = request.Email, - Details = request.Details, + Name = request.Name.Trim(), + Email = request.Email?.Trim(), + Details = request.Details?.Trim(), GeoLocation = request.GeoLocation.Select(x => new GeoLocation { Place = x.Place, @@ -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(), }; } } diff --git a/Application/Services/GeoLocationsService.cs b/Application/Services/GeoLocationsService.cs index 1fad5b8..e09a01f 100644 --- a/Application/Services/GeoLocationsService.cs +++ b/Application/Services/GeoLocationsService.cs @@ -1,6 +1,6 @@ -using Core.Entities; +using Application.Exceptions; +using Core.Entities; using Core.Repositories; -using Application.Exceptions; namespace Application.Services { @@ -14,7 +14,36 @@ public GeoLocationsService(IGeoLocationsRepository geoLocationsRepository) } public async Task> 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."); + } } } } diff --git a/Application/Services/NameEntryService.cs b/Application/Services/NameEntryService.cs index 8f22bd0..b1c8c12 100644 --- a/Application/Services/NameEntryService.cs +++ b/Application/Services/NameEntryService.cs @@ -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; } diff --git a/Application/Validation/CreateSuggestedNameValidator.cs b/Application/Validation/CreateSuggestedNameValidator.cs index 6c52f1d..2b44837 100644 --- a/Application/Validation/CreateSuggestedNameValidator.cs +++ b/Application/Validation/CreateSuggestedNameValidator.cs @@ -1,6 +1,4 @@ using Core.Dto.Request; -using Core.Dto.Response; -using Core.Enums; using FluentValidation; namespace Application.Validation diff --git a/Application/Validation/GeoLocationValidator.cs b/Application/Validation/GeoLocationValidator.cs index 8dbb1bd..233e46e 100644 --- a/Application/Validation/GeoLocationValidator.cs +++ b/Application/Validation/GeoLocationValidator.cs @@ -1,15 +1,10 @@ using Core.Dto.Request; using Core.Repositories; using FluentValidation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Application.Validation { - public class GeoLocationValidator : AbstractValidator + public class GeoLocationValidator : AbstractValidator { private readonly IGeoLocationsRepository _geoLocationsRepository; diff --git a/Application/Validation/NameValidator.cs b/Application/Validation/NameValidator.cs index 83b27f3..c6b0a52 100644 --- a/Application/Validation/NameValidator.cs +++ b/Application/Validation/NameValidator.cs @@ -1,13 +1,9 @@ -using Core.Dto; -using Core.Dto.Request; +using Core.Dto.Request; using FluentValidation; namespace Application.Validation { public class NameValidator : AbstractValidator { - - - public NameValidator(GeoLocationValidator geoLocationValidator, EmbeddedVideoValidator embeddedVideoValidator, EtymologyValidator etymologyValidator) { RuleLevelCascadeMode = CascadeMode.Stop; diff --git a/Core/Dto/CharacterSeparatedString.cs b/Core/Dto/CharacterSeparatedString.cs index 1f5b5da..57eb98b 100644 --- a/Core/Dto/CharacterSeparatedString.cs +++ b/Core/Dto/CharacterSeparatedString.cs @@ -8,7 +8,7 @@ public abstract class CharacterSeparatedString where T : CharacterSeparatedSt public CharacterSeparatedString(string? value) { - this.value = value ?? string.Empty; + this.value = value?.Trim() ?? string.Empty; } public override string ToString() diff --git a/Core/Dto/Request/CreateGeoLocationDto.cs b/Core/Dto/Request/CreateGeoLocationDto.cs new file mode 100644 index 0000000..e334b9c --- /dev/null +++ b/Core/Dto/Request/CreateGeoLocationDto.cs @@ -0,0 +1,6 @@ +namespace Core.Dto.Request +{ + public record CreateGeoLocationDto(string Place, string Region) + { + } +} diff --git a/Core/Dto/Request/CreateSuggestedNameDto.cs b/Core/Dto/Request/CreateSuggestedNameDto.cs index 7b53430..de0cb9c 100644 --- a/Core/Dto/Request/CreateSuggestedNameDto.cs +++ b/Core/Dto/Request/CreateSuggestedNameDto.cs @@ -1,15 +1,14 @@ - -namespace Core.Dto.Request; +namespace Core.Dto.Request; public record CreateSuggestedNameDto { public string Name { get; init; } public string Details { get; init; } public string Email { get; init; } - public List GeoLocation { get; set; } + public List GeoLocation { get; set; } public CreateSuggestedNameDto() { - GeoLocation = new List(); + GeoLocation = new List(); } } diff --git a/Core/Dto/Request/GeoLocationDto.cs b/Core/Dto/Request/GeoLocationDto.cs deleted file mode 100644 index 0f797d0..0000000 --- a/Core/Dto/Request/GeoLocationDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Core.Dto.Request -{ - public record GeoLocationDto(string Place, string Region) - { - public override string ToString() - { - return Place; - } - } - -} diff --git a/Core/Dto/Request/NameDto.cs b/Core/Dto/Request/NameDto.cs index 4ab175d..beb674e 100644 --- a/Core/Dto/Request/NameDto.cs +++ b/Core/Dto/Request/NameDto.cs @@ -1,5 +1,4 @@ using Core.Enums; -using System.ComponentModel.DataAnnotations; namespace Core.Dto.Request { @@ -13,7 +12,7 @@ public abstract class NameDto public State? State { get; set; } public List Etymology { get; set; } public List Videos { get; set; } - public List GeoLocation { get; set; } + public List GeoLocation { get; set; } public CommaSeparatedString? FamousPeople { get; set; } @@ -33,14 +32,14 @@ public NameDto(string name, string meaning) Meaning = meaning ?? throw new ArgumentNullException(nameof(meaning)); Etymology = new List(); Videos = new List(); - GeoLocation = new List(); + GeoLocation = new List(); } public NameDto() { Etymology = new List(); Videos = new List(); - GeoLocation = new List(); + GeoLocation = new List(); } } } diff --git a/Core/Dto/Response/GeoLocationDto.cs b/Core/Dto/Response/GeoLocationDto.cs new file mode 100644 index 0000000..99441ae --- /dev/null +++ b/Core/Dto/Response/GeoLocationDto.cs @@ -0,0 +1,10 @@ +namespace Core.Dto.Response +{ + public record GeoLocationDto(string Id, string Place, string Region) + { + public override string ToString() + { + return Place; + } + } +} diff --git a/Core/Dto/Response/SuggestedNameDto.cs b/Core/Dto/Response/SuggestedNameDto.cs index 5f6927f..c1d1cc8 100644 --- a/Core/Dto/Response/SuggestedNameDto.cs +++ b/Core/Dto/Response/SuggestedNameDto.cs @@ -1,6 +1,4 @@ -using Core.Dto.Request; - -namespace Core.Dto.Response; +namespace Core.Dto.Response; public record SuggestedNameDto { diff --git a/Core/Repositories/IGeoLocationsRepository.cs b/Core/Repositories/IGeoLocationsRepository.cs index 6eca5b5..3f826ec 100644 --- a/Core/Repositories/IGeoLocationsRepository.cs +++ b/Core/Repositories/IGeoLocationsRepository.cs @@ -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 { @@ -13,5 +8,7 @@ public interface IGeoLocationsRepository Task FindByPlace(string place); Task FindByPlaceAndRegion(string region, string place); + Task Create(GeoLocation geoLocation); + Task Delete(string id, string place); } } diff --git a/Infrastructure.MongoDB/Repositories/GeoLocationsRepository.cs b/Infrastructure.MongoDB/Repositories/GeoLocationsRepository.cs index e4f6567..c50ccc5 100644 --- a/Infrastructure.MongoDB/Repositories/GeoLocationsRepository.cs +++ b/Infrastructure.MongoDB/Repositories/GeoLocationsRepository.cs @@ -5,7 +5,7 @@ namespace Infrastructure.MongoDB.Repositories { - public class GeoLocationsRepository : IGeoLocationsRepository + public class GeoLocationsRepository : MongoDBRepository, IGeoLocationsRepository { private readonly IMongoCollection _geoLocationsCollection; @@ -18,25 +18,47 @@ public GeoLocationsRepository(IMongoDatabase database) InitGeoLocation(); } } - + public async Task FindByPlace(string place) { - var filter = Builders.Filter.Eq("Place", place); - return await _geoLocationsCollection.Find(filter).SingleOrDefaultAsync(); + var filter = Builders.Filter.Eq( ge => ge.Place, place); + var options = SetCollationPrimary(new FindOptions()); + return await _geoLocationsCollection.Find(filter, options).SingleOrDefaultAsync(); } public async Task FindByPlaceAndRegion(string region, string place) { var filter = Builders.Filter.And( - Builders.Filter.Eq("Region", region.ToUpper()), - Builders.Filter.Eq("Place", place.ToUpper()) + Builders.Filter.Eq(ge => ge.Region, region), + Builders.Filter.Eq(ge => ge.Place, place) ); - return await _geoLocationsCollection.Find(filter).FirstOrDefaultAsync(); + var options = SetCollationPrimary(new FindOptions()); + return await _geoLocationsCollection.Find(filter, options).FirstOrDefaultAsync(); } - public async Task> GetAll() + public async Task> GetAll() => await _geoLocationsCollection + .Find(Builders.Filter.Empty) + .Sort(Builders.Sort.Ascending(g => g.Place)) + .ToListAsync(); + + public async Task Create(GeoLocation geoLocation) { - return await _geoLocationsCollection.Find(FilterDefinition.Empty).ToListAsync(); + geoLocation.Id = ObjectId.GenerateNewId().ToString(); + await _geoLocationsCollection.InsertOneAsync(geoLocation); + } + + public async Task Delete(string id, string place) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.Eq(g => g.Id, id), + filterBuilder.Eq(g => g.Place, place) + ); + + var options = SetCollationPrimary(new DeleteOptions()); + var deleteResult = await _geoLocationsCollection.DeleteOneAsync(filter, options); + return (int)deleteResult.DeletedCount; } private void InitGeoLocation() @@ -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", @@ -186,5 +214,6 @@ private void InitGeoLocation() } }); } + } } diff --git a/README.md b/README.md index fd63673..f528172 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Website/Pages/SubmitName.cshtml.cs b/Website/Pages/SubmitName.cshtml.cs index 35c0ba3..886918d 100644 --- a/Website/Pages/SubmitName.cshtml.cs +++ b/Website/Pages/SubmitName.cshtml.cs @@ -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; diff --git a/Website/Services/ApiService.cs b/Website/Services/ApiService.cs index 43f0ec3..e924411 100644 --- a/Website/Services/ApiService.cs +++ b/Website/Services/ApiService.cs @@ -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; diff --git a/YorubaNameDictionary.sln b/YorubaNameDictionary.sln index a2cd3e4..4da7efc 100644 --- a/YorubaNameDictionary.sln +++ b/YorubaNameDictionary.sln @@ -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