diff --git a/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanBaseDto.cs b/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanBaseDto.cs index a6d4fd56..e8404c7f 100644 --- a/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanBaseDto.cs +++ b/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanBaseDto.cs @@ -19,6 +19,11 @@ public abstract class ProcessMapPlanBaseDto : ChangeLogAbstract, IId, IWellRelat [Range(1, int.MaxValue, ErrorMessage = "Id секции скважины не может быть меньше 1")] public int IdWellSectionType { get; set; } + /// + /// Название секции + /// + public string? Section { get; set; } + /// /// Глубина по стволу от, м /// @@ -41,6 +46,6 @@ public abstract class ProcessMapPlanBaseDto : ChangeLogAbstract, IId, IWellRelat public virtual IEnumerable Validate(ValidationContext validationContext) { if(DepthEnd <= DepthStart) - yield return new ("глубина окончания должна быть больше глубины начала", new string[] {nameof(DepthEnd), nameof(DepthStart) }); + yield return new ("Глубина окончания должна быть больше глубины начала", new string[] {nameof(DepthEnd), nameof(DepthStart) }); } } \ No newline at end of file diff --git a/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanDrillingDto.cs b/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanDrillingDto.cs index 15f30300..7ab7db5d 100644 --- a/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanDrillingDto.cs +++ b/AsbCloudApp/Data/ProcessMapPlan/ProcessMapPlanDrillingDto.cs @@ -12,6 +12,11 @@ public class ProcessMapPlanDrillingDto : ProcessMapPlanBaseDto /// [Range(1, 2, ErrorMessage = "Id режима должен быть либо 1-ротор либо 2-слайд")] public int IdMode { get; set; } + + /// + /// Название режима бурения + /// + public string? Mode { get; set; } /// /// Осевая нагрузка, т план diff --git a/AsbCloudApp/Data/Trajectory/TrajectoryGeoDto.cs b/AsbCloudApp/Data/Trajectory/TrajectoryGeoDto.cs index e47a66e8..237c20b2 100644 --- a/AsbCloudApp/Data/Trajectory/TrajectoryGeoDto.cs +++ b/AsbCloudApp/Data/Trajectory/TrajectoryGeoDto.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; namespace AsbCloudApp.Data.Trajectory { /// /// Базовая географическая траектория /// - public abstract class TrajectoryGeoDto : IId + public abstract class TrajectoryGeoDto : IId, IValidatableObject { /// /// ИД строки с координатами @@ -49,5 +52,11 @@ namespace AsbCloudApp.Data.Trajectory /// ИД пользователя /// public int IdUser { get; set; } + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + return Enumerable.Empty(); + } } } diff --git a/AsbCloudApp/Services/IParserService.cs b/AsbCloudApp/Services/IParserService.cs index 2cfd8eb7..8c01af5a 100644 --- a/AsbCloudApp/Services/IParserService.cs +++ b/AsbCloudApp/Services/IParserService.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Threading; -using System.Threading.Tasks; using AsbCloudApp.Data; using AsbCloudApp.Requests.ParserOptions; @@ -10,10 +8,8 @@ namespace AsbCloudApp.Services; /// Сервис парсинга /// /// -/// -public interface IParserService : IParserService +public interface IParserService : IParserService where TDto : class, IId - where TOptions : IParserOptionsRequest { /// /// Распарсить файл @@ -21,7 +17,8 @@ public interface IParserService : IParserService /// /// /// - ParserResultDto Parse(Stream file, TOptions options); + ParserResultDto Parse(Stream file, TOptions options) + where TOptions : IParserOptionsRequest; /// /// Получение шаблона для заполнения diff --git a/AsbCloudApp/Services/ProcessMaps/IProcessMapPlanImportService.cs b/AsbCloudApp/Services/ProcessMaps/IProcessMapPlanImportService.cs index 7b30b80d..a0c3d4ec 100644 --- a/AsbCloudApp/Services/ProcessMaps/IProcessMapPlanImportService.cs +++ b/AsbCloudApp/Services/ProcessMaps/IProcessMapPlanImportService.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; using System.Threading; @@ -7,6 +8,7 @@ namespace AsbCloudApp.Services.ProcessMaps; /// /// Сервис импорта РТК /// +[Obsolete] public interface IProcessMapPlanImportService { /// diff --git a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj index cdb57ae5..ba012eb5 100644 --- a/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj +++ b/AsbCloudInfrastructure/AsbCloudInfrastructure.csproj @@ -36,6 +36,7 @@ + diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 5e1ea903..7cfc86f9 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using AsbCloudApp.Data; using AsbCloudApp.Data.DrillTestReport; using AsbCloudApp.Data.Manuals; @@ -45,6 +46,8 @@ using AsbCloudDb.Model.WellSections; using AsbCloudInfrastructure.Services.ProcessMaps; using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Requests; +using AsbCloudInfrastructure.Services.Parser; +using AsbCloudInfrastructure.Services.ProcessMapPlan.Parser; using AsbCloudInfrastructure.Services.Trajectory.Parser; namespace AsbCloudInfrastructure @@ -205,8 +208,6 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -334,8 +335,10 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); - services.AddSingleton(); - + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; } } diff --git a/AsbCloudInfrastructure/ParserServiceBase.cs b/AsbCloudInfrastructure/ParserServiceBase.cs deleted file mode 100644 index d98404e7..00000000 --- a/AsbCloudInfrastructure/ParserServiceBase.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using AsbCloudApp.Data; -using AsbCloudApp.Requests.ParserOptions; -using AsbCloudApp.Services; -using ClosedXML.Excel; - -namespace AsbCloudInfrastructure; - -public abstract class ParserServiceBase : IParserService - where TDto : class, IId - where TOptions : IParserOptionsRequest -{ - protected readonly IServiceProvider serviceProvider; - - protected ParserServiceBase(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } - - public abstract ParserResultDto Parse(Stream file, TOptions options); - public abstract Stream GetTemplateFile(); - - protected virtual ParserResultDto ParseExcelSheet(IXLWorksheet sheet, - Func> parseRow, - int columnCount, - int headerRowsCount = 0) - { - if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < columnCount) - throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов."); - - var count = sheet.RowsUsed().Count() - headerRowsCount; - - if (count > 1024) - throw new FileFormatException($"Лист {sheet.Name} содержит слишком большое количество строк."); - - if (count <= 0) - return new ParserResultDto(); - - var dtos = new List>(count); - var warnings = new List(); - - for (var i = 0; i < count; i++) - { - var row = sheet.Row(1 + i + headerRowsCount); - - try - { - var dto = parseRow.Invoke(row); - dtos.Add(dto); - } - catch (FileFormatException ex) - { - var warning = new ValidationResult(ex.Message); - warnings.Add(warning); - } - } - - var parserResult = new ParserResultDto - { - Item = dtos - }; - - if (warnings.Any()) - parserResult.Warnings = warnings; - - return parserResult; - } -} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs b/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs index f9265ba8..7a01f7df 100644 --- a/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs +++ b/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs @@ -30,7 +30,6 @@ public abstract class ChangeLogRepositoryAbstract : ICh var result = 0; if (dtos.Any()) { - using var transaction = await db.Database.BeginTransactionAsync(token); var entities = dtos.Select(Convert); var creation = DateTimeOffset.UtcNow; var dbSet = db.Set(); @@ -47,7 +46,6 @@ public abstract class ChangeLogRepositoryAbstract : ICh } result += await SaveChangesWithExceptionHandling(token); - await transaction.CommitAsync(token); } return result; } @@ -82,30 +80,39 @@ public abstract class ChangeLogRepositoryAbstract : ICh } using var transaction = db.Database.BeginTransaction(); - foreach (var entity in entitiesToDelete) + try { - entity.IdState = ChangeLogAbstract.IdStateReplaced; - entity.Obsolete = updateTime; - entity.IdEditor = idUser; - } - result += await db.SaveChangesAsync(token); + foreach (var entity in entitiesToDelete) + { + entity.IdState = ChangeLogAbstract.IdStateReplaced; + entity.Obsolete = updateTime; + entity.IdEditor = idUser; + } + result += await db.SaveChangesAsync(token); - var entitiesNew = dtos.Select(Convert); - foreach (var entity in entitiesNew) + 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; + } + catch { - 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); + await transaction.RollbackAsync(token); + throw; } - - result += await SaveChangesWithExceptionHandling(token); - await transaction.CommitAsync(token); - return result; } public async Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token) @@ -146,10 +153,19 @@ public abstract class ChangeLogRepositoryAbstract : ICh { var result = 0; using var transaction = await db.Database.BeginTransactionAsync(token); - result += await Clear(idUser, request, token); - result += await InsertRange(idUser, dtos, token); - await transaction.CommitAsync(token); - return result; + try + { + result += await Clear(idUser, request, token); + result += await InsertRange(idUser, dtos, token); + + await transaction.CommitAsync(token); + return result; + } + catch + { + await transaction.RollbackAsync(token); + throw; + } } public async Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token) diff --git a/AsbCloudInfrastructure/Services/Parser/Cell.cs b/AsbCloudInfrastructure/Services/Parser/Cell.cs new file mode 100644 index 00000000..381ca57b --- /dev/null +++ b/AsbCloudInfrastructure/Services/Parser/Cell.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.Parser; + +public class Cell +{ + private static IDictionary> converters = new Dictionary>() + { + { typeof(bool), cell => cell.GetBoolean() }, + { typeof(double), cell => cell.GetValue() }, + { typeof(float), cell => cell.GetValue() }, + { typeof(long), cell => cell.GetValue() }, + { typeof(ulong), cell => cell.GetValue() }, + { typeof(int), cell => cell.GetValue() }, + { typeof(uint), cell => cell.GetValue() }, + { typeof(short), cell => cell.GetValue() }, + { typeof(ushort), cell => cell.GetValue() }, + { typeof(string), cell => cell.GetString() }, + + { + typeof(DateTime), cell => + { + if (cell.DataType == XLDataType.DateTime) + return cell.GetDateTime(); + + var stringValue = cell.GetString(); + return DateTime.Parse(stringValue); + } + }, + + { + typeof(DateTimeOffset), cell => + { + var stringValue = cell.GetString(); + return DateTimeOffset.Parse(stringValue); + } + }, + + { + typeof(DateOnly), cell => + { + var stringValue = cell.GetString(); + return DateOnly.Parse(stringValue); + } + }, + + { + typeof(TimeOnly), cell => + { + var stringValue = cell.GetString(); + return TimeOnly.Parse(stringValue); + } + }, + + { + typeof(TimeSpan), cell => + { + if (cell.DataType == XLDataType.TimeSpan) + return cell.GetTimeSpan(); + + var stringValue = cell.GetString(); + return TimeSpan.Parse(stringValue); + } + }, + }; + + private readonly Type type; + + public Cell(int columnNumber, + Type type) + { + ColumnNumber = columnNumber; + this.type = type; + } + + public int ColumnNumber { get; } + + public object? GetValueFromCell(IXLCell cell) + { + try + { + return converters[type].Invoke(cell); + } + catch + { + var message = string.Format(XLExtentions.InvalidValueTemplate, cell.Worksheet.Name, cell.Address.RowNumber, + cell.Address.ColumnNumber); + throw new FileFormatException(message); + } + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Parser/ParserExcelService.cs b/AsbCloudInfrastructure/Services/Parser/ParserExcelService.cs new file mode 100644 index 00000000..661c729f --- /dev/null +++ b/AsbCloudInfrastructure/Services/Parser/ParserExcelService.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Reflection; +using AsbCloudApp.Data; +using AsbCloudApp.Requests.ParserOptions; +using AsbCloudApp.Services; +using ClosedXML.Excel; +using Mapster; + +namespace AsbCloudInfrastructure.Services.Parser; + +public abstract class ParserExcelService : IParserService + where TDto : class, IValidatableObject, IId +{ + protected abstract string SheetName { get; } + + protected virtual int HeaderRowsCount => 0; + + protected abstract string TemplateFileName { get; } + + protected abstract IDictionary Cells { get; } + + public virtual ParserResultDto Parse(Stream file, TOptions options) + where TOptions : IParserOptionsRequest + { + using var workbook = new XLWorkbook(file); + var sheet = workbook.GetWorksheet(SheetName); + var dtos = ParseExcelSheet(sheet); + return dtos; + } + + public virtual Stream GetTemplateFile() => + Assembly.GetExecutingAssembly().GetTemplateCopyStream(TemplateFileName) + ?? throw new ArgumentNullException($"Файл '{TemplateFileName}' не найден"); + + + protected virtual IDictionary ParseRow(IXLRow xlRow) + { + var cells = Cells.ToDictionary(x => x.Key, x => + { + var columnNumber = x.Value.ColumnNumber; + var xlCell = xlRow.Cell(columnNumber); + var cellValue = x.Value.GetValueFromCell(xlCell); + return cellValue; + }); + + return cells; + } + + protected virtual TDto BuildDto(IDictionary row, int rowNumber) + { + var dto = row.Adapt(); + return dto; + } + + private ValidationResultDto Validate(TDto dto, int rowNumber) + { + var validationResults = new List(); + + var isValid = dto.Validate(validationResults); + + if (isValid) + { + var validDto = new ValidationResultDto + { + Item = dto + }; + + return validDto; + } + + var columnsDict = Cells.ToDictionary(x => x.Key, x => x.Value.ColumnNumber); + + var invalidDto = new ValidationResultDto + { + Item = dto, + Warnings = validationResults + .SelectMany(v => v.MemberNames + .Where(columnsDict.ContainsKey) + .Select(m => + { + var columnNumber = columnsDict[m]; + var errorMessage = v.ErrorMessage; + var warningMessage = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, rowNumber, columnNumber, + errorMessage); + var warning = new ValidationResult(warningMessage, new[] { m }); + return warning; + })) + }; + + return invalidDto; + } + + protected virtual ParserResultDto ParseExcelSheet(IXLWorksheet sheet) + { + var count = sheet.RowsUsed().Count() - HeaderRowsCount; + if (count <= 0) + return new ParserResultDto(); + + var valiationResults = new List>(count); + var warnings = new List(); + + for (var i = 0; i < count; i++) + { + var xlRow = sheet.Row(1 + i + HeaderRowsCount); + var rowNumber = xlRow.RowNumber(); + + try + { + var row = ParseRow(xlRow); + var dto = BuildDto(row, rowNumber); + var validationResult = Validate(dto, rowNumber); + valiationResults.Add(validationResult); + } + catch (FileFormatException ex) + { + var warning = new ValidationResult(ex.Message); + warnings.Add(warning); + } + } + + var parserResult = new ParserResultDto + { + Item = valiationResults + }; + + if (warnings.Any()) + parserResult.Warnings = warnings; + + return parserResult; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ParserServiceFactory.cs b/AsbCloudInfrastructure/Services/ParserServiceFactory.cs deleted file mode 100644 index 5b852a8e..00000000 --- a/AsbCloudInfrastructure/Services/ParserServiceFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using AsbCloudApp.Data; -using AsbCloudApp.Requests.ParserOptions; -using AsbCloudApp.Services; -using AsbCloudInfrastructure.Services.Trajectory.Parser; - -namespace AsbCloudInfrastructure.Services; - -public class ParserServiceFactory -{ - public const int IdTrajectoryFactManualParserService = 1; - public const int IdTrajectoryPlanParserService = 2; - - private readonly IDictionary> parsers; - - public ParserServiceFactory(IServiceProvider serviceProvider) - { - parsers = new Dictionary> - { - { IdTrajectoryPlanParserService, () => new TrajectoryPlanParserService(serviceProvider) }, - { IdTrajectoryFactManualParserService, () => new TrajectoryFactManualParserService(serviceProvider) } - }; - } - - public IParserService Create(int idParserService) - where TDto : class, IId - where TOptions : IParserOptionsRequest - { - if (!parsers.TryGetValue(idParserService, out var parserService)) - throw new ArgumentNullException(nameof(idParserService), "Не правильный идентификатор парсера"); - - return parserService.Invoke() as IParserService - ?? throw new ArgumentNullException(nameof(idParserService), "Ошибка приведения типа"); - } -} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanDrillingParser.cs b/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanDrillingParser.cs new file mode 100644 index 00000000..2268b821 --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanDrillingParser.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AsbCloudApp.Data; +using AsbCloudApp.Data.ProcessMapPlan; +using AsbCloudApp.Repositories; +using AsbCloudInfrastructure.Services.Parser; +using Microsoft.Extensions.DependencyInjection; + +namespace AsbCloudInfrastructure.Services.ProcessMapPlan.Parser; + +public class ProcessMapPlanDrillingParser : ProcessMapPlanParser +{ + private readonly IEnumerable sections; + + public ProcessMapPlanDrillingParser(IWellOperationRepository wellOperationRepository) + { + sections = wellOperationRepository.GetSectionTypes(); + } + + protected override string SheetName => "План"; + protected override string TemplateFileName => "ProcessMapPlanDrillingTemplate.xlsx"; + + private const int ColumnSection = 1; + private const int ColumnMode = 2; + + protected override IDictionary Cells => new Dictionary + { + { nameof(ProcessMapPlanDrillingDto.Section), new Cell(ColumnSection, typeof(string)) }, + { nameof(ProcessMapPlanDrillingDto.Mode), new Cell(ColumnMode, typeof(string)) }, + { nameof(ProcessMapPlanDrillingDto.DepthStart), new Cell(3, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.DepthEnd), new Cell(4, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.DeltaPressurePlan), new Cell(5, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.DeltaPressureLimitMax), new Cell(6, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.AxialLoadPlan), new Cell(7, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.AxialLoadLimitMax), new Cell(8, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.TopDriveTorquePlan), new Cell(9, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.TopDriveTorqueLimitMax), new Cell(10, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.TopDriveSpeedPlan), new Cell(11, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.TopDriveSpeedLimitMax), new Cell(12, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.FlowPlan), new Cell(13, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.FlowLimitMax), new Cell(14, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.RopPlan), new Cell(15, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.UsageSaub), new Cell(16, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.UsageSpin), new Cell(17, typeof(double)) }, + { nameof(ProcessMapPlanDrillingDto.Comment), new Cell(18, typeof(string)) } + }; + + protected override ProcessMapPlanDrillingDto BuildDto(IDictionary row, int rowNumber) + { + var dto = base.BuildDto(row, rowNumber); + + var section = sections.FirstOrDefault(s => + string.Equals(s.Caption.Trim(), dto.Section?.Trim(), StringComparison.CurrentCultureIgnoreCase)); + + if (section is null) + { + var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, rowNumber, ColumnSection, + "Указана некорректная секция"); + throw new FileFormatException(message); + } + + var idMode = GetIdMode(dto.Mode); + + if (idMode is null) + { + var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, rowNumber, ColumnSection, + "Указан некорректный режим бурения"); + throw new FileFormatException(message); + } + + dto.IdWellSectionType = section.Id; + dto.IdMode = idMode.Value; + + return dto; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanParser.cs b/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanParser.cs new file mode 100644 index 00000000..496010c4 --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMapPlan/Parser/ProcessMapPlanParser.cs @@ -0,0 +1,19 @@ +using System; +using AsbCloudApp.Data.ProcessMapPlan; +using AsbCloudInfrastructure.Services.Parser; + +namespace AsbCloudInfrastructure.Services.ProcessMapPlan.Parser; + +public abstract class ProcessMapPlanParser : ParserExcelService + where TDto : ProcessMapPlanBaseDto +{ + protected override int HeaderRowsCount => 2; + + protected static int? GetIdMode(string? modeName) => + modeName?.Trim().ToLower() switch + { + "ротор" => 1, + "слайд" => 2, + _ => null + }; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMapPlan/Templates/ProcessMapPlanDrillingTemplate.xlsx b/AsbCloudInfrastructure/Services/ProcessMapPlan/Templates/ProcessMapPlanDrillingTemplate.xlsx new file mode 100644 index 00000000..bdf13143 Binary files /dev/null and b/AsbCloudInfrastructure/Services/ProcessMapPlan/Templates/ProcessMapPlanDrillingTemplate.xlsx differ diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrilling/ProcessMapPlanImportWellDrillingService.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrilling/ProcessMapPlanImportWellDrillingService.cs index f322c798..454daa7a 100644 --- a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrilling/ProcessMapPlanImportWellDrillingService.cs +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrilling/ProcessMapPlanImportWellDrillingService.cs @@ -18,6 +18,8 @@ namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrilling; /* * password for ProcessMapImportTemplate.xlsx is ASB2020! */ + +[Obsolete] public class ProcessMapPlanImportWellDrillingService : IProcessMapPlanImportService { private readonly IProcessMapPlanRepository processMapPlanWellDrillingRepository; diff --git a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParser.cs b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParser.cs new file mode 100644 index 00000000..5de4e2c5 --- /dev/null +++ b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParser.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using AsbCloudApp.Data.Trajectory; +using AsbCloudInfrastructure.Services.Parser; + +namespace AsbCloudInfrastructure.Services.Trajectory.Parser; + +public class TrajectoryFactManualParser : ParserExcelService +{ + protected override string SheetName => "Фактическая траектория"; + + protected override int HeaderRowsCount => 2; + + protected override string TemplateFileName => "TrajectoryFactManualTemplate.xlsx"; + + protected override IDictionary Cells => new Dictionary + { + { nameof(TrajectoryGeoFactDto.WellboreDepth), new Cell(1, typeof(double)) }, + { nameof(TrajectoryGeoFactDto.ZenithAngle), new Cell(2, typeof(double)) }, + { nameof(TrajectoryGeoFactDto.AzimuthGeo), new Cell(3, typeof(double)) }, + { nameof(TrajectoryGeoFactDto.AzimuthMagnetic), new Cell(4, typeof(double)) }, + { nameof(TrajectoryGeoFactDto.VerticalDepth), new Cell(5, typeof(double)) }, + { nameof(TrajectoryGeoFactDto.Comment), new Cell(6, typeof(string)) } + }; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParserService.cs b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParserService.cs deleted file mode 100644 index 6e4034a5..00000000 --- a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryFactManualParserService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using AsbCloudApp.Data; -using AsbCloudApp.Data.Trajectory; -using ClosedXML.Excel; - -namespace AsbCloudInfrastructure.Services.Trajectory.Parser; - -public class TrajectoryFactManualParserService : TrajectoryParserService -{ - protected override string SheetName => "Фактическая траектория"; - protected override string TemplateFileName => "TrajectoryFactManualTemplate.xlsx"; - - public TrajectoryFactManualParserService(IServiceProvider serviceProvider) - : base(serviceProvider) - { - } - - protected override ValidationResultDto ParseRow(IXLRow row) - { - var trajectoryRow = new TrajectoryGeoFactDto - { - WellboreDepth = row.Cell(1).GetCellValue(), - ZenithAngle = row.Cell(2).GetCellValue(), - AzimuthGeo = row.Cell(3).GetCellValue(), - AzimuthMagnetic = row.Cell(4).GetCellValue(), - VerticalDepth = row.Cell(5).GetCellValue(), - Comment = row.Cell(6).GetCellValue() - }; - - //TODO: Добавить валидацию модели - - var validationResult = new ValidationResultDto - { - Item = trajectoryRow - }; - - return validationResult; - } -} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryParserService.cs b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryParserService.cs deleted file mode 100644 index b306e602..00000000 --- a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryParserService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using AsbCloudApp.Data.Trajectory; -using ClosedXML.Excel; -using System.IO; -using System.Linq; -using System.Reflection; -using AsbCloudApp.Data; -using AsbCloudApp.Requests.ParserOptions; - -namespace AsbCloudInfrastructure.Services.Trajectory.Parser; - -public abstract class TrajectoryParserService : ParserServiceBase - where T : TrajectoryGeoDto -{ - private const int HeaderRowsCount = 2; - private const int ColumnCount = 6; - - protected TrajectoryParserService(IServiceProvider serviceProvider) - : base(serviceProvider) - { - } - - protected abstract string SheetName { get; } - - protected abstract string TemplateFileName { get; } - - protected abstract ValidationResultDto ParseRow(IXLRow row); - - public override Stream GetTemplateFile() => - Assembly.GetExecutingAssembly().GetTemplateCopyStream(TemplateFileName) - ?? throw new ArgumentNullException($"Файл '{TemplateFileName}' не найден"); - - public override ParserResultDto Parse(Stream file, IParserOptionsRequest options) - { - using var workbook = new XLWorkbook(file); - - var sheet = workbook.GetWorksheet(SheetName); - - var trajectoryRows = ParseExcelSheet(sheet, ParseRow, ColumnCount, HeaderRowsCount); - return trajectoryRows; - } -} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParser.cs b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParser.cs new file mode 100644 index 00000000..4c100766 --- /dev/null +++ b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParser.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using AsbCloudApp.Data.Trajectory; +using AsbCloudInfrastructure.Services.Parser; + +namespace AsbCloudInfrastructure.Services.Trajectory.Parser; + +public class TrajectoryPlanParser : ParserExcelService +{ + protected override string SheetName => "Плановая траектория"; + + protected override int HeaderRowsCount => 2; + + protected override string TemplateFileName => "TrajectoryPlanTemplate.xlsx"; + + protected override IDictionary Cells => new Dictionary + { + { nameof(TrajectoryGeoPlanDto.WellboreDepth), new Cell(1, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.ZenithAngle), new Cell(2, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.AzimuthGeo), new Cell(3, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.AzimuthMagnetic), new Cell(4, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.VerticalDepth), new Cell(5, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.Radius), new Cell(6, typeof(double)) }, + { nameof(TrajectoryGeoPlanDto.Comment), new Cell(7, typeof(string)) } + }; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParserService.cs b/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParserService.cs deleted file mode 100644 index fbf5a537..00000000 --- a/AsbCloudInfrastructure/Services/Trajectory/Parser/TrajectoryPlanParserService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using AsbCloudApp.Data; -using AsbCloudApp.Data.Trajectory; -using ClosedXML.Excel; - -namespace AsbCloudInfrastructure.Services.Trajectory.Parser; - -public class TrajectoryPlanParserService : TrajectoryParserService -{ - protected override string SheetName => "Плановая траектория"; - protected override string TemplateFileName => "TrajectoryPlanTemplate.xlsx"; - - public TrajectoryPlanParserService(IServiceProvider serviceProvider) - : base(serviceProvider) - { - } - - protected override ValidationResultDto ParseRow(IXLRow row) - { - var trajectoryRow = new TrajectoryGeoPlanDto - { - WellboreDepth = row.Cell(1).GetCellValue(), - ZenithAngle = row.Cell(2).GetCellValue(), - AzimuthGeo = row.Cell(3).GetCellValue(), - AzimuthMagnetic = row.Cell(4).GetCellValue(), - VerticalDepth = row.Cell(5).GetCellValue(), - Radius = row.Cell(6).GetCellValue(), - Comment = row.Cell(7).GetCellValue() - }; - - //TODO: Добавить валидацию модели - - var validationResult = new ValidationResultDto - { - Item = trajectoryRow - }; - - return validationResult; - } -} \ No newline at end of file diff --git a/AsbCloudInfrastructure/ValidationExtensions.cs b/AsbCloudInfrastructure/ValidationExtensions.cs new file mode 100644 index 00000000..feb9c06b --- /dev/null +++ b/AsbCloudInfrastructure/ValidationExtensions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace AsbCloudInfrastructure; + +public static class ValidationExtensions +{ + public static bool Validate(this IValidatableObject validatableObject, ICollection validationResults) + { + var validationContext = new ValidationContext(validatableObject, serviceProvider: null, items: null); + + foreach (var validationResult in validatableObject.Validate(validationContext)) + validationResults.Add(validationResult); + + return Validator.TryValidateObject(validatableObject, validationContext, validationResults, true); + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/XLExtentions.cs b/AsbCloudInfrastructure/XLExtentions.cs index 0aeeca00..927c90a8 100644 --- a/AsbCloudInfrastructure/XLExtentions.cs +++ b/AsbCloudInfrastructure/XLExtentions.cs @@ -7,9 +7,13 @@ namespace AsbCloudInfrastructure; public static class XLExtentions { + public const string ProblemDetailsTemplate = "Лист: {0}, Строка: {1}, Столбец: {2}. {3}"; + public const string NotFoundSheetTemplate = "Книга excel не содержит листа {0}"; + public const string InvalidValueTemplate = "Лист: {0}, Строка: {1}, Столбец: {2}. Содержит некорректное значение"; + public static IXLWorksheet GetWorksheet(this IXLWorkbook workbook, string sheetName) => workbook.Worksheets.FirstOrDefault(ws => string.Equals(ws.Name.Trim(), sheetName.Trim(), StringComparison.CurrentCultureIgnoreCase)) - ?? throw new FileFormatException($"Книга excel не содержит листа {sheetName}."); + ?? throw new FileFormatException(string.Format(NotFoundSheetTemplate, sheetName)); public static IXLCell SetCellValue(this IXLCell cell, T value) { @@ -39,8 +43,8 @@ public static class XLExtentions } catch { - throw new FileFormatException( - $"Лист '{cell.Worksheet.Name}'. {cell.Address.RowNumber} строка содержит некорректное значение в {cell.Address.ColumnNumber} столбце"); + var message = string.Format(InvalidValueTemplate, cell.Worksheet.Name, cell.Address.RowNumber, cell.Address.ColumnNumber); + throw new FileFormatException(message); } } } \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj b/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj index f20fc316..a9d7713a 100644 --- a/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj +++ b/AsbCloudWebApi.IntegrationTests/AsbCloudWebApi.IntegrationTests.csproj @@ -22,4 +22,9 @@ + + + + + diff --git a/AsbCloudWebApi.IntegrationTests/AssemblyExtensions.cs b/AsbCloudWebApi.IntegrationTests/AssemblyExtensions.cs new file mode 100644 index 00000000..d4013aa1 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/AssemblyExtensions.cs @@ -0,0 +1,25 @@ +using System.Reflection; + +namespace AsbCloudWebApi.IntegrationTests; + +internal static class AssemblyExtensions +{ + internal static Stream GetFileCopyStream(this Assembly assembly, string templateName) + { + var resourceName = assembly + .GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith(templateName)); + + if (string.IsNullOrWhiteSpace(resourceName)) + throw new ArgumentNullException(nameof(resourceName)); + + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(resourceName); + + var memoryStream = new MemoryStream(); + stream?.CopyTo(memoryStream); + memoryStream.Position = 0; + + return memoryStream; + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs b/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs index 7c8c65e3..e60f5e90 100644 --- a/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs +++ b/AsbCloudWebApi.IntegrationTests/Clients/IProcessMapPlanDrillingClient.cs @@ -1,6 +1,6 @@ -using AsbCloudApp.Data.ProcessMapPlan; +using AsbCloudApp.Data; +using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Requests; -using Microsoft.AspNetCore.Mvc; using Refit; namespace AsbCloudWebApi.IntegrationTests.Clients; @@ -32,4 +32,8 @@ public interface IProcessMapPlanDrillingClient [Put(BaseRoute)] Task> UpdateOrInsertRange(int idWell, IEnumerable dtos); + + [Multipart] + [Post(BaseRoute + "/parse")] + Task>> Parse(int idWell, [AliasAs("files")] IEnumerable streams); } diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingInvalid.xlsx b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingInvalid.xlsx new file mode 100644 index 00000000..09865489 Binary files /dev/null and b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingInvalid.xlsx differ diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingValid.xlsx b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingValid.xlsx new file mode 100644 index 00000000..b2bdd446 Binary files /dev/null and b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/Files/ProcessMapPlanDrillingValid.xlsx differ diff --git a/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/ProcessMapPlanDrillingControllerTest.cs similarity index 89% rename from AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs rename to AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/ProcessMapPlanDrillingControllerTest.cs index c0ba7093..f06e2133 100644 --- a/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlanDrillingControllerTest.cs +++ b/AsbCloudWebApi.IntegrationTests/Controllers/ProcessMapPlan/ProcessMapPlanDrillingControllerTest.cs @@ -1,31 +1,35 @@ using AsbCloudApp.Data.ProcessMapPlan; using AsbCloudApp.Requests; using AsbCloudDb.Model.ProcessMapPlan; -using AsbCloudDb.Model.ProcessMaps; using AsbCloudWebApi.IntegrationTests.Clients; using Mapster; using Microsoft.EntityFrameworkCore; using System.Net; +using System.Reflection; +using AsbCloudDb.Model.ProcessMaps; +using AsbCloudWebApi.IntegrationTests.Data; +using Refit; using Xunit; -namespace AsbCloudWebApi.IntegrationTests.Controllers; +namespace AsbCloudWebApi.IntegrationTests.Controllers.ProcessMapPlan; public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest { - private IProcessMapPlanDrillingClient client; private readonly ProcessMapPlanDrillingDto dto = new (){ Id = 0, Creation = new(), Obsolete = null, IdState = 0, IdPrevious = null, - + IdWell = 1, - IdWellSectionType = 1, + Section = "Кондуктор", + IdWellSectionType = 3, DepthStart = 0.5, DepthEnd = 1.5, IdMode = 1, + Mode = "Ротор", AxialLoadPlan = 2.718281, AxialLoadLimitMax = 3.1415926, DeltaPressurePlan = 4, @@ -73,6 +77,8 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest Comment = "это тестовая запись", }; + private IProcessMapPlanDrillingClient client; + public ProcessMapPlanDrillingControllerTest(WebAppFactoryFixture factory) : base(factory) { dbContext.CleanupDbSet(); @@ -109,6 +115,8 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest nameof(ProcessMapPlanDrillingDto.IdState), nameof(ProcessMapPlanDrillingDto.Author), nameof(ProcessMapPlanDrillingDto.Creation), + nameof(ProcessMapPlanDrillingDto.Mode), + nameof(ProcessMapPlanDrillingDto.Section) }; MatchHelper.Match(expected, actual, excludeProps); } @@ -556,4 +564,59 @@ public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest }; MatchHelper.Match(expected, actual, excludeProps); } + + [Fact] + public async Task Parse_returns_success() + { + //arrange + const string fileName = "ProcessMapPlanDrillingValid.xlsx"; + var stream = Assembly.GetExecutingAssembly().GetFileCopyStream(fileName); + + //act + var streamPart = new StreamPart(stream, fileName, "application/octet-stream"); + var response = await client.Parse(Defaults.Wells[0].Id, new[] { streamPart }); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var parserResult = response.Content; + + Assert.NotNull(parserResult); + Assert.Single(parserResult.Item); + Assert.True(parserResult.IsValid); + + var row = parserResult.Item.First(); + var dtoActual = row.Item; + + Assert.True(row.IsValid); + + var excludeProps = new[] { nameof(ProcessMapPlanDrillingDto.IdWell) }; + MatchHelper.Match(dto, dtoActual, excludeProps); + } + + [Fact] + public async Task Parse_returns_success_for_result_with_warnings() + { + //arrange + const string fileName = "ProcessMapPlanDrillingInvalid.xlsx"; + var stream = Assembly.GetExecutingAssembly().GetFileCopyStream(fileName); + + //act + var streamPart = new StreamPart(stream, fileName, "application/octet-stream"); + var response = await client.Parse(Defaults.Wells[0].Id, new[] { streamPart }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var parserResult = response.Content; + + Assert.NotNull(parserResult); + Assert.False(parserResult.IsValid); + Assert.Single(parserResult.Warnings); + Assert.Single(parserResult.Item); + + var row = parserResult.Item.First(); + + Assert.False(row.IsValid); + Assert.Equal(2, row.Warnings.Count()); + } } diff --git a/AsbCloudWebApi.IntegrationTests/Converters/ValidationResultConverter.cs b/AsbCloudWebApi.IntegrationTests/Converters/ValidationResultConverter.cs new file mode 100644 index 00000000..15423bc0 --- /dev/null +++ b/AsbCloudWebApi.IntegrationTests/Converters/ValidationResultConverter.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AsbCloudWebApi.IntegrationTests.Converters; + +public class ValidationResultConverter : JsonConverter +{ + public override ValidationResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected the start of an object."); + } + + string? errorMessage = null; + List? memberNames = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException($"Unexpected token type: {reader.TokenType}"); + } + + var propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "errorMessage": + errorMessage = reader.GetString(); + break; + case "memberNames": + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException("Expected the start of an array for 'memberNames'."); + } + memberNames = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + memberNames.Add(reader.GetString() ?? string.Empty); + } + break; + default: + reader.Skip(); + break; + } + } + + if (errorMessage == null) + { + throw new JsonException("Missing 'errorMessage' property."); + } + + return new ValidationResult(errorMessage, memberNames ?? Enumerable.Empty()); + } + + public override void Write(Utf8JsonWriter writer, ValidationResult value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs index af7303b6..aa8bd27e 100644 --- a/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs +++ b/AsbCloudWebApi.IntegrationTests/WebAppFactoryFixture.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Refit; using System.Net.Http.Headers; using System.Text.Json; +using AsbCloudWebApi.IntegrationTests.Converters; using Xunit; namespace AsbCloudWebApi.IntegrationTests; @@ -18,7 +19,8 @@ public class WebAppFactoryFixture : WebApplicationFactory, private static readonly JsonSerializerOptions jsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, + Converters = { new ValidationResultConverter() } }; private static readonly RefitSettings refitSettings = new RefitSettings(new SystemTextJsonContentSerializer(jsonSerializerOptions)); diff --git a/AsbCloudWebApi.Tests/Services/Trajectory/TrajectoryParserTest.cs b/AsbCloudWebApi.Tests/Services/Trajectory/TrajectoryParserTest.cs index 1fddd56d..8921d982 100644 --- a/AsbCloudWebApi.Tests/Services/Trajectory/TrajectoryParserTest.cs +++ b/AsbCloudWebApi.Tests/Services/Trajectory/TrajectoryParserTest.cs @@ -1,10 +1,8 @@ -using System; -using System.Linq; +using System.Linq; using AsbCloudApp.Data.Trajectory; using AsbCloudApp.Requests.ParserOptions; -using AsbCloudInfrastructure.Services; -using Microsoft.Extensions.DependencyInjection; -using NSubstitute; +using AsbCloudInfrastructure.Services.Parser; +using AsbCloudInfrastructure.Services.Trajectory.Parser; using Xunit; namespace AsbCloudWebApi.Tests.Services.Trajectory; @@ -12,21 +10,10 @@ namespace AsbCloudWebApi.Tests.Services.Trajectory; public class TrajectoryParserTest { private const string UsingTemplateFile = "AsbCloudWebApi.Tests.Services.Trajectory.Templates"; + + private readonly TrajectoryPlanParser trajectoryPlanParser = new(); + private readonly TrajectoryFactManualParser trajectoryFactManualParser = new(); - private readonly IServiceProvider serviceProviderMock = Substitute.For(); - private readonly IServiceScope serviceScopeMock = Substitute.For(); - private readonly IServiceScopeFactory serviceScopeFactoryMock = Substitute.For(); - - private readonly ParserServiceFactory parserServiceFactory; - - public TrajectoryParserTest() - { - serviceScopeFactoryMock.CreateScope().Returns(serviceScopeMock); - ((ISupportRequiredService)serviceProviderMock).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactoryMock); - - parserServiceFactory = new ParserServiceFactory(serviceProviderMock); - } - [Fact] public void Parse_trajectory_plan() { @@ -35,11 +22,8 @@ public class TrajectoryParserTest if (stream is null) Assert.Fail("Файла для импорта не существует"); - - var parserService = parserServiceFactory.Create( - ParserServiceFactory.IdTrajectoryPlanParserService); - var trajectoryRows = parserService.Parse(stream, IParserOptionsRequest.Empty()); + var trajectoryRows = trajectoryPlanParser.Parse(stream, IParserOptionsRequest.Empty()); Assert.Equal(3, trajectoryRows.Item.Count()); } @@ -53,10 +37,7 @@ public class TrajectoryParserTest if (stream is null) Assert.Fail("Файла для импорта не существует"); - var parserService = parserServiceFactory.Create( - ParserServiceFactory.IdTrajectoryFactManualParserService); - - var trajectoryRows = parserService.Parse(stream, IParserOptionsRequest.Empty()); + var trajectoryRows = trajectoryFactManualParser.Parse(stream, IParserOptionsRequest.Empty()); Assert.Equal(4, trajectoryRows.Item.Count()); } diff --git a/AsbCloudWebApi.Tests/XLExtensionsTests.cs b/AsbCloudWebApi.Tests/XLExtensionsTests.cs index 7f5fd781..601e71e0 100644 --- a/AsbCloudWebApi.Tests/XLExtensionsTests.cs +++ b/AsbCloudWebApi.Tests/XLExtensionsTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using AsbCloudInfrastructure; using ClosedXML.Excel; using Xunit; @@ -112,6 +113,16 @@ public class XLExtensionsTests Assert.Equal(DateTimeKind.Unspecified, actualValue.Kind); } + [Fact] + public void GetCellValue_returns_exception() + { + //arrange + SetCellValue("test"); + + //assert + Assert.Throws(() => GetCell(cellUsed).GetCellValue()); + } + [Fact] public void GetCellValue_returns_nullable() { diff --git a/AsbCloudWebApi/Controllers/Interfaces/IControllerWithParser.cs b/AsbCloudWebApi/Controllers/Interfaces/IControllerWithParser.cs deleted file mode 100644 index d11ac201..00000000 --- a/AsbCloudWebApi/Controllers/Interfaces/IControllerWithParser.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.IO; -using AsbCloudApp.Data; -using Microsoft.AspNetCore.Mvc; - -namespace AsbCloudWebApi.Controllers.Interfaces; - -public interface IControllerWithParser - where TDto : class, IId -{ - ActionResult> Parse(Stream file, TOptions options); - - IActionResult GetTemplate(); -} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs index c3fd0c49..9c0c8594 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanBaseController.cs @@ -9,8 +9,12 @@ using Microsoft.AspNetCore.Http; using AsbCloudApp.Exceptions; using AsbCloudApp.Requests; using System; +using System.IO; using AsbCloudApp.Services; using System.Linq; +using AsbCloudApp.Data; +using AsbCloudApp.Requests.ParserOptions; +using AsbCloudInfrastructure.Services.Parser; namespace AsbCloudWebApi.Controllers.ProcessMapPlan; @@ -21,18 +25,24 @@ namespace AsbCloudWebApi.Controllers.ProcessMapPlan; [Route("api/well/{idWell}/[controller]")] [Authorize] public abstract class ProcessMapPlanBaseController : ControllerBase - where TDto : ProcessMapPlanBaseDto + where TDto : ProcessMapPlanBaseDto { - private readonly IChangeLogRepository repository; - private readonly IWellService wellService; + private readonly IChangeLogRepository repository; + private readonly IWellService wellService; + private readonly ParserExcelService parserService; + + protected ProcessMapPlanBaseController(IChangeLogRepository repository, + IWellService wellService, + ParserExcelService parserService) + { + this.repository = repository; + this.wellService = wellService; + this.parserService = parserService; + } + + protected abstract string TemplateFileName { get; } - public ProcessMapPlanBaseController(IChangeLogRepository repository, IWellService wellService) - { - this.repository = repository; - this.wellService = wellService; - } - - /// + /// /// Добавление /// /// @@ -190,6 +200,49 @@ public abstract class ProcessMapPlanBaseController : ControllerBase var result = await repository.UpdateOrInsertRange(idUser, dtos, token); return Ok(result); } + + /// + /// Импорт РТК из excel (xlsx) файла + /// + /// + /// + /// + /// + [HttpPost("parse")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + public async Task>> Parse(int idWell, + [FromForm] IFormFileCollection files, + CancellationToken token) + { + await AssertUserHasAccessToWell(idWell, token); + + var stream = files.GetExcelFile(); + + try + { + var dto = parserService.Parse(stream, IParserOptionsRequest.Empty()); + return Ok(dto); + } + catch (FileFormatException ex) + { + return this.ValidationBadRequest(nameof(files), ex.Message); + } + } + + /// + /// Получение шаблона для заполнения РТК + /// + /// + [HttpGet("template")] + [AllowAnonymous] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK, "application/octet-stream")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public IActionResult GetTemplate() + { + var stream = parserService.GetTemplateFile(); + return File(stream, "application/octet-stream", TemplateFileName); + } /// /// returns user id, if he has access to well @@ -221,4 +274,4 @@ public abstract class ProcessMapPlanBaseController : ControllerBase var idUser = User.GetUserId() ?? throw new ForbidException("Неизвестный пользователь"); return idUser; } -} +} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs index b0a20f1e..a22676d8 100644 --- a/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs +++ b/AsbCloudWebApi/Controllers/ProcessMapPlan/ProcessMapPlanDrillingController.cs @@ -2,15 +2,18 @@ using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudApp.Services; +using AsbCloudInfrastructure.Services.ProcessMapPlan.Parser; namespace AsbCloudWebApi.Controllers.ProcessMapPlan; public class ProcessMapPlanDrillingController : ProcessMapPlanBaseController { - public ProcessMapPlanDrillingController( - IChangeLogRepository repository, - IWellService wellService) - : base(repository, wellService) - { - } -} + public ProcessMapPlanDrillingController(IChangeLogRepository repository, + IWellService wellService, + ProcessMapPlanDrillingParser parserService) + : base(repository, wellService, parserService) + { + } + + protected override string TemplateFileName => "ЕЦП_шаблон_файла_РТК_план_бурение.xlsx"; +} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryEditableController.cs b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryEditableController.cs index 7a04e7fc..344ed707 100644 --- a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryEditableController.cs +++ b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryEditableController.cs @@ -1,4 +1,5 @@ -using AsbCloudApp.Data.Trajectory; +using System; +using AsbCloudApp.Data.Trajectory; using AsbCloudApp.Repositories; using AsbCloudApp.Services; using AsbCloudInfrastructure.Services.Trajectory.Export; @@ -11,8 +12,7 @@ using System.Threading; using System.Threading.Tasks; using AsbCloudApp.Data; using AsbCloudApp.Requests.ParserOptions; -using AsbCloudInfrastructure.Services; -using AsbCloudWebApi.Controllers.Interfaces; +using AsbCloudInfrastructure.Services.Parser; namespace AsbCloudWebApi.Controllers.Trajectory { @@ -22,42 +22,23 @@ namespace AsbCloudWebApi.Controllers.Trajectory /// [ApiController] [Authorize] - public abstract class TrajectoryEditableController : TrajectoryController, - IControllerWithParser - where TDto : TrajectoryGeoDto + public abstract class TrajectoryEditableController : TrajectoryController + where TDto : TrajectoryGeoDto { - private readonly IParserService parserService; + private readonly ParserExcelService parserService; private readonly ITrajectoryEditableRepository trajectoryRepository; protected TrajectoryEditableController(IWellService wellService, - ParserServiceFactory parserServiceFactory, + ParserExcelService parserService, TrajectoryExportService trajectoryExportService, - ITrajectoryEditableRepository trajectoryRepository, - int idParserService) - : base( - wellService, - trajectoryExportService, - trajectoryRepository) - { - parserService = parserServiceFactory.Create(idParserService); + ITrajectoryEditableRepository trajectoryRepository) + : base(wellService, trajectoryExportService, trajectoryRepository) + { + this.parserService = parserService; this.trajectoryRepository = trajectoryRepository; } - - ActionResult> IControllerWithParser.Parse(Stream file, - IParserOptionsRequest options) - { - try - { - var parserResult = parserService.Parse(file, options); - return Ok(parserResult); - } - catch (FileFormatException ex) - { - return this.ValidationBadRequest("files", ex.Message); - } - } - /// + /// /// Возвращает excel шаблон для заполнения строк траектории /// /// Запрашиваемый файл @@ -93,7 +74,17 @@ namespace AsbCloudWebApi.Controllers.Trajectory if (!await CanUserAccessToWellAsync(idWell, token)) return Forbid(); - return this.ParseExcelFile(files, IParserOptionsRequest.Empty()); + var stream = files.GetExcelFile(); + + try + { + var dto = parserService.Parse(stream, IParserOptionsRequest.Empty()); + return Ok(dto); + } + catch (FileFormatException ex) + { + return this.ValidationBadRequest(nameof(files), ex.Message); + } } /// diff --git a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryFactManualController.cs b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryFactManualController.cs index 97b84b07..aa1a60dc 100644 --- a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryFactManualController.cs +++ b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryFactManualController.cs @@ -1,8 +1,8 @@ using AsbCloudApp.Data.Trajectory; using AsbCloudApp.Repositories; using AsbCloudApp.Services; -using AsbCloudInfrastructure.Services; using AsbCloudInfrastructure.Services.Trajectory.Export; +using AsbCloudInfrastructure.Services.Trajectory.Parser; using Microsoft.AspNetCore.Mvc; namespace AsbCloudWebApi.Controllers.Trajectory; @@ -18,13 +18,9 @@ public class TrajectoryFactManualController : TrajectoryEditableController trajectoryRepository) - : base(wellService, - parserServiceFactory, - trajectoryExportService, - trajectoryRepository, - ParserServiceFactory.IdTrajectoryFactManualParserService) + : base(wellService, parserService, trajectoryExportService, trajectoryRepository) { } } \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryPlanController.cs b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryPlanController.cs index cf3f648d..bffd7d15 100644 --- a/AsbCloudWebApi/Controllers/Trajectory/TrajectoryPlanController.cs +++ b/AsbCloudWebApi/Controllers/Trajectory/TrajectoryPlanController.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using AsbCloudInfrastructure.Services; +using AsbCloudInfrastructure.Services.Trajectory.Parser; namespace AsbCloudWebApi.Controllers.Trajectory { @@ -23,15 +23,11 @@ namespace AsbCloudWebApi.Controllers.Trajectory protected override string fileName => "ЕЦП_шаблон_файла_плановая_траектория.xlsx"; public TrajectoryPlanController(IWellService wellService, + TrajectoryPlanParser parserService, TrajectoryPlanExportService trajectoryExportService, - ParserServiceFactory parserServiceFactory, ITrajectoryEditableRepository trajectoryRepository, TrajectoryService trajectoryVisualizationService) - : base(wellService, - parserServiceFactory, - trajectoryExportService, - trajectoryRepository, - ParserServiceFactory.IdTrajectoryPlanParserService) + : base(wellService, parserService, trajectoryExportService, trajectoryRepository) { this.trajectoryVisualizationService = trajectoryVisualizationService; } diff --git a/AsbCloudWebApi/Extensions.cs b/AsbCloudWebApi/Extensions.cs index 489d6149..59c72e37 100644 --- a/AsbCloudWebApi/Extensions.cs +++ b/AsbCloudWebApi/Extensions.cs @@ -8,8 +8,9 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Security.Claims; using AsbCloudApp.Data; +using AsbCloudApp.Exceptions; using AsbCloudApp.Requests.ParserOptions; -using AsbCloudWebApi.Controllers.Interfaces; +using AsbCloudInfrastructure.Services.Parser; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Mvc; @@ -96,30 +97,20 @@ public static class Extensions } /// - /// Вызов парсера со стандартной валидацией входного файла + /// Получение Excel /// - /// - /// - /// /// - /// /// - public static ActionResult> ParseExcelFile( - this IControllerWithParser controller, - IFormFileCollection files, - TOptions options) - where TDto : class, IId - where TOptions : class, IParserOptionsRequest + /// + public static Stream GetExcelFile(this IFormFileCollection files) { if (files.Count < 1) - return MakeBadRequestObjectResult(nameof(files), "Нет файла"); + throw new ArgumentInvalidException(nameof(files), "Нет файла"); var file = files[0]; if (Path.GetExtension(file.FileName).ToLower() != ".xlsx") - return MakeBadRequestObjectResult(nameof(files), "Требуется .xlsx файл."); + throw new ArgumentInvalidException(nameof(files), "Требуется .xlsx файл."); - var stream = file.OpenReadStream(); - - return controller.Parse(stream, options); + return file.OpenReadStream(); } } \ No newline at end of file