Merge pull request '#24509912 - Валидация даты начала операции перед вставкой, удалением, импортом...' (#204) from fix/24509912-wellOperations-date-difference into dev

Reviewed-on: http://test.digitaldrilling.ru:8080/DDrilling/AsbCloudServer/pulls/204
This commit is contained in:
Никита Фролов 2024-01-26 10:43:11 +05:00
commit 148e111b6a
9 changed files with 391 additions and 34 deletions

View File

@ -2,6 +2,7 @@
using AsbCloudApp.Requests;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
@ -114,5 +115,20 @@ namespace AsbCloudApp.Repositories
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken);
/// <summary>
/// Валидация данных
/// </summary>
/// <param name="wellOperations"></param>
/// <returns></returns>
IEnumerable<ValidationResult> Validate(IEnumerable<WellOperationDto> wellOperations);
/// <summary>
/// Валидация данных (проверка с базой)
/// </summary>
/// <param name="wellOperations"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<ValidationResult>> ValidateWithDbAsync(IEnumerable<WellOperationDto> wellOperations, CancellationToken cancellationToken);
}
}

View File

@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -22,6 +23,8 @@ namespace AsbCloudInfrastructure.Repository;
public class WellOperationRepository : IWellOperationRepository
{
private const string KeyCacheSections = "OperationsBySectionSummarties";
private const int Gap = 90;
private readonly IAsbCloudDbContext db;
private readonly IMemoryCache memoryCache;
private readonly IWellService wellService;
@ -311,7 +314,8 @@ public class WellOperationRepository : IWellOperationRepository
{
dtos = dtos
.GroupBy(o => o.IdParent!)
.Select(g => {
.Select(g =>
{
var idCategory = g.Key ?? int.MinValue;
var category = parentRelationDictionary.GetValueOrDefault(idCategory);
var newDto = new WellGroupOpertionDto
@ -330,6 +334,66 @@ public class WellOperationRepository : IWellOperationRepository
return dtos;
}
public async Task<IEnumerable<ValidationResult>> ValidateWithDbAsync(IEnumerable<WellOperationDto> wellOperationDtos, CancellationToken token)
{
var firstOperation = wellOperationDtos
.FirstOrDefault();
if (firstOperation is null)
return Enumerable.Empty<ValidationResult>();
var request = new WellOperationRequest()
{
IdWell = firstOperation.IdWell,
OperationType = firstOperation.IdType,
};
var entities = await BuildQuery(request)
.AsNoTracking()
.ToArrayAsync(token);
var wellOperationsUnion = entities.Union(wellOperationDtos).OrderBy(o => o.DateStart);
var results = Validate(wellOperationsUnion);
return results;
}
public IEnumerable<ValidationResult> Validate(IEnumerable<WellOperationDto> wellOperationDtos)
{
var enumerator = wellOperationDtos.OrderBy(o => o.DateStart)
.GetEnumerator();
if (!enumerator.MoveNext())
yield break;
var previous = enumerator.Current;
while(enumerator.MoveNext())
{
var current = enumerator.Current;
var previousDateStart = previous.DateStart.ToUniversalTime();
var currentDateStart = current.DateStart.ToUniversalTime();
var previousDateEnd = previous.DateStart.AddHours(previous.DurationHours).ToUniversalTime();
if (previousDateStart.AddDays(Gap) < currentDateStart)
{
yield return new ValidationResult(
"Разница дат между операциями не должна превышать 90 дней",
new[] { nameof(wellOperationDtos) });
}
if (previousDateEnd > currentDateStart)
{
yield return new ValidationResult(
"Предыдущая операция не завершена",
new[] { nameof(wellOperationDtos) });
}
previous = current;
}
}
/// <inheritdoc/>
public async Task<int> InsertRangeAsync(
IEnumerable<WellOperationDto> wellOperationDtos,

View File

@ -6,7 +6,7 @@ namespace AsbCloudWebApi.IntegrationTests;
public abstract class BaseIntegrationTest : IClassFixture<WebAppFactoryFixture>
{
private readonly IServiceScope scope;
protected readonly IServiceScope scope;
protected readonly IAsbCloudDbContext dbContext;

View File

@ -0,0 +1,17 @@
using AsbCloudApp.Data;
using Microsoft.AspNetCore.Mvc;
using Refit;
namespace AsbCloudWebApi.IntegrationTests.Clients;
public interface IWellOperationClient
{
private const string BaseRoute = "/api/well/{idWell}/wellOperations";
[Post(BaseRoute + "/{idType}/{deleteBeforeInsert}")]
Task<IApiResponse<int>> InsertRangeAsync(int idWell, int idType, bool deleteBeforeInsert, [Body] IEnumerable<WellOperationDto> dtos);
[Put(BaseRoute + "/{idOperation}")]
Task<IApiResponse<int>> UpdateAsync(int idWell, int idOperation, [FromBody] WellOperationDto value, CancellationToken token);
}

View File

@ -0,0 +1,186 @@
using AsbCloudApp.Data;
using AsbCloudDb.Model;
using AsbCloudWebApi.IntegrationTests.Clients;
using System.Net;
using Xunit;
namespace AsbCloudWebApi.IntegrationTests.Controllers;
public class WellOperationControllerTest : BaseIntegrationTest
{
private static int idWell = 1;
private readonly WellOperationDto[] dtos = new WellOperationDto[]
{
new WellOperationDto()
{
Id = 2,
IdWell = idWell,
IdType = 1,
DateStart = DateTimeOffset.Now,
CategoryInfo = "1",
CategoryName = "1",
Comment = "1",
Day = 1,
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdParentCategory = null,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.Now,
NptHours = 1,
WellSectionTypeName = null,
UserName = null
}
};
private readonly WellOperationDto[] dtosWithError = new WellOperationDto[]
{
new WellOperationDto()
{
Id = 3,
IdWell = idWell,
IdType = 1,
DateStart = DateTimeOffset.Now,
CategoryInfo = "1",
CategoryName = "1",
Comment = "1",
Day = 1,
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdParentCategory = null,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.Now,
NptHours = 1,
WellSectionTypeName = null,
UserName = null
},
new WellOperationDto()
{
Id = 4,
IdWell = idWell,
IdType = 1,
DateStart = DateTimeOffset.Now.AddDays(1000),
CategoryInfo = "1",
CategoryName = "1",
Comment = "1",
Day = 1,
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdParentCategory = null,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.Now,
NptHours = 1,
WellSectionTypeName = null,
UserName = null
}
};
private IWellOperationClient wellOperationClient;
public WellOperationControllerTest(WebAppFactoryFixture factory)
: base(factory)
{
wellOperationClient = factory.GetAuthorizedHttpClient<IWellOperationClient>();
}
/// <summary>
/// Успешное добавление операций (без предварительной очистки данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRange_returns_success()
{
dbContext.CleanupDbSet<WellOperation>();
//act
var response = await wellOperationClient.InsertRangeAsync(idWell, 1, false, dtos);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное добавление операций (без предварительной очистки данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRange_returns_error()
{
//act
var response = await wellOperationClient.InsertRangeAsync(idWell, 1, false, dtosWithError);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
/// <summary>
/// Успешное добавление операций (с предварительной очисткой данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRangeWithDeleteBefore_returns_success()
{
//act
var response = await wellOperationClient.InsertRangeAsync(idWell, 1, true, dtos);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное добавление операций (с предварительной очисткой данных)
/// </summary>
/// <returns></returns>
[Fact]
public async Task InsertRangeWithDeleteBefore_returns_error()
{
//act
var response = await wellOperationClient.InsertRangeAsync(idWell, 1, true, dtosWithError);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
/// <summary>
/// Успешное обновление операции
/// </summary>
/// <returns></returns>
[Fact]
public async Task UpdateAsync_returns_success()
{
//act
var dto = dtos.FirstOrDefault()!;
var response = await wellOperationClient.UpdateAsync(idWell, 1, dto, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Неуспешное обновление операции
/// </summary>
/// <returns></returns>
[Fact]
public async Task UpdateAsync_returns_error()
{
//act
var dto = dtosWithError.LastOrDefault()!;
var response = await wellOperationClient.UpdateAsync(idWell, 1, dto, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}

View File

@ -15,6 +15,27 @@ namespace AsbCloudWebApi.IntegrationTests.Data
}
};
public static WellOperation[] WellOperations = new WellOperation[]
{
new()
{
Id = 1,
IdWell = 1,
IdType = 1,
DateStart = DateTimeOffset.UtcNow.AddDays(-1),
CategoryInfo = "1",
Comment = "1",
DepthEnd = 20,
DepthStart = 10,
DurationHours = 1,
IdCategory = 5000,
IdPlan = null,
IdUser = 1,
IdWellSectionType = 1,
LastUpdateDate = DateTimeOffset.UtcNow
}
};
public static Deposit[] Deposits = new Deposit[] {
new()
{

View File

@ -60,6 +60,7 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>,
dbContext.AddRange(Data.Defaults.RelationsCompanyWell);
dbContext.AddRange(Data.Defaults.Telemetries);
dbContext.AddRange(Data.Defaults.Drillers);
dbContext.AddRange(Data.Defaults.WellOperations);
await dbContext.SaveChangesAsync();
}

View File

@ -1,7 +1,13 @@
using AsbCloudApp.Data;
using AsbCloudApp.Data.WellOperationImport;
using AsbCloudApp.Data.WellOperationImport.Options;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudApp.Services.WellOperationImport;
using AsbCloudDb.Model;
using AsbCloudInfrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -12,12 +18,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.WellOperationImport;
using AsbCloudApp.Services.WellOperationImport;
using AsbCloudApp.Data.WellOperationImport.Options;
using AsbCloudApp.Exceptions;
using AsbCloudDb.Model;
using AsbCloudInfrastructure;
namespace AsbCloudWebApi.Controllers
{
@ -237,6 +237,10 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdUser = User.GetUserId();
wellOperation.IdType = idType;
var validationResult = await operationRepository.ValidateWithDbAsync(new[] { wellOperation }, cancellationToken);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.InsertRangeAsync(new[] { wellOperation }, cancellationToken);
return Ok(result);
@ -287,11 +291,32 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdType = idType;
}
var validationResult = await Validate(wellOperations, deleteBeforeInsert, cancellationToken);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken);
return Ok(result);
}
/// <summary>
/// Валидация данных перед вставкой / обновлением / импортом
/// </summary>
/// <param name="wellOperations"></param>
/// <param name="deleteBeforeInsert"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IEnumerable<ValidationResult>> Validate(IEnumerable<WellOperationDto> wellOperations, bool deleteBeforeInsert, CancellationToken cancellationToken)
{
if (deleteBeforeInsert)
return operationRepository.Validate(wellOperations);
else
return await operationRepository.ValidateWithDbAsync(wellOperations, cancellationToken);
}
/// <summary>
/// Обновляет выбранную операцию на скважине
/// </summary>
@ -317,6 +342,10 @@ namespace AsbCloudWebApi.Controllers
value.LastUpdateDate = DateTimeOffset.UtcNow;
value.IdUser = User.GetUserId();
var validationResult = await operationRepository.ValidateWithDbAsync(new[] { value }, token);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.UpdateAsync(value, token)
.ConfigureAwait(false);
return Ok(result);

View File

@ -3,6 +3,8 @@ using AsbCloudWebApi.Converters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Mvc
@ -53,6 +55,27 @@ namespace Microsoft.AspNetCore.Mvc
return controller.BadRequest(problem);
}
/// <summary>
/// <para>
/// Returns BadRequest with ValidationProblemDetails as body
/// </para>
/// <para>
/// Используйте этот метод только если валидацию нельзя сделать через
/// атрибуты валидации или IValidatableObject модели.
/// </para>
/// </summary>
/// <param name="controller"></param>
/// <param name="validationResults"></param>
/// <returns></returns>
public static BadRequestObjectResult ValidationBadRequest(this ControllerBase controller, IEnumerable<ValidationResult> validationResults)
{
var errors = validationResults
.SelectMany(e => e.MemberNames.Select(name=> new { name, e.ErrorMessage }))
.ToDictionary(e => e.name, e => new[] { e.ErrorMessage ?? string.Empty });
var problem = new ValidationProblemDetails(errors);
return controller.BadRequest(problem);
}
public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)
{
TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));