Merge branch 'dev' into feature/import

# Conflicts:
#	AsbCloudInfrastructure/DependencyInjection.cs
#	AsbCloudWebApi/Extentions.cs
This commit is contained in:
Степанов Дмитрий 2024-01-29 15:44:46 +05:00
commit cf399a4aa7
21 changed files with 913 additions and 374 deletions

View File

@ -1,15 +1,18 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Requests;
namespace AsbCloudApp.Repositories; namespace AsbCloudApp.Repositories;
/// <summary> /// <summary>
/// Репозиторий для записей с историей /// Репозиторий для записей с историей
/// </summary> /// </summary>
public interface IChangeLogRepository<T> public interface IChangeLogRepository<TDto, TRequest>
where T : ChangeLogAbstract where TDto : ChangeLogAbstract
where TRequest : ChangeLogBaseRequest
{ {
/// <summary> /// <summary>
/// Добавление записей /// Добавление записей
@ -18,7 +21,7 @@ public interface IChangeLogRepository<T>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> InsertRange(int idUser, IEnumerable<T> dtos, CancellationToken token); Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Редактирование записей /// Редактирование записей
@ -27,15 +30,69 @@ public interface IChangeLogRepository<T>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> UpdateRange(int idUser, IEnumerable<T> dtos, CancellationToken token); Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Добавляет Dto у которых id == 0, изменяет dto у которых id != 0
/// </summary>
/// <param name="idUser"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Добавление записей с удалением старых (для импорта)
/// </summary>
/// <param name="idUser"></param>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Clear(int idUser, TRequest request, CancellationToken token);
/// <summary>
/// Очистить и добавить новые
/// </summary>
/// <param name="idUser"></param>
/// <param name="request"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndInsertRange(int idUser, TRequest request, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Удаление записей
/// </summary>
/// <param name="idUser"></param>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(int idUser, IEnumerable<int> ids, CancellationToken token);
/// <summary>
/// Получение дат изменений записей
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(TRequest request, CancellationToken token);
/// <summary>
/// Получение журнала изменений
/// </summary>
/// <param name="request"></param>
/// <param name="date">Фильтр по дате. Если null - вернет все</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> GetChangeLog(TRequest request, DateOnly? date, CancellationToken token);
/// <summary>
/// Получение записей по параметрам
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> Get(TRequest request, CancellationToken token);
/// <summary>
/// Удаление записей
/// </summary>
/// <param name="idUser"></param>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(int idUser, IEnumerable<int> ids, CancellationToken token);
} }

View File

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Requests;
namespace AsbCloudApp.Repositories;
/// <summary>
/// Общий интерфейс для РТК план с учетом истории изменений
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IProcessMapPlanBaseRepository<T>: IChangeLogRepository<T>
where T: ProcessMapPlanBaseDto
{
/// <summary>
/// Добавление записей с удалением старых (для импорта)
/// </summary>
/// <param name="idUser"></param>
/// <param name="idWell"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndInsertRange(int idUser, int idWell, IEnumerable<T> dtos, CancellationToken token);
/// <summary>
/// Получение дат изменений записей
/// </summary>
/// <param name="idWell"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(int idWell, CancellationToken token);
/// <summary>
/// Получение журнала изменений
/// </summary>
/// <param name="idWell"></param>
/// <param name="date"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> GetChangeLog(int idWell, DateOnly? date, CancellationToken token);
/// <summary>
/// Получение записей по параметрам
/// </summary>
/// <param name="idWell"></param>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> Get(int idWell, ProcessMapPlanBaseRequest request, CancellationToken token);
}

View File

@ -4,12 +4,14 @@ using System.Threading;
using AsbCloudApp.Data.ProcessMaps; using AsbCloudApp.Data.ProcessMaps;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using System;
namespace AsbCloudApp.Repositories; namespace AsbCloudApp.Repositories;
/// <summary> /// <summary>
/// РТК план /// РТК план
/// </summary> /// </summary>
[Obsolete]
public interface IProcessMapPlanRepository<TDto> : IRepositoryWellRelated<TDto> public interface IProcessMapPlanRepository<TDto> : IRepositoryWellRelated<TDto>
where TDto : ProcessMapPlanBaseDto where TDto : ProcessMapPlanBaseDto
{ {

View File

@ -2,6 +2,7 @@
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -114,5 +115,20 @@ namespace AsbCloudApp.Repositories
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken); 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

@ -18,4 +18,37 @@ public class ProcessMapPlanBaseRequest: ChangeLogBaseRequest
/// Вернуть данные, которые поменялись с указанной даты /// Вернуть данные, которые поменялись с указанной даты
/// </summary> /// </summary>
public DateTimeOffset? UpdateFrom { get; set; } public DateTimeOffset? UpdateFrom { get; set; }
}
/// <summary>
/// Запрос для получения РТК план по скважине
/// </summary>
public class ProcessMapPlanBaseRequestWithWell: ProcessMapPlanBaseRequest
{
/// <summary>
/// Запрос для получения РТК план по скважине
/// </summary>
/// <param name="idWell"></param>
public ProcessMapPlanBaseRequestWithWell(int idWell)
{
IdWell = idWell;
}
/// <summary>
/// Запрос для получения РТК план по скважине
/// </summary>
/// <param name="request"></param>
/// <param name="idWell"></param>
public ProcessMapPlanBaseRequestWithWell(ProcessMapPlanBaseRequest request, int idWell)
{
IdWell=idWell;
IdWellSectionType=request.IdWellSectionType;
UpdateFrom = request.UpdateFrom;
Moment = request.Moment;
}
/// <summary>
/// Id скважины
/// </summary>
public int IdWell { get; set; }
} }

View File

@ -28,7 +28,7 @@ public abstract class ChangeLogAbstract
/// <summary> /// <summary>
/// Очищено при импорте /// Очищено при импорте
/// </summary> /// </summary>
public const int IdClearedOnImport = 3; public const int IdCleared = 3;
/// <summary> /// <summary>
/// Ид записи /// Ид записи

View File

@ -195,10 +195,10 @@ namespace AsbCloudInfrastructure
services.AddTransient<IOperationsStatService, OperationsStatService>(); services.AddTransient<IOperationsStatService, OperationsStatService>();
services.AddTransient<IReportService, ReportService>(); services.AddTransient<IReportService, ReportService>();
services.AddTransient<ISetpointsService, SetpointsService>(); services.AddTransient<ISetpointsService, SetpointsService>();
services.AddTransient<ITelemetryService, TelemetryService>(); services.AddScoped<ITelemetryService, TelemetryService>();
services.AddTransient<ITelemetryUserService, TelemetryUserService>(); services.AddTransient<ITelemetryUserService, TelemetryUserService>();
services.AddTransient<ITimezoneService, TimezoneService>(); services.AddTransient<ITimezoneService, TimezoneService>();
services.AddTransient<IWellService, WellService>(); services.AddScoped<IWellService, WellService>();
services.AddTransient<IWellOperationImportService, WellOperationImportService>(); services.AddTransient<IWellOperationImportService, WellOperationImportService>();
services.AddTransient<IProcessMapReportWellDrillingExportService, ProcessMapReportWellDrillingExportService>(); services.AddTransient<IProcessMapReportWellDrillingExportService, ProcessMapReportWellDrillingExportService>();
services.AddTransient<TrajectoryPlanExportService>(); services.AddTransient<TrajectoryPlanExportService>();
@ -222,7 +222,9 @@ namespace AsbCloudInfrastructure
services.AddTransient<IHelpPageService, HelpPageService>(); services.AddTransient<IHelpPageService, HelpPageService>();
services.AddTransient<IScheduleReportService, ScheduleReportService>(); services.AddTransient<IScheduleReportService, ScheduleReportService>();
services.AddTransient<IProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto>, ProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto, ProcessMapPlanDrilling>>(); services.AddTransient<
IChangeLogRepository<ProcessMapPlanDrillingDto, ProcessMapPlanBaseRequestWithWell>,
ProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto, ProcessMapPlanDrilling>>();
services.AddTransient<TrajectoryService>(); services.AddTransient<TrajectoryService>();

View File

@ -0,0 +1,279 @@
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Repository;
public abstract class ChangeLogRepositoryAbstract<TDto, TEntity, TRequest> : IChangeLogRepository<TDto, TRequest>
where TDto : AsbCloudApp.Data.ChangeLogAbstract
where TEntity : ChangeLogAbstract
where TRequest : ChangeLogBaseRequest
{
protected readonly IAsbCloudDbContext context;
public ChangeLogRepositoryAbstract(IAsbCloudDbContext context)
{
this.context = context;
}
public async Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
var result = 0;
if (dtos.Any())
{
var entities = dtos.Select(Convert);
var creation = DateTimeOffset.UtcNow;
var dbSet = context.Set<TEntity>();
foreach (var entity in entities)
{
entity.Id = default;
entity.IdAuthor = idUser;
entity.Creation = creation;
entity.IdState = ChangeLogAbstract.IdStateActual;
entity.IdEditor = null;
entity.IdPrevious = null;
entity.Obsolete = null;
dbSet.Add(entity);
}
result += await SaveChangesWithExceptionHandling(token);
}
return result;
}
public async Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
if (dtos.Any(d => d.Id == 0))
throw new ArgumentInvalidException(nameof(dtos), "Отредактированные значения должны иметь id не 0");
if (!dtos.Any())
return 0;
var updateTime = DateTimeOffset.UtcNow;
var ids = dtos.Select(d => d.Id);
using var transaction = context.Database.BeginTransaction();
var result = 0;
var dbSet = context
.Set<TEntity>();
var entitiesToDelete = await dbSet
.Where(e => e.Obsolete == null)
.Where(e => ids.Contains(e.Id))
.ToArrayAsync(token);
var entitiesNotFound = dtos.Where(d => !entitiesToDelete.Any(e => e.Id == d.Id));
if (entitiesNotFound.Any())
{
var notFoundIds = entitiesNotFound.Select(e => e.Id);
var stringnotFoundIds = string.Join(", ", notFoundIds);
throw new ArgumentInvalidException(nameof(dtos), $"записи с id:[{stringnotFoundIds}] не найдены, или не актуальны.");
}
foreach (var entity in entitiesToDelete)
{
entity.IdState = ChangeLogAbstract.IdStateReplaced;
entity.Obsolete = updateTime;
entity.IdEditor = idUser;
}
result += await context.SaveChangesAsync(token);
var entitiesNew = dtos.Select(Convert);
foreach (var entity in entitiesNew)
{
entity.IdPrevious = entity.Id;
entity.Id = default;
entity.Creation = updateTime;
entity.IdAuthor = idUser;
entity.Obsolete = null;
entity.IdEditor = null;
entity.IdState = ChangeLogAbstract.IdStateActual;
dbSet.Add(entity);
}
result += await SaveChangesWithExceptionHandling(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
var itemsToInsert = dtos.Where(e => e.Id == 0);
var itemsToUpdate = dtos.Where(e => e.Id != 0);
var result = 0;
if (itemsToInsert.Any())
result += await InsertRange(idUser, itemsToInsert, token);
if (itemsToUpdate.Any())
result += await UpdateRange(idUser, itemsToUpdate, token);
return result;
}
public async Task<int> Clear(int idUser, TRequest request, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
var query = BuildQuery(request);
query = query.Where(e => e.Obsolete == null);
var entitiesToDelete = await query.ToArrayAsync(token);
foreach (var entity in entitiesToDelete)
{
entity.IdState = ChangeLogAbstract.IdCleared;
entity.Obsolete = updateTime;
entity.IdEditor = idUser;
}
var result = await SaveChangesWithExceptionHandling(token);
return result;
}
public async Task<int> ClearAndInsertRange(int idUser, TRequest request, IEnumerable<TDto> dtos, CancellationToken token)
{
var result = 0;
var transaction = await context.Database.BeginTransactionAsync(token);
result += await Clear(idUser, request, token);
result += await InsertRange(idUser, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> DeleteRange(int idUser, IEnumerable<int> ids, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
var query = context.Set<TEntity>()
.Where(e => ids.Contains(e.Id))
.Where(e => e.Obsolete == null);
var entitiesToDelete = await query.ToArrayAsync(token);
foreach (var entity in entitiesToDelete)
{
entity.IdState = ChangeLogAbstract.IdStateDeleted;
entity.Obsolete = updateTime;
entity.IdEditor = idUser;
}
var result = await SaveChangesWithExceptionHandling(token);
return result;
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(TRequest request, CancellationToken token)
{
var query = BuildQuery(request);
var datesCreateQuery = query
.Select(e => e.Creation)
.Distinct();
var datesCreate = await datesCreateQuery.ToArrayAsync(token);
var datesUpdateQuery = query
.Where(e => e.Obsolete != null)
.Select(e => e.Obsolete!.Value)
.Distinct();
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
TimeSpan offset = GetTimezoneOffset(request);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
dates = dates.Select(date => date.ToOffset(offset));
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct();
return datesOnly;
}
public async Task<IEnumerable<TDto>> GetChangeLog(TRequest request, DateOnly? date, CancellationToken token)
{
var query = BuildQuery(request);
TimeSpan offset = GetTimezoneOffset(request);
if (date.HasValue)
{
var min = new DateTimeOffset(date.Value.Year, date.Value.Month, date.Value.Day, 0, 0, 0, offset).ToUniversalTime();
var max = min.AddDays(1);
var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);
query = createdQuery.Union(editedQuery);
}
var entities = await query.ToListAsync(token);
var dtos = entities.Select(e => Convert(e, offset));
return dtos;
}
public async Task<IEnumerable<TDto>> Get(TRequest request, CancellationToken token)
{
var query = BuildQuery(request);
var entities = await query.ToArrayAsync(token);
TimeSpan offset = GetTimezoneOffset(request);
var dtos = entities.Select(e => Convert(e, offset));
return dtos;
}
protected abstract TimeSpan GetTimezoneOffset(TRequest request);
protected abstract IQueryable<TEntity> BuildQuery(TRequest request);
protected virtual TEntity Convert(TDto dto)
{
var entity = dto.Adapt<TEntity>();
entity.Creation = entity.Creation.ToUniversalTime();
if(entity.Obsolete.HasValue)
entity.Obsolete = entity.Obsolete.Value.ToUniversalTime();
return entity;
}
protected virtual TDto Convert(TEntity entity, TimeSpan offset)
{
var dto = entity.Adapt<TDto>();
dto.Creation = entity.Creation.ToOffset(offset);
if (entity.Obsolete.HasValue)
dto.Obsolete = entity.Obsolete.Value.ToOffset(offset);
return dto;
}
private async Task<int> SaveChangesWithExceptionHandling(CancellationToken token)
{
try
{
var result = await context.SaveChangesAsync(token);
return result;
}
catch (DbUpdateException ex)
{
if (ex.InnerException is PostgresException pgException)
TryConvertPostgresExceptionToValidateException(pgException);
throw;
}
}
private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
{
if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
}
}

View File

@ -1,115 +1,36 @@
using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using AsbCloudDb.Model.ProcessMapPlan; using AsbCloudDb.Model.ProcessMapPlan;
using AsbCloudDb.Model.WellSections;
using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Repository; namespace AsbCloudInfrastructure.Repository;
public class ProcessMapPlanBaseRepository<TDto, TEntity> : IProcessMapPlanBaseRepository<TDto> public class ProcessMapPlanBaseRepository<TDto, TEntity> : ChangeLogRepositoryAbstract<TDto, TEntity, ProcessMapPlanBaseRequestWithWell>
where TDto : ProcessMapPlanBaseDto where TDto : ProcessMapPlanBaseDto
where TEntity : ProcessMapPlanBase where TEntity : ProcessMapPlanBase
{ {
private readonly IAsbCloudDbContext context;
private readonly IWellService wellService; private readonly IWellService wellService;
public ProcessMapPlanBaseRepository(IAsbCloudDbContext context, IWellService wellService) public ProcessMapPlanBaseRepository(IAsbCloudDbContext context, IWellService wellService)
: base(context)
{ {
this.context = context;
this.wellService = wellService; this.wellService = wellService;
} }
public async Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token) protected override IQueryable<TEntity> BuildQuery(ProcessMapPlanBaseRequestWithWell request)
{ {
var result = 0;
if (dtos.Any())
{
var entities = dtos.Select(Convert);
var creation = DateTimeOffset.UtcNow;
var dbSet = context.Set<TEntity>();
foreach (var entity in entities) {
entity.Id = default;
entity.IdAuthor = idUser;
entity.Creation = creation;
entity.IdState = ChangeLogAbstract.IdStateActual;
entity.IdEditor = null;
entity.Editor = null;
entity.IdPrevious = null;
entity.Obsolete = null;
dbSet.Add(entity);
}
result += await SaveChangesWithExceptionHandling(token);
}
return result;
}
public async Task<int> ClearAndInsertRange(int idUser, int idWell, IEnumerable<TDto> dtos, CancellationToken token)
{
if (dtos.Any(d => d.IdWell != idWell))
throw new ArgumentInvalidException(nameof(dtos), $"Все записи должны относиться к скважине idWell = {idWell}");
using var transaction = context.Database.BeginTransaction();
var result = 0;
var dbSet = context.Set<TEntity>();
var entitiesToMarkDeleted = dbSet
.Where(e => e.IdWell == idWell)
.Where(e => e.Obsolete == null);
var obsolete = DateTimeOffset.UtcNow;
foreach (var entity in entitiesToMarkDeleted)
{
entity.IdState = ChangeLogAbstract.IdClearedOnImport;
entity.Obsolete = obsolete;
entity.IdEditor = idUser;
}
result += await SaveChangesWithExceptionHandling(token);
result += await InsertRange(idUser, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> DeleteRange(int idUser, IEnumerable<int> ids, CancellationToken token)
{
var dbSet = context.Set<TEntity>();
var entitiesToMarkDeleted = dbSet
.Where(e => ids.Contains(e.Id))
.Where(e => e.Obsolete == null);
var obsolete = DateTimeOffset.UtcNow;
foreach (var entity in entitiesToMarkDeleted)
{
entity.IdState = ChangeLogAbstract.IdStateDeleted;
entity.Obsolete = obsolete;
entity.IdEditor = idUser;
}
var result = await SaveChangesWithExceptionHandling(token);
return result;
}
public async Task<IEnumerable<TDto>> Get(int idWell, ProcessMapPlanBaseRequest request, CancellationToken token)
{
var timezone = wellService.GetTimezone(idWell);
var offset = TimeSpan.FromHours(timezone.Hours);
var query = context var query = context
.Set<TEntity>() .Set<TEntity>()
.Include(e => e.Author) .Include(e => e.Author)
.Include(e => e.Editor) .Include(e => e.Editor)
.Where(e => e.IdWell == idWell); .Include(e => e.Well)
.Where(e => e.IdWell == request.IdWell);
if(request.IdWellSectionType.HasValue) if (request.IdWellSectionType.HasValue)
query = query.Where(e => e.IdWellSectionType == request.IdWellSectionType); query = query.Where(e => e.IdWellSectionType == request.IdWellSectionType);
if (request.UpdateFrom.HasValue) if (request.UpdateFrom.HasValue)
@ -126,156 +47,13 @@ public class ProcessMapPlanBaseRepository<TDto, TEntity> : IProcessMapPlanBaseRe
.Where(e => e.Obsolete == null || e.Obsolete >= moment); .Where(e => e.Obsolete == null || e.Obsolete >= moment);
} }
var entities = await query.ToArrayAsync(token); return query;
var dtos = entities.Select(e => Convert(e, offset));
return dtos;
} }
public async Task<IEnumerable<TDto>> GetChangeLog(int idWell, DateOnly? date, CancellationToken token) protected override TimeSpan GetTimezoneOffset(ProcessMapPlanBaseRequestWithWell request)
{ {
var query = context var timezone = wellService.GetTimezone(request.IdWell);
.Set<TEntity>()
.Include(e => e.Author)
.Include(e => e.Editor)
.Where(e => e.IdWell == idWell);
var timezone = wellService.GetTimezone(idWell);
var offset = TimeSpan.FromHours(timezone.Hours); var offset = TimeSpan.FromHours(timezone.Hours);
return offset;
if (date.HasValue)
{
var min = new DateTimeOffset(date.Value.Year, date.Value.Month, date.Value.Day, 0, 0, 0, offset).ToUniversalTime();
var max = min.AddDays(1);
var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);
query = createdQuery.Union(editedQuery);
}
var entities = await query.ToListAsync(token);
var dtos = entities.Select(e => Convert(e, offset));
return dtos;
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(int idWell, CancellationToken token)
{
var wellEntitiesQuery = context
.Set<TEntity>()
.Where(e => e.IdWell == idWell);
var datesCreateQuery = wellEntitiesQuery
.Select(e => e.Creation)
.Distinct();
var datesCreate = await datesCreateQuery.ToArrayAsync(token);
var datesUpdateQuery = wellEntitiesQuery
.Where(e => e.Obsolete != null)
.Select(e => e.Obsolete!.Value)
.Distinct();
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var timezone = wellService.GetTimezone(idWell);
var offset = TimeSpan.FromHours(timezone.Hours);
var dates = Enumerable.Concat( datesCreate, datesUpdate);
dates = dates.Select(date => date.ToOffset(offset));
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct();
return datesOnly;
}
public async Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
if (dtos.Any(d => d.Id == 0))
throw new ArgumentInvalidException(nameof(dtos), "Отредактированные значения должны иметь id больше 0");
if (!dtos.Any())
return 0;
using var transaction = context.Database.BeginTransaction();
var result = 0;
var ids = dtos.Select(d => d.Id);
var dbSet = context.Set<TEntity>();
var entitiesToDelete = dbSet
.Where(e => ids.Contains(e.Id));
var updateTime = DateTimeOffset.UtcNow;
foreach (var entity in entitiesToDelete)
{
if(entity.Obsolete is not null)
throw new ArgumentInvalidException(nameof(dtos), "Недопустимо редактировать устаревшие записи");
entity.IdState = ChangeLogAbstract.IdStateReplaced;
entity.Obsolete = updateTime;
entity.IdEditor = idUser;
}
result += await context.SaveChangesAsync(token);
var entitiesNew = dtos.Select(Convert);
foreach (var entity in entitiesNew)
{
entity.IdPrevious = entity.Id;
entity.Id = default;
entity.Creation = updateTime;
entity.IdAuthor = idUser;
entity.Obsolete = null;
entity.IdEditor = null;
entity.IdState = ChangeLogAbstract.IdStateActual;
dbSet.Add(entity);
}
result += await SaveChangesWithExceptionHandling(token);
await transaction.CommitAsync(token);
return result;
}
protected virtual TEntity Convert(TDto dto)
{
var entity = dto.Adapt<TEntity>();
entity.Creation = entity.Creation.ToUniversalTime();
if(entity.Obsolete.HasValue)
entity.Obsolete = entity.Obsolete.Value.ToUniversalTime();
return entity;
}
protected virtual TDto Convert(TEntity entity, TimeSpan offset)
{
var dto = entity.Adapt<TDto>();
dto.Creation = entity.Creation.ToOffset(offset);
if (entity.Obsolete.HasValue)
dto.Obsolete = entity.Obsolete.Value.ToOffset(offset);
return dto;
}
private async Task<int> SaveChangesWithExceptionHandling(CancellationToken token)
{
try
{
var result = await context.SaveChangesAsync(token);
return result;
}
catch (DbUpdateException ex)
{
if (ex.InnerException is PostgresException pgException)
TryConvertPostgresExceptionToValidateException(pgException);
throw;
}
}
private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
{
if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
} }
} }

View File

@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -22,6 +23,8 @@ namespace AsbCloudInfrastructure.Repository;
public class WellOperationRepository : IWellOperationRepository public class WellOperationRepository : IWellOperationRepository
{ {
private const string KeyCacheSections = "OperationsBySectionSummarties"; private const string KeyCacheSections = "OperationsBySectionSummarties";
private const int Gap = 90;
private readonly IAsbCloudDbContext db; private readonly IAsbCloudDbContext db;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private readonly IWellService wellService; private readonly IWellService wellService;
@ -50,7 +53,7 @@ public class WellOperationRepository : IWellOperationRepository
} }
var result = categories var result = categories
.OrderBy(o => o.Name) .OrderBy(o => o.Name)
.Adapt<IEnumerable<WellOperationCategoryDto>>(); .Adapt<IEnumerable<WellOperationCategoryDto>>();
return result; return result;
@ -89,14 +92,14 @@ public class WellOperationRepository : IWellOperationRepository
} }
private async Task<DateTime?> GetDateLastAssosiatedPlanOperationAsync( private async Task<DateTime?> GetDateLastAssosiatedPlanOperationAsync(
int idWell, int idWell,
DateTime? lessThenDate, DateTime? lessThenDate,
double timeZoneHours, double timeZoneHours,
CancellationToken token) CancellationToken token)
{ {
if (lessThenDate is null) if (lessThenDate is null)
return null; return null;
var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours); var currentDateOffset = lessThenDate.Value.ToUtcDateTimeOffset(timeZoneHours);
var timeZoneOffset = TimeSpan.FromHours(timeZoneHours); var timeZoneOffset = TimeSpan.FromHours(timeZoneHours);
@ -187,7 +190,7 @@ public class WellOperationRepository : IWellOperationRepository
public async Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken) public async Task<DatesRangeDto?> GetDatesRangeAsync(int idWell, int idType, CancellationToken cancellationToken)
{ {
var timezone = wellService.GetTimezone(idWell); var timezone = wellService.GetTimezone(idWell);
var query = db.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType); var query = db.WellOperations.Where(o => o.IdWell == idWell && o.IdType == idType);
if (!await query.AnyAsync(cancellationToken)) if (!await query.AnyAsync(cancellationToken))
@ -195,7 +198,7 @@ public class WellOperationRepository : IWellOperationRepository
var minDate = await query.MinAsync(o => o.DateStart, cancellationToken); var minDate = await query.MinAsync(o => o.DateStart, cancellationToken);
var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken); var maxDate = await query.MaxAsync(o => o.DateStart, cancellationToken);
return new DatesRangeDto return new DatesRangeDto
{ {
From = minDate.ToRemoteDateTime(timezone.Hours), From = minDate.ToRemoteDateTime(timezone.Hours),
@ -306,12 +309,13 @@ public class WellOperationRepository : IWellOperationRepository
DeltaDepth = g.Sum(o => o.DurationDepth), DeltaDepth = g.Sum(o => o.DurationDepth),
IdParent = parentRelationDictionary[g.Key].IdParent IdParent = parentRelationDictionary[g.Key].IdParent
}); });
while (dtos.All(x => x.IdParent != null)) while (dtos.All(x => x.IdParent != null))
{ {
dtos = dtos dtos = dtos
.GroupBy(o => o.IdParent!) .GroupBy(o => o.IdParent!)
.Select(g => { .Select(g =>
{
var idCategory = g.Key ?? int.MinValue; var idCategory = g.Key ?? int.MinValue;
var category = parentRelationDictionary.GetValueOrDefault(idCategory); var category = parentRelationDictionary.GetValueOrDefault(idCategory);
var newDto = new WellGroupOpertionDto var newDto = new WellGroupOpertionDto
@ -330,6 +334,66 @@ public class WellOperationRepository : IWellOperationRepository
return dtos; 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/> /// <inheritdoc/>
public async Task<int> InsertRangeAsync( public async Task<int> InsertRangeAsync(
IEnumerable<WellOperationDto> wellOperationDtos, IEnumerable<WellOperationDto> wellOperationDtos,

View File

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

View File

@ -18,6 +18,9 @@ public interface IProcessMapPlanDrillingClient
[Delete(BaseRoute)] [Delete(BaseRoute)]
Task<IApiResponse<int>> DeleteRange(int idWell, [Body] IEnumerable<int> ids); Task<IApiResponse<int>> DeleteRange(int idWell, [Body] IEnumerable<int> ids);
[Delete($"{BaseRoute}/clear")]
Task<IApiResponse<int>> Clear(int idWell);
[Get(BaseRoute)] [Get(BaseRoute)]
Task<IApiResponse<IEnumerable<ProcessMapPlanDrillingDto>>> Get(int idWell, ProcessMapPlanBaseRequest request); Task<IApiResponse<IEnumerable<ProcessMapPlanDrillingDto>>> Get(int idWell, ProcessMapPlanBaseRequest request);
@ -28,5 +31,5 @@ public interface IProcessMapPlanDrillingClient
Task<IApiResponse<IEnumerable<DateOnly>>> GetDatesChange(int idWell); Task<IApiResponse<IEnumerable<DateOnly>>> GetDatesChange(int idWell);
[Put(BaseRoute)] [Put(BaseRoute)]
Task<IApiResponse<int>> UpdateRangeAsync(int idWell, IEnumerable<ProcessMapPlanDrillingDto> dtos); Task<IApiResponse<int>> UpdateOrInsertRange(int idWell, IEnumerable<ProcessMapPlanDrillingDto> dtos);
} }

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

@ -179,7 +179,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
Assert.Equal(2, count); Assert.Equal(2, count);
var oldEntity = dbset.First(p => p.Id == entry.Entity.Id); var oldEntity = dbset.First(p => p.Id == entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdClearedOnImport, oldEntity.IdState); Assert.Equal(ProcessMapPlanBase.IdCleared, oldEntity.IdState);
Assert.Equal(1, oldEntity.IdEditor); Assert.Equal(1, oldEntity.IdEditor);
Assert.NotNull(oldEntity.Obsolete); Assert.NotNull(oldEntity.Obsolete);
Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime); Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime);
@ -194,44 +194,61 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
} }
[Fact] [Fact]
public async Task UpdateRange_returns_success() public async Task UpdateOrInsertRange_returns_success()
{ {
// arrange // arrange
var startTime = DateTimeOffset.UtcNow; var startTime = DateTimeOffset.UtcNow;
var dbset = dbContext.Set<ProcessMapPlanDrilling>(); var dbset = dbContext.Set<ProcessMapPlanDrilling>();
var entry = dbset.Add(entity); var entry = dbset.Add(entity);
dbContext.SaveChanges(); dbContext.SaveChanges();
entry.State = EntityState.Detached; entry.State = EntityState.Detached;
var dtoCopy = dto.Adapt<ProcessMapPlanDrillingDto>(); var dtoUpdate = dto.Adapt<ProcessMapPlanDrillingDto>();
dtoCopy.Id = entry.Entity.Id; dtoUpdate.Id = entry.Entity.Id;
dtoCopy.Comment = "nebuchadnezzar"; dtoUpdate.Comment = "nebuchadnezzar";
dtoCopy.DeltaPressureLimitMax ++; dtoUpdate.DeltaPressureLimitMax++;
dtoCopy.DeltaPressurePlan ++; dtoUpdate.DeltaPressurePlan++;
dtoCopy.FlowPlan ++; dtoUpdate.FlowPlan++;
dtoCopy.FlowLimitMax ++; dtoUpdate.FlowLimitMax++;
dtoCopy.RopPlan ++; dtoUpdate.RopPlan++;
dtoCopy.AxialLoadPlan ++; dtoUpdate.AxialLoadPlan++;
dtoCopy.AxialLoadLimitMax ++; dtoUpdate.AxialLoadLimitMax++;
dtoCopy.DepthStart ++; dtoUpdate.DepthStart++;
dtoCopy.DepthEnd ++; dtoUpdate.DepthEnd++;
dtoCopy.TopDriveSpeedPlan ++; dtoUpdate.TopDriveSpeedPlan++;
dtoCopy.TopDriveSpeedLimitMax ++; dtoUpdate.TopDriveSpeedLimitMax++;
dtoCopy.TopDriveTorquePlan ++; dtoUpdate.TopDriveTorquePlan++;
dtoCopy.TopDriveTorqueLimitMax ++; dtoUpdate.TopDriveTorqueLimitMax++;
var dtoInsert = dtoUpdate.Adapt<ProcessMapPlanDrillingDto>();
dtoInsert.Id = 0;
dtoInsert.Comment = "nebuchad";
dtoInsert.DeltaPressureLimitMax++;
dtoInsert.DeltaPressurePlan++;
dtoInsert.FlowPlan++;
dtoInsert.FlowLimitMax++;
dtoInsert.RopPlan++;
dtoInsert.AxialLoadPlan++;
dtoInsert.AxialLoadLimitMax++;
dtoInsert.DepthStart++;
dtoInsert.DepthEnd++;
dtoInsert.TopDriveSpeedPlan++;
dtoInsert.TopDriveSpeedLimitMax++;
dtoInsert.TopDriveTorquePlan++;
dtoInsert.TopDriveTorqueLimitMax++;
// act // act
var result = await client.UpdateRangeAsync(entity.IdWell, new ProcessMapPlanDrillingDto[] { dtoCopy }); var result = await client.UpdateOrInsertRange(entity.IdWell, new ProcessMapPlanDrillingDto[] { dtoUpdate, dtoInsert });
// assert // assert
var doneTime = DateTimeOffset.UtcNow; var doneTime = DateTimeOffset.UtcNow;
Assert.Equal(HttpStatusCode.OK, result.StatusCode); Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content); Assert.Equal(3, result.Content);
var count = dbset.Count(); var count = dbset.Count();
Assert.Equal(2, count); Assert.Equal(3, count);
var oldEntity = dbset.First(p => p.Id == entry.Entity.Id); var oldEntity = dbset.First(p => p.Id == entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdStateReplaced, oldEntity.IdState); Assert.Equal(ProcessMapPlanBase.IdStateReplaced, oldEntity.IdState);
@ -239,7 +256,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
Assert.NotNull(oldEntity.Obsolete); Assert.NotNull(oldEntity.Obsolete);
Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime); Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime);
var newEntity = dbset.First(p => p.Id != entry.Entity.Id); var newEntity = dbset.First(p => p.Comment == dtoUpdate.Comment);
Assert.Equal(ProcessMapPlanBase.IdStateActual, newEntity.IdState); Assert.Equal(ProcessMapPlanBase.IdStateActual, newEntity.IdState);
Assert.Equal(1, newEntity.IdAuthor); Assert.Equal(1, newEntity.IdAuthor);
Assert.Null(newEntity.IdEditor); Assert.Null(newEntity.IdEditor);
@ -247,7 +264,7 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
Assert.Equal(oldEntity.Id, newEntity.IdPrevious); Assert.Equal(oldEntity.Id, newEntity.IdPrevious);
Assert.InRange(newEntity.Creation, startTime, doneTime); Assert.InRange(newEntity.Creation, startTime, doneTime);
var expected = dtoCopy.Adapt<ProcessMapPlanDrilling>(); var expected = dtoUpdate.Adapt<ProcessMapPlanDrilling>();
var excludeProps = new[] { var excludeProps = new[] {
nameof(ProcessMapPlanDrilling.Id), nameof(ProcessMapPlanDrilling.Id),
nameof(ProcessMapPlanDrilling.Author), nameof(ProcessMapPlanDrilling.Author),
@ -289,6 +306,39 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
Assert.InRange(actual.Obsolete.Value, startTime, doneTime); Assert.InRange(actual.Obsolete.Value, startTime, doneTime);
} }
[Fact]
public async Task Clear_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
var entry = dbset.Add(entity);
dbContext.SaveChanges();
entry.State = EntityState.Detached;
var startTime = DateTimeOffset.UtcNow;
//act
var response = await client.Clear(dto.IdWell);
//assert
var doneTime = DateTimeOffset.UtcNow;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(1, response.Content);
var actual = dbContext
.Set<ProcessMapPlanDrilling>()
.FirstOrDefault(p => p.Id == entry.Entity.Id);
Assert.NotNull(actual);
Assert.Equal(ProcessMapPlanBase.IdCleared, actual.IdState);
Assert.Equal(1, actual.IdEditor);
Assert.NotNull(actual.Obsolete);
Assert.InRange(actual.Obsolete.Value, startTime, doneTime);
}
[Fact] [Fact]
public async Task GetDatesChange_returns_success() public async Task GetDatesChange_returns_success()
{ {

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

@ -14,7 +14,28 @@ namespace AsbCloudWebApi.IntegrationTests.Data
Surname = "test" Surname = "test"
} }
}; };
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[] { public static Deposit[] Deposits = new Deposit[] {
new() new()
{ {

View File

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

View File

@ -23,10 +23,10 @@ namespace AsbCloudWebApi.Controllers.ProcessMapPlan;
public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
where TDto : ProcessMapPlanBaseDto where TDto : ProcessMapPlanBaseDto
{ {
private readonly IProcessMapPlanBaseRepository<TDto> repository; private readonly IChangeLogRepository<TDto, ProcessMapPlanBaseRequestWithWell> repository;
private readonly IWellService wellService; private readonly IWellService wellService;
public ProcessMapPlanBaseController(IProcessMapPlanBaseRepository<TDto> repository, IWellService wellService) public ProcessMapPlanBaseController(IChangeLogRepository<TDto, ProcessMapPlanBaseRequestWithWell> repository, IWellService wellService)
{ {
this.repository = repository; this.repository = repository;
this.wellService = wellService; this.wellService = wellService;
@ -68,7 +68,9 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell"); return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell");
var idUser = await AssertUserHasAccessToWell(idWell, token); var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.ClearAndInsertRange(idUser, idWell, dtos, token);
var request = new ProcessMapPlanBaseRequestWithWell(idWell);
var result = await repository.ClearAndInsertRange(idUser, request, dtos, token);
return Ok(result); return Ok(result);
} }
@ -85,10 +87,29 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
public async Task<IActionResult> DeleteRange([FromRoute]int idWell, IEnumerable<int> ids, CancellationToken token) public async Task<IActionResult> DeleteRange([FromRoute]int idWell, IEnumerable<int> ids, CancellationToken token)
{ {
var idUser = await AssertUserHasAccessToWell(idWell, token); var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.DeleteRange(idUser, ids, token); var result = await repository.DeleteRange(idUser, ids, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Очистка
/// </summary>
/// <param name="idWell"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete("clear")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Clear([FromRoute] int idWell, CancellationToken token)
{
var idUser = await AssertUserHasAccessToWell(idWell, token);
var request = new ProcessMapPlanBaseRequestWithWell(idWell);
var result = await repository.Clear(idUser, request, token);
return Ok(result);
}
/// <summary> /// <summary>
/// Получение /// Получение
/// </summary> /// </summary>
@ -102,7 +123,9 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
public async Task<ActionResult<IEnumerable<TDto>>> Get([FromRoute] int idWell, [FromQuery]ProcessMapPlanBaseRequest request, CancellationToken token) public async Task<ActionResult<IEnumerable<TDto>>> Get([FromRoute] int idWell, [FromQuery]ProcessMapPlanBaseRequest request, CancellationToken token)
{ {
await AssertUserHasAccessToWell(idWell, token); await AssertUserHasAccessToWell(idWell, token);
var result = await repository.Get(idWell, request, token);
var serviceRequest = new ProcessMapPlanBaseRequestWithWell(request, idWell);
var result = await repository.Get(serviceRequest, token);
return Ok(result); return Ok(result);
} }
@ -119,7 +142,9 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
public async Task<ActionResult<IEnumerable<TDto>>> GetChangeLog([FromRoute] int idWell, [FromQuery] DateOnly? date, CancellationToken token) public async Task<ActionResult<IEnumerable<TDto>>> GetChangeLog([FromRoute] int idWell, [FromQuery] DateOnly? date, CancellationToken token)
{ {
await AssertUserHasAccessToWell(idWell, token); await AssertUserHasAccessToWell(idWell, token);
var result = await repository.GetChangeLog(idWell, date, token);
var serviceRequest = new ProcessMapPlanBaseRequestWithWell(idWell);
var result = await repository.GetChangeLog(serviceRequest, date, token);
return Ok(result); return Ok(result);
} }
@ -135,24 +160,26 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
public async Task<ActionResult<IEnumerable<DateOnly>>> GetDatesChange([FromRoute] int idWell, CancellationToken token) public async Task<ActionResult<IEnumerable<DateOnly>>> GetDatesChange([FromRoute] int idWell, CancellationToken token)
{ {
await AssertUserHasAccessToWell(idWell, token); await AssertUserHasAccessToWell(idWell, token);
var result = await repository.GetDatesChange(idWell, token);
var serviceRequest = new ProcessMapPlanBaseRequestWithWell(idWell);
var result = await repository.GetDatesChange(serviceRequest, token);
return Ok(result); return Ok(result);
} }
/// <summary> /// <summary>
/// Редактирование /// Редактирование или добавление [для пакетного редактирования]
/// </summary> /// </summary>
/// <param name="idWell"></param> /// <param name="idWell"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpPut] [HttpPut()]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)] [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdateRange([FromRoute] int idWell, IEnumerable<TDto> dtos, CancellationToken token) public async Task<IActionResult> UpdateOrInsertRange([FromRoute] int idWell, IEnumerable<TDto> dtos, CancellationToken token)
{ {
var first = dtos.FirstOrDefault(); var first = dtos.FirstOrDefault();
if(first is null) if (first is null)
return NoContent(); return NoContent();
if (idWell == 0 || dtos.Any(d => d.IdWell != idWell)) if (idWell == 0 || dtos.Any(d => d.IdWell != idWell))
@ -160,7 +187,7 @@ public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
var idUser = await AssertUserHasAccessToWell(idWell, token); var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.UpdateRange(idUser, dtos, token); var result = await repository.UpdateOrInsertRange(idUser, dtos, token);
return Ok(result); return Ok(result);
} }

View File

@ -1,12 +1,15 @@
using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
namespace AsbCloudWebApi.Controllers.ProcessMapPlan; namespace AsbCloudWebApi.Controllers.ProcessMapPlan;
public class ProcessMapPlanDrillingController : ProcessMapPlanBaseController<ProcessMapPlanDrillingDto> public class ProcessMapPlanDrillingController : ProcessMapPlanBaseController<ProcessMapPlanDrillingDto>
{ {
public ProcessMapPlanDrillingController(IProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto> repository, IWellService wellService) public ProcessMapPlanDrillingController(
IChangeLogRepository<ProcessMapPlanDrillingDto, ProcessMapPlanBaseRequestWithWell> repository,
IWellService wellService)
: base(repository, wellService) : base(repository, wellService)
{ {
} }

View File

@ -1,7 +1,13 @@
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Data.WellOperationImport;
using AsbCloudApp.Data.WellOperationImport.Options;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudApp.Services.WellOperationImport;
using AsbCloudDb.Model;
using AsbCloudInfrastructure;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -12,12 +18,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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 namespace AsbCloudWebApi.Controllers
{ {
@ -39,8 +39,8 @@ namespace AsbCloudWebApi.Controllers
private readonly IWellOperationExcelParser<WellOperationImportGazpromKhantosOptionsDto> wellOperationGazpromKhantosExcelParser; private readonly IWellOperationExcelParser<WellOperationImportGazpromKhantosOptionsDto> wellOperationGazpromKhantosExcelParser;
private readonly IUserRepository userRepository; private readonly IUserRepository userRepository;
public WellOperationController(IWellOperationRepository operationRepository, public WellOperationController(IWellOperationRepository operationRepository,
IWellService wellService, IWellService wellService,
IWellOperationImportTemplateService wellOperationImportTemplateService, IWellOperationImportTemplateService wellOperationImportTemplateService,
IWellOperationExportService wellOperationExportService, IWellOperationExportService wellOperationExportService,
IWellOperationImportService wellOperationImportService, IWellOperationImportService wellOperationImportService,
@ -231,12 +231,16 @@ namespace AsbCloudWebApi.Controllers
if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken)) if (!await CanUserEditWellOperationsAsync(idWell, cancellationToken))
return Forbid(); return Forbid();
wellOperation.IdWell = idWell; wellOperation.IdWell = idWell;
wellOperation.LastUpdateDate = DateTimeOffset.UtcNow; wellOperation.LastUpdateDate = DateTimeOffset.UtcNow;
wellOperation.IdUser = User.GetUserId(); wellOperation.IdUser = User.GetUserId();
wellOperation.IdType = idType; 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); var result = await operationRepository.InsertRangeAsync(new[] { wellOperation }, cancellationToken);
return Ok(result); return Ok(result);
@ -278,7 +282,7 @@ namespace AsbCloudWebApi.Controllers
await operationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken); await operationRepository.DeleteAsync(existingOperations.Select(o => o.Id), cancellationToken);
} }
foreach (var wellOperation in wellOperations) foreach (var wellOperation in wellOperations)
{ {
wellOperation.IdWell = idWell; wellOperation.IdWell = idWell;
@ -287,11 +291,32 @@ namespace AsbCloudWebApi.Controllers
wellOperation.IdType = idType; wellOperation.IdType = idType;
} }
var validationResult = await Validate(wellOperations, deleteBeforeInsert, cancellationToken);
if (validationResult.Any())
return this.ValidationBadRequest(validationResult);
var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken); var result = await operationRepository.InsertRangeAsync(wellOperations, cancellationToken);
return Ok(result); 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>
/// Обновляет выбранную операцию на скважине /// Обновляет выбранную операцию на скважине
/// </summary> /// </summary>
@ -308,7 +333,7 @@ namespace AsbCloudWebApi.Controllers
{ {
if (!await CanUserAccessToWellAsync(idWell, token)) if (!await CanUserAccessToWellAsync(idWell, token))
return Forbid(); return Forbid();
if (!await CanUserEditWellOperationsAsync(idWell, token)) if (!await CanUserEditWellOperationsAsync(idWell, token))
return Forbid(); return Forbid();
@ -317,6 +342,10 @@ namespace AsbCloudWebApi.Controllers
value.LastUpdateDate = DateTimeOffset.UtcNow; value.LastUpdateDate = DateTimeOffset.UtcNow;
value.IdUser = User.GetUserId(); 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) var result = await operationRepository.UpdateAsync(value, token)
.ConfigureAwait(false); .ConfigureAwait(false);
return Ok(result); return Ok(result);
@ -336,7 +365,7 @@ namespace AsbCloudWebApi.Controllers
{ {
if (!await CanUserAccessToWellAsync(idWell, token)) if (!await CanUserAccessToWellAsync(idWell, token))
return Forbid(); return Forbid();
if (!await CanUserEditWellOperationsAsync(idWell, token)) if (!await CanUserEditWellOperationsAsync(idWell, token))
return Forbid(); return Forbid();
@ -373,7 +402,7 @@ namespace AsbCloudWebApi.Controllers
deleteBeforeInsert, deleteBeforeInsert,
cancellationToken); cancellationToken);
} }
/// <summary> /// <summary>
/// Импорт плановых операций из excel (xlsx) файла. Стандартный заполненный шаблон /// Импорт плановых операций из excel (xlsx) файла. Стандартный заполненный шаблон
/// </summary> /// </summary>
@ -393,7 +422,7 @@ namespace AsbCloudWebApi.Controllers
{ {
IdType = WellOperation.IdOperationTypePlan IdType = WellOperation.IdOperationTypePlan
}; };
return ImportExcelFileAsync(idWell, files, options, return ImportExcelFileAsync(idWell, files, options,
(stream, _) => wellOperationDefaultExcelParser.Parse(stream, options), (stream, _) => wellOperationDefaultExcelParser.Parse(stream, options),
null, null,
@ -522,7 +551,7 @@ namespace AsbCloudWebApi.Controllers
return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл."); return this.ValidationBadRequest(nameof(files), "Требуется xlsx файл.");
using Stream stream = file.OpenReadStream(); using Stream stream = file.OpenReadStream();
try try
{ {
var sheet = parseMethod(stream, options); var sheet = parseMethod(stream, options);
@ -541,8 +570,8 @@ namespace AsbCloudWebApi.Controllers
//TODO: очень быстрый костыль //TODO: очень быстрый костыль
if (deleteBeforeInsert is not null && options.IdType == WellOperation.IdOperationTypeFact) if (deleteBeforeInsert is not null && options.IdType == WellOperation.IdOperationTypeFact)
{ {
return await InsertRangeAsync(idWell, options.IdType, return await InsertRangeAsync(idWell, options.IdType,
deleteBeforeInsert.Value, deleteBeforeInsert.Value,
wellOperations, wellOperations,
cancellationToken); cancellationToken);
} }
@ -554,21 +583,21 @@ namespace AsbCloudWebApi.Controllers
return this.ValidationBadRequest(nameof(files), ex.Message); return this.ValidationBadRequest(nameof(files), ex.Message);
} }
} }
private async Task<bool> CanUserAccessToWellAsync(int idWell, CancellationToken token) private async Task<bool> CanUserAccessToWellAsync(int idWell, CancellationToken token)
{ {
int? idCompany = User.GetCompanyId(); int? idCompany = User.GetCompanyId();
return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany, return idCompany is not null && await wellService.IsCompanyInvolvedInWellAsync((int)idCompany,
idWell, token).ConfigureAwait(false); idWell, token).ConfigureAwait(false);
} }
private async Task<bool> CanUserEditWellOperationsAsync(int idWell, CancellationToken token) private async Task<bool> CanUserEditWellOperationsAsync(int idWell, CancellationToken token)
{ {
var idUser = User.GetUserId(); var idUser = User.GetUserId();
if (!idUser.HasValue) if (!idUser.HasValue)
return false; return false;
var well = await wellService.GetOrDefaultAsync(idWell, token); var well = await wellService.GetOrDefaultAsync(idWell, token);
if (well is null) if (well is null)

View File

@ -4,6 +4,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Requests.Import; using AsbCloudApp.Requests.Import;
@ -58,6 +60,27 @@ namespace Microsoft.AspNetCore.Mvc
return controller.BadRequest(problem); 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) public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)
{ {
TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter))); TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));