Merge pull request '#26822781 РТК - план - история' (#199) from feature/ProcessMap-history into dev

Reviewed-on: http://test.digitaldrilling.ru:8080/DDrilling/AsbCloudServer/pulls/199
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
Reviewed-by: Степанов Дмитрий Александрович <da.stepanov@digitaldrilling.ru>
This commit is contained in:
Никита Фролов 2024-01-22 11:56:58 +05:00
commit 6045333f53
30 changed files with 11604 additions and 93 deletions

View File

@ -0,0 +1,58 @@
using System;
namespace AsbCloudApp.Data;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public abstract class ChangeLogAbstract
{
/// <summary>
/// ИД записи
/// </summary>
public int Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public int IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public int? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// ИД состояния записи:
/// <list type="table">
/// <item>
/// <term>0</term>
/// <description>актуальная запись</description>
/// </item>
/// <item>
/// <term>1</term>
/// <description>замененная запись</description>
/// </item>
/// <item>
/// <term>2</term>
/// <description>удаленная запись</description>
/// </item>
/// </list>
/// </summary>
public int IdState { get; set; }
/// <summary>
/// Id заменяемой записи
/// </summary>
public int? IdPrevious { get; set; }
}

View File

@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace AsbCloudApp.Data
{
@ -19,5 +21,66 @@ namespace AsbCloudApp.Data
[Required]
public double LimitMax { get; set; }
/// <summary>
/// Валидация
/// </summary>
/// <param name="commonRange">Общий диапазон для плана и ограничения</param>
/// <param name="paramName"></param>
/// <returns></returns>
public virtual IEnumerable<ValidationResult> Validate((double GE, double LE) commonRange, string paramName)
=> Validate(commonRange, commonRange, paramName);
/// <summary>
/// Валидация
/// </summary>
/// <param name="planRange"></param>
/// <param name="limitMaxRange"></param>
/// <param name="paramName">Название параметра для которого задается план и ограничение</param>
/// <returns></returns>
public virtual IEnumerable<ValidationResult> Validate((double GE, double LE) planRange, (double GE, double LE) limitMaxRange, string paramName)
{
if (Plan < planRange.GE || Plan > planRange.LE)
yield return new ValidationResult($"{paramName} плановое значение должно быть в диапазоне [{planRange.GE}; {planRange.LE}].");
if (Plan < planRange.GE || Plan > planRange.LE)
yield return new ValidationResult($"{paramName} ограничивающее значение должно быть в диапазоне [{limitMaxRange.GE}; {limitMaxRange.LE}].");
}
}
/// <summary>
/// Реализация RangeAttribute для PlanLimitDto
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = false)]
public class RangePlanLimitAttribute : ValidationAttribute
{
private readonly double minimum;
private readonly double maximum;
/// <summary>
/// Реализация RangeAttribute для PlanLimitDto
/// </summary>
/// <param name="minimum"></param>
/// <param name="maximum"></param>
public RangePlanLimitAttribute(double minimum, double maximum)
{
this.minimum = minimum;
this.maximum = maximum;
}
/// <inheritdoc/>
public override bool IsValid(object? value)
{
try
{
if(value is PlanLimitDto dto)
{
var isPlanValid = dto.Plan <= maximum && dto.Plan >= minimum;
var isLimitMaxValid = dto.LimitMax <= maximum && dto.LimitMax >= minimum;
return isPlanValid && isLimitMaxValid;
}
}catch{}
return false;
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace AsbCloudApp.Data.ProcessMapPlan;
/// <inheritdoc/>
public abstract class ProcessMapPlanBaseDto : ChangeLogAbstract, IId, IWellRelated, IValidatableObject
{
/// <summary>
/// Id скважины
/// </summary>
[Range(1, int.MaxValue, ErrorMessage = "Id скважины не может быть меньше 1")]
public int IdWell { get; set; }
/// <summary>
/// Тип секции
/// </summary>
[Range(1, int.MaxValue, ErrorMessage = "Id секции скважины не может быть меньше 1")]
public int IdWellSectionType { get; set; }
/// <summary>
/// Глубина по стволу от, м
/// <para>
/// на начало интервала
/// </para>
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Глубина не может быть отрицательной")]
public double DepthStart { get; set; }
/// <summary>
/// Глубина по стволу до, м
/// <para>
/// на конец интервала
/// </para>
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Глубина не может быть отрицательной")]
public double DepthEnd { get; set; }
/// <inheritdoc/>
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(DepthEnd <= DepthStart)
yield return new ("глубина окончания должна быть больше глубины начала", new string[] {nameof(DepthEnd), nameof(DepthStart) });
}
}

View File

@ -0,0 +1,98 @@
using System.ComponentModel.DataAnnotations;
namespace AsbCloudApp.Data.ProcessMapPlan;
/// <summary>
/// РТК план бурение скважины
/// </summary>
public class ProcessMapPlanDrillingDto : ProcessMapPlanBaseDto
{
/// <summary>
/// Id режима 1-ротор, 2 - слайд
/// </summary>
[Range(1, 2, ErrorMessage = "Id режима должен быть либо 1-ротор либо 2-слайд")]
public int IdMode { get; set; }
/// <summary>
/// Осевая нагрузка, т план
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Осевая нагрузка, т должна быть в пределах от 0 до 99999.9")]
public double AxialLoadPlan { get; set; }
/// <summary>
/// Осевая нагрузка, т ограничение
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Осевая нагрузка, т должна быть в пределах от 0 до 99999.9")]
public double AxialLoadLimitMax { get; set; }
/// <summary>
/// Перепад давления, атм план
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Перепад давления, атм должна быть в пределах от 0 до 99999.9")]
public double DeltaPressurePlan { get; set; }
/// <summary>
/// Перепад давления, атм ограничение
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Перепад давления, атм должна быть в пределах от 0 до 99999.9")]
public double DeltaPressureLimitMax { get; set; }
/// <summary>
/// Момент на ВСП, кН*м план
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Момент на ВСП, кН*м должна быть в пределах от 0 до 99999.9")]
public double TopDriveTorquePlan { get; set; }
/// <summary>
/// Момент на ВСП, кН*м ограничение
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Момент на ВСП, кН*м должна быть в пределах от 0 до 99999.9")]
public double TopDriveTorqueLimitMax { get; set; }
/// <summary>
/// Обороты на ВСП, об/мин план
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Обороты на ВСП, об/мин должна быть в пределах от 0 до 99999.9")]
public double TopDriveSpeedPlan { get; set; }
/// <summary>
/// Обороты на ВСП, об/мин ограничение
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Обороты на ВСП, об/мин должна быть в пределах от 0 до 99999.9")]
public double TopDriveSpeedLimitMax { get; set; }
/// <summary>
/// Расход, л/с план
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Расход, л/с должна быть в пределах от 0 до 99999.9")]
public double FlowPlan { get; set; }
/// <summary>
/// Расход, л/с ограничение
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Расход, л/с должна быть в пределах от 0 до 99999.9")]
public double FlowLimitMax { get; set; }
/// <summary>
/// Плановая механическая скорость, м/ч
/// </summary>
[Range(0, 99999.9, ErrorMessage = "Плановая механическая скорость, м/ч должно быть в пределах от 0 до 99999.9")]
public double RopPlan { get; set; }
/// <summary>
/// Плановый процент использования АКБ
/// </summary>
[Range(0, 100, ErrorMessage = "Процент использования АКБ должен быть в пределах от 0 до 100")]
public double UsageSaub { get; set; }
/// <summary>
/// Плановый процент использования spin master
/// </summary>
[Range(0, 100, ErrorMessage = "Процент использования spin master должен быть в пределах от 0 до 100")]
public double UsageSpin { get; set; }
/// <summary>
/// Комментарий
/// </summary>
public string Comment { get; set; } = string.Empty;
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
namespace AsbCloudApp.Repositories;
/// <summary>
/// Репозиторий для записей с историей
/// </summary>
public interface IChangeLogRepository<T>
where T : ChangeLogAbstract
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="idUser"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> InsertRange(int idUser, IEnumerable<T> dtos, CancellationToken token);
/// <summary>
/// Редактирование записей
/// </summary>
/// <param name="idUser"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(int idUser, IEnumerable<T> 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);
}

View File

@ -0,0 +1,51 @@
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="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> Get(ProcessMapPlanBaseRequest request, CancellationToken token);
}

View File

@ -0,0 +1,14 @@
using System;
namespace AsbCloudApp.Requests;
/// <summary>
/// Базовый запрос актуальных данных
/// </summary>
public class ChangeLogBaseRequest
{
/// <summary>
/// Дата/время на которую записи были актуальны. Если не задано, то не фильтруется
/// </summary>
public DateTimeOffset? Moment { get; set; }
}

View File

@ -0,0 +1,27 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace AsbCloudApp.Requests;
/// <summary>
/// Запрос для получения РТК план
/// </summary>
public class ProcessMapPlanBaseRequest: ChangeLogBaseRequest
{
/// <summary>
/// Идентификатор скважины
/// </summary>
[Range(1, int.MaxValue, ErrorMessage = "Id скважины - положительное число")]
public int IdWell { get; set; }
/// <summary>
/// Тип секции
/// </summary>
[Range(1, int.MaxValue, ErrorMessage = "Id секции - положительное число")]
public int? IdWellSectionType { get; set; }
/// <summary>
/// Вернуть данные, которые поменялись с указанной даты
/// </summary>
public DateTimeOffset? UpdateFrom { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,112 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AsbCloudDb.Migrations
{
public partial class Add_processmap_plan_drilling_with_changeLog : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "t_process_map_plan_drilling",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
id_mode = table.Column<int>(type: "integer", nullable: false, comment: "Id режима (1- ротор, 2 слайд)"),
axial_load_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Осевая нагрузка, т, план"),
axial_load_limit_max = table.Column<double>(type: "double precision", nullable: false, comment: "Осевая нагрузка, т, допустимый максимум"),
delta_pressure_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Перепад давления, атм, план"),
delta_pressure_limit_max = table.Column<double>(type: "double precision", nullable: false, comment: "Перепад давления, атм, допустимый максимум"),
top_drive_torque_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Момент на ВСП, план"),
top_drive_torque_limit_max = table.Column<double>(type: "double precision", nullable: false, comment: "Момент на ВСП, допустимый максимум"),
top_drive_speed_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Обороты на ВСП, план"),
top_drive_speed_limit_max = table.Column<double>(type: "double precision", nullable: false, comment: "Обороты на ВСП, допустимый максимум"),
flow_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Расход, л/с, план"),
flow_limit_max = table.Column<double>(type: "double precision", nullable: false, comment: "Расход, л/с, допустимый максимум"),
rop_plan = table.Column<double>(type: "double precision", nullable: false, comment: "Плановая механическая скорость, м/ч"),
usage_saub = table.Column<double>(type: "double precision", nullable: false, comment: "Плановый процент использования АКБ"),
usage_spin = table.Column<double>(type: "double precision", nullable: false, comment: "Плановый процент использования spin master"),
comment = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false, comment: "Комментарий"),
id_author = table.Column<int>(type: "integer", nullable: false, comment: "Автор"),
id_editor = table.Column<int>(type: "integer", nullable: true, comment: "Редактор"),
creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "дата создания"),
obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "дата устаревания"),
id_state = table.Column<int>(type: "integer", nullable: false, comment: "ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная"),
id_previous = table.Column<int>(type: "integer", nullable: true, comment: "ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная"),
id_well = table.Column<int>(type: "integer", nullable: false, comment: "Id скважины"),
id_wellsection_type = table.Column<int>(type: "integer", nullable: false, comment: "Тип секции"),
depth_start = table.Column<double>(type: "double precision", nullable: false, comment: "Глубина по стволу от, м"),
depth_end = table.Column<double>(type: "double precision", nullable: false, comment: "Глубина по стволу до, м")
},
constraints: table =>
{
table.PrimaryKey("PK_t_process_map_plan_drilling", x => x.id);
table.ForeignKey(
name: "FK_t_process_map_plan_drilling_t_process_map_plan_drilling_id_~",
column: x => x.id_previous,
principalTable: "t_process_map_plan_drilling",
principalColumn: "id");
table.ForeignKey(
name: "FK_t_process_map_plan_drilling_t_user_id_author",
column: x => x.id_author,
principalTable: "t_user",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_t_process_map_plan_drilling_t_user_id_editor",
column: x => x.id_editor,
principalTable: "t_user",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_t_process_map_plan_drilling_t_well_id_well",
column: x => x.id_well,
principalTable: "t_well",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_t_process_map_plan_drilling_t_well_section_type_id_wellsect~",
column: x => x.id_wellsection_type,
principalTable: "t_well_section_type",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
},
comment: "РТК план бурение");
migrationBuilder.CreateIndex(
name: "IX_t_process_map_plan_drilling_id_author",
table: "t_process_map_plan_drilling",
column: "id_author");
migrationBuilder.CreateIndex(
name: "IX_t_process_map_plan_drilling_id_editor",
table: "t_process_map_plan_drilling",
column: "id_editor");
migrationBuilder.CreateIndex(
name: "IX_t_process_map_plan_drilling_id_previous",
table: "t_process_map_plan_drilling",
column: "id_previous");
migrationBuilder.CreateIndex(
name: "IX_t_process_map_plan_drilling_id_well",
table: "t_process_map_plan_drilling",
column: "id_well");
migrationBuilder.CreateIndex(
name: "IX_t_process_map_plan_drilling_id_wellsection_type",
table: "t_process_map_plan_drilling",
column: "id_wellsection_type");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "t_process_map_plan_drilling");
}
}
}

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System.Collections.Generic;
#nullable disable
@ -20,7 +19,7 @@ namespace AsbCloudDb.Migrations
#pragma warning disable 612, 618
modelBuilder
.UseCollation("Russian_Russia.1251")
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("ProductVersion", "6.0.22")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
@ -388,7 +387,7 @@ namespace AsbCloudDb.Migrations
.HasColumnName("enabled_subsystems")
.HasComment("флаги включенных подсистем");
b.Property<IDictionary<string, object>>("ExtraData")
b.Property<string>("ExtraData")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("extra_data")
@ -2464,6 +2463,160 @@ namespace AsbCloudDb.Migrations
});
});
modelBuilder.Entity("AsbCloudDb.Model.ProcessMaps.ProcessMapPlanDrilling", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("Идентификатор");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double>("AxialLoadLimitMax")
.HasColumnType("double precision")
.HasColumnName("axial_load_limit_max")
.HasComment("Осевая нагрузка, т, допустимый максимум");
b.Property<double>("AxialLoadPlan")
.HasColumnType("double precision")
.HasColumnName("axial_load_plan")
.HasComment("Осевая нагрузка, т, план");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("comment")
.HasComment("Комментарий");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("creation")
.HasComment("дата создания");
b.Property<double>("DeltaPressureLimitMax")
.HasColumnType("double precision")
.HasColumnName("delta_pressure_limit_max")
.HasComment("Перепад давления, атм, допустимый максимум");
b.Property<double>("DeltaPressurePlan")
.HasColumnType("double precision")
.HasColumnName("delta_pressure_plan")
.HasComment("Перепад давления, атм, план");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasColumnName("depth_end")
.HasComment("Глубина по стволу до, м");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasColumnName("depth_start")
.HasComment("Глубина по стволу от, м");
b.Property<double>("FlowLimitMax")
.HasColumnType("double precision")
.HasColumnName("flow_limit_max")
.HasComment("Расход, л/с, допустимый максимум");
b.Property<double>("FlowPlan")
.HasColumnType("double precision")
.HasColumnName("flow_plan")
.HasComment("Расход, л/с, план");
b.Property<int>("IdAuthor")
.HasColumnType("integer")
.HasColumnName("id_author")
.HasComment("Автор");
b.Property<int?>("IdEditor")
.HasColumnType("integer")
.HasColumnName("id_editor")
.HasComment("Редактор");
b.Property<int>("IdMode")
.HasColumnType("integer")
.HasColumnName("id_mode")
.HasComment("Id режима (1- ротор, 2 слайд)");
b.Property<int?>("IdPrevious")
.HasColumnType("integer")
.HasColumnName("id_previous")
.HasComment("ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная");
b.Property<int>("IdState")
.HasColumnType("integer")
.HasColumnName("id_state")
.HasComment("ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная");
b.Property<int>("IdWell")
.HasColumnType("integer")
.HasColumnName("id_well")
.HasComment("Id скважины");
b.Property<int>("IdWellSectionType")
.HasColumnType("integer")
.HasColumnName("id_wellsection_type")
.HasComment("Тип секции");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("obsolete")
.HasComment("дата устаревания");
b.Property<double>("RopPlan")
.HasColumnType("double precision")
.HasColumnName("rop_plan")
.HasComment("Плановая механическая скорость, м/ч");
b.Property<double>("TopDriveSpeedLimitMax")
.HasColumnType("double precision")
.HasColumnName("top_drive_speed_limit_max")
.HasComment("Обороты на ВСП, допустимый максимум");
b.Property<double>("TopDriveSpeedPlan")
.HasColumnType("double precision")
.HasColumnName("top_drive_speed_plan")
.HasComment("Обороты на ВСП, план");
b.Property<double>("TopDriveTorqueLimitMax")
.HasColumnType("double precision")
.HasColumnName("top_drive_torque_limit_max")
.HasComment("Момент на ВСП, допустимый максимум");
b.Property<double>("TopDriveTorquePlan")
.HasColumnType("double precision")
.HasColumnName("top_drive_torque_plan")
.HasComment("Момент на ВСП, план");
b.Property<double>("UsageSaub")
.HasColumnType("double precision")
.HasColumnName("usage_saub")
.HasComment("Плановый процент использования АКБ");
b.Property<double>("UsageSpin")
.HasColumnType("double precision")
.HasColumnName("usage_spin")
.HasComment("Плановый процент использования spin master");
b.HasKey("Id");
b.HasIndex("IdAuthor");
b.HasIndex("IdEditor");
b.HasIndex("IdPrevious");
b.HasIndex("IdWell");
b.HasIndex("IdWellSectionType");
b.ToTable("t_process_map_plan_drilling");
b.HasComment("РТК план бурение");
});
modelBuilder.Entity("AsbCloudDb.Model.ProcessMaps.ProcessMapWellDrilling", b =>
{
b.Property<int>("Id")
@ -8400,6 +8553,46 @@ namespace AsbCloudDb.Migrations
b.Navigation("Well");
});
modelBuilder.Entity("AsbCloudDb.Model.ProcessMaps.ProcessMapPlanDrilling", b =>
{
b.HasOne("AsbCloudDb.Model.User", "Author")
.WithMany()
.HasForeignKey("IdAuthor")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("AsbCloudDb.Model.User", "Editor")
.WithMany()
.HasForeignKey("IdEditor")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("AsbCloudDb.Model.ProcessMaps.ProcessMapPlanDrilling", "Previous")
.WithMany()
.HasForeignKey("IdPrevious");
b.HasOne("AsbCloudDb.Model.Well", "Well")
.WithMany()
.HasForeignKey("IdWell")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AsbCloudDb.Model.WellSectionType", "WellSectionType")
.WithMany()
.HasForeignKey("IdWellSectionType")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
b.Navigation("Editor");
b.Navigation("Previous");
b.Navigation("Well");
b.Navigation("WellSectionType");
});
modelBuilder.Entity("AsbCloudDb.Model.ProcessMaps.ProcessMapWellDrilling", b =>
{
b.HasOne("AsbCloudDb.Model.User", "User")

View File

@ -21,6 +21,7 @@ namespace AsbCloudDb.Model
public virtual DbSet<TrajectoryPlan> TrajectoriesPlan => Set<TrajectoryPlan>();
public virtual DbSet<ProcessMapWellDrilling> ProcessMapWellDrillings => Set<ProcessMapWellDrilling>();
public virtual DbSet<ProcessMapWellReam> ProcessMapWellReams => Set<ProcessMapWellReam>();
public virtual DbSet<ProcessMapPlanDrilling> ProcessMapPlanDrilling => Set<ProcessMapPlanDrilling>();
public virtual DbSet<DrillingProgramPart> DrillingProgramParts => Set<DrillingProgramPart>();
public virtual DbSet<FileCategory> FileCategories => Set<FileCategory>();
public virtual DbSet<FileInfo> Files => Set<FileInfo>();
@ -440,6 +441,16 @@ namespace AsbCloudDb.Model
.HasIndex(w => new { w.IdWell, w.IdSectionType })
.IsUnique();
modelBuilder.Entity<ProcessMapPlanDrilling>()
.HasOne(p => p.Author)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ProcessMapPlanDrilling>()
.HasOne(p => p.Editor)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
DefaultData.DefaultContextData.Fill(modelBuilder);
}

View File

@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AsbCloudDb.Model;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public abstract class ChangeLogAbstract
{
/// <summary>
/// Актуальная
/// </summary>
public const int IdStateActual = 0;
/// <summary>
/// Замененная
/// </summary>
public const int IdStateReplaced = 1;
/// <summary>
/// Удаленная
/// </summary>
public const int IdStateDeleted = 2;
/// <summary>
/// Очищено при импорте
/// </summary>
public const int IdClearedOnImport = 3;
/// <summary>
/// Ид записи
/// </summary>
[Key]
[Column("id"), Comment("Идентификатор")]
public int Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
[Column("id_author"), Comment("Автор")]
public int IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
[Column("id_editor"), Comment("Редактор")]
public int? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
[Column("creation"), Comment("дата создания")]
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
[Column("obsolete"), Comment("дата устаревания")]
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// ИД состояния записи:
/// <list type="table">
/// <item>
/// <term>0</term>
/// <description>актуальная запись</description>
/// </item>
/// <item>
/// <term>1</term>
/// <description>замененная запись</description>
/// </item>
/// <item>
/// <term>2</term>
/// <description>удаленная запись</description>
/// </item>
/// </list>
/// </summary>
[Column("id_state"), Comment("ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная")]
public int IdState { get; set; }
/// <summary>
/// Id заменяемой записи
/// </summary>
[Column("id_previous"), Comment("ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная")]
public int? IdPrevious { get; set; }
}

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace AsbCloudDb.Model.ProcessMapPlan;
public abstract class ProcessMapPlanBase : ChangeLogAbstract, IId, IWellRelated
{
[Column("id_well"), Comment("Id скважины")]
public int IdWell { get; set; }
[Column("id_wellsection_type"), Comment("Тип секции")]
public int IdWellSectionType { get; set; }
[Column("depth_start"), Comment("Глубина по стволу от, м")]
public double DepthStart { get; set; }
[Column("depth_end"), Comment("Глубина по стволу до, м")]
public double DepthEnd { get; set; }
[ForeignKey(nameof(IdWell))]
public virtual Well Well { get; set; } = null!;
[ForeignKey(nameof(IdAuthor))]
public virtual User Author { get; set; } = null!;
[ForeignKey(nameof(IdEditor))]
public virtual User? Editor { get; set; } = null!;
[ForeignKey(nameof(IdWellSectionType))]
public virtual WellSectionType WellSectionType { get; set; } = null!;
}

View File

@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using AsbCloudDb.Model.ProcessMapPlan;
using Microsoft.EntityFrameworkCore;
namespace AsbCloudDb.Model.ProcessMaps;
[Table("t_process_map_plan_drilling"), Comment("РТК план бурение")]
public class ProcessMapPlanDrilling : ProcessMapPlanBase
{
[Column("id_mode"), Comment("Id режима (1- ротор, 2 слайд)")]
public int IdMode { get; set; }
[Column("axial_load_plan"), Comment("Осевая нагрузка, т, план")]
public double AxialLoadPlan { get; set; }
[Column("axial_load_limit_max"), Comment("Осевая нагрузка, т, допустимый максимум")]
public double AxialLoadLimitMax { get; set; }
[Column("delta_pressure_plan"), Comment("Перепад давления, атм, план")]
public double DeltaPressurePlan { get; set; }
[Column("delta_pressure_limit_max"), Comment("Перепад давления, атм, допустимый максимум")]
public double DeltaPressureLimitMax { get; set; }
[Column("top_drive_torque_plan"), Comment("Момент на ВСП, план")]
public double TopDriveTorquePlan { get; set; }
[Column("top_drive_torque_limit_max"), Comment("Момент на ВСП, допустимый максимум")]
public double TopDriveTorqueLimitMax { get; set; }
[Column("top_drive_speed_plan"), Comment("Обороты на ВСП, план")]
public double TopDriveSpeedPlan { get; set; }
[Column("top_drive_speed_limit_max"), Comment("Обороты на ВСП, допустимый максимум")]
public double TopDriveSpeedLimitMax { get; set; }
[Column("flow_plan"), Comment("Расход, л/с, план")]
public double FlowPlan { get; set; }
[Column("flow_limit_max"), Comment("Расход, л/с, допустимый максимум")]
public double FlowLimitMax { get; set; }
[Column("rop_plan"), Comment("Плановая механическая скорость, м/ч")]
public double RopPlan { get; set; }
[Column("usage_saub"), Comment("Плановый процент использования АКБ")]
public double UsageSaub { get; set; }
[Column("usage_spin"), Comment("Плановый процент использования spin master")]
public double UsageSpin { get; set; }
[Column("comment"), Comment("Комментарий"), StringLength(1024)]
public string Comment { get; set; } = string.Empty;
[ForeignKey(nameof(IdPrevious))]
public virtual ProcessMapPlanDrilling? Previous { get; set; }
}

View File

@ -44,10 +44,10 @@ using AsbCloudApp.Services.DailyReport;
using AsbCloudDb.Model.DailyReports.Blocks.TimeBalance;
using AsbCloudDb.Model.WellSections;
using AsbCloudInfrastructure.Services.ProcessMaps;
using AsbCloudApp.Data.ProcessMapPlan;
namespace AsbCloudInfrastructure
{
public static class DependencyInjection
{
public static void MapsterSetup()
@ -222,6 +222,8 @@ namespace AsbCloudInfrastructure
services.AddTransient<IHelpPageService, HelpPageService>();
services.AddTransient<IScheduleReportService, ScheduleReportService>();
services.AddTransient<IProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto>, ProcessMapPlanBaseRepository<ProcessMapPlanDrillingDto, ProcessMapPlanDrilling>>();
services.AddTransient<TrajectoryService>();
services.AddTransient<IGtrRepository, GtrWitsRepository>();

View File

@ -0,0 +1,277 @@
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudDb.Model.ProcessMapPlan;
using AsbCloudDb.Model.WellSections;
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 class ProcessMapPlanBaseRepository<TDto, TEntity> : IProcessMapPlanBaseRepository<TDto>
where TDto : ProcessMapPlanBaseDto
where TEntity : ProcessMapPlanBase
{
private readonly IAsbCloudDbContext context;
private readonly IWellService wellService;
public ProcessMapPlanBaseRepository(IAsbCloudDbContext context, IWellService wellService)
{
this.context = context;
this.wellService = wellService;
}
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.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(ProcessMapPlanBaseRequest request, CancellationToken token)
{
var timezone = wellService.GetTimezone(request.IdWell);
var offset = TimeSpan.FromHours(timezone.Hours);
var query = context
.Set<TEntity>()
.Where(e => e.IdWell == request.IdWell);
if(request.IdWellSectionType.HasValue)
query = query.Where(e => e.IdWellSectionType == request.IdWellSectionType);
if (request.UpdateFrom.HasValue)
{
var from = request.UpdateFrom.Value.ToUniversalTime();
query = query.Where(e => e.Creation >= from || e.Obsolete >= from);
}
if (request.Moment.HasValue)
{
var moment = request.Moment.Value.ToUniversalTime();
query = query
.Where(e => e.Creation <= moment)
.Where(e => e.Obsolete == null || e.Obsolete >= moment);
}
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => Convert(e, offset));
return dtos;
}
public async Task<IEnumerable<TDto>> GetChangeLog(int idWell, DateOnly? date, CancellationToken token)
{
var query = context
.Set<TEntity>()
.Where(e => e.IdWell == idWell);
var timezone = wellService.GetTimezone(idWell);
var offset = TimeSpan.FromHours(timezone.Hours);
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

@ -10,31 +10,14 @@ namespace AsbCloudWebApi.IntegrationTests;
public abstract class BaseIntegrationTest : IClassFixture<WebAppFactoryFixture>
{
private readonly IServiceScope scope;
private readonly HttpClient httpClient;
protected readonly IServiceScope scope;
protected readonly IAsbCloudDbContext dbContext;
protected readonly WebAppFactoryFixture factory;
protected IAdminDepositClient adminDepositClient;
protected BaseIntegrationTest(WebAppFactoryFixture factory)
protected BaseIntegrationTest(WebAppFactoryFixture factory)
{
scope = factory.Services.CreateScope();
httpClient = factory.CreateClient();
dbContext = scope.ServiceProvider.GetRequiredService<IAsbCloudDbContext>();
var jwtToken = ApiTokenHelper.GetAdminUserToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
var jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
var refitSettings = new RefitSettings(new SystemTextJsonContentSerializer(jsonSerializerOptions));
adminDepositClient = RestService.For<IAdminDepositClient>(httpClient, refitSettings);
}
this.factory = factory;
}
}

View File

@ -24,4 +24,4 @@ public interface IAdminDepositClient
[Delete(BaseRoute + "/{id}")]
Task<IApiResponse<int>> DeleteAsync(int id);
}
}

View File

@ -0,0 +1,32 @@
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Requests;
using Microsoft.AspNetCore.Mvc;
using Refit;
namespace AsbCloudWebApi.IntegrationTests.Clients;
public interface IProcessMapPlanDrillingClient
{
private const string BaseRoute = "/api/well/{idWell}/ProcessMapPlanDrilling";
[Post(BaseRoute)]
Task<IApiResponse<int>> InsertRange(int idWell, [Body] IEnumerable<ProcessMapPlanDrillingDto> dtos);
[Post($"{BaseRoute}/replace")]
Task<IApiResponse<int>> ClearAndInsertRange(int idWell, [Body] IEnumerable<ProcessMapPlanDrillingDto> dtos);
[Delete(BaseRoute)]
Task<IApiResponse<int>> DeleteRange(int idWell, [Body] IEnumerable<int> ids);
[Get(BaseRoute)]
Task<IApiResponse<IEnumerable<ProcessMapPlanDrillingDto>>> Get(int idWell, ProcessMapPlanBaseRequest request);
[Get($"{BaseRoute}/changeLog")]
Task<IApiResponse<IEnumerable<ProcessMapPlanDrillingDto>>> GetChangeLog(int idWell, DateOnly? date);
[Get($"{BaseRoute}/dates")]
Task<IApiResponse<IEnumerable<DateOnly>>> GetDatesChange(int idWell);
[Put(BaseRoute)]
Task<IApiResponse<int>> UpdateRangeAsync(int idWell, IEnumerable<ProcessMapPlanDrillingDto> dtos);
}

View File

@ -1,5 +1,8 @@
using System.Net;
using AsbCloudApp.Data;
using AsbCloudDb.Model;
using AsbCloudDb.Model.ProcessMaps;
using AsbCloudWebApi.IntegrationTests.Clients;
using AsbCloudWebApi.IntegrationTests.TestFakers;
using Mapster;
using Microsoft.EntityFrameworkCore;
@ -9,19 +12,26 @@ namespace AsbCloudWebApi.IntegrationTests.Controllers;
public class AdminDepositControllerTests : BaseIntegrationTest
{
public AdminDepositControllerTests(WebAppFactoryFixture factory)
: base(factory)
{
}
protected IAdminDepositClient client;
public AdminDepositControllerTests(WebAppFactoryFixture factory)
: base(factory)
{
client = factory.GetAuthorizedHttpClient<IAdminDepositClient>();
}
[Fact]
public async Task InsertAsync_ReturnsSuccess_WhenNewItemIsValid()
{
//arrange
var expectedDto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
//arrange
var dbset = dbContext.Set<Deposit>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var expectedDto = EntitiesFaker.Deposit.Generate().Adapt<DepositBaseDto>();
//act
var response = await adminDepositClient.InsertAsync(expectedDto);
var response = await client.InsertAsync(expectedDto);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -37,11 +47,15 @@ public class AdminDepositControllerTests : BaseIntegrationTest
[Fact]
public async Task InsertRangeAsync_ReturnsSuccess_WhenAllNewItemsIsValid()
{
//arrange
var dto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
//arrange
var dbset = dbContext.Set<Deposit>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
//act
var responce = await adminDepositClient.InsertRangeAsync(new[] { dto });
var dto = EntitiesFaker.Deposit.Generate().Adapt<DepositBaseDto>();
//act
var responce = await client.InsertRangeAsync(new[] { dto });
//assert
Assert.Equal(HttpStatusCode.OK, responce.StatusCode);
@ -58,10 +72,10 @@ public class AdminDepositControllerTests : BaseIntegrationTest
public async Task UpdateAsync_ReturnsBadRequest_WhenUpdatedItemHasInvalidId()
{
//arrange
var dto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
var dto = EntitiesFaker.Deposit.Generate().Adapt<DepositBaseDto>();
//act
var response = await adminDepositClient.UpdateAsync(dto);
var response = await client.UpdateAsync(dto);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
@ -71,8 +85,8 @@ public class AdminDepositControllerTests : BaseIntegrationTest
public async Task UpdateAsync_ReturnsSuccess_WhenUpdatedItemIsValid()
{
//arrange
var dto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
var insertResponse = await adminDepositClient.InsertAsync(dto);
var dto = EntitiesFaker.Deposit.Generate().Adapt<DepositBaseDto>();
var insertResponse = await client.InsertAsync(dto);
dto.Id = insertResponse.Content;
dto.Caption = "Test";
@ -85,7 +99,7 @@ public class AdminDepositControllerTests : BaseIntegrationTest
};
//act
var response = await adminDepositClient.UpdateAsync(dto);
var response = await client.UpdateAsync(dto);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -101,22 +115,17 @@ public class AdminDepositControllerTests : BaseIntegrationTest
public async Task GetOrDefaultAsync_ReturnsSuccess_WhenIdIsValid()
{
//arrange
var dto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
var insertResponse = await adminDepositClient.InsertAsync(dto);
var id = insertResponse.Content;
var entity = await dbContext.Deposits.FirstAsync();
var expected = entity.Adapt<DepositBaseDto>();
//act
var response = await adminDepositClient.GetOrDefaultAsync(id);
//act
var response = await client.GetOrDefaultAsync(entity.Id);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var entity = await dbContext.Deposits.FirstOrDefaultAsync(d => d.Id == response.Content.Id);
var deposit = entity?.Adapt<DepositBaseDto>();
var excludeProps = new[] { nameof(DepositBaseDto.Id) };
MatchHelper.Match(dto, deposit, excludeProps);
MatchHelper.Match(expected, response.Content);
}
[Fact]
@ -126,7 +135,7 @@ public class AdminDepositControllerTests : BaseIntegrationTest
const int id = 0;
//act
var responce = await adminDepositClient.GetOrDefaultAsync(id);
var responce = await client.GetOrDefaultAsync(id);
//assert
Assert.Equal(HttpStatusCode.NoContent, responce.StatusCode);
@ -136,7 +145,7 @@ public class AdminDepositControllerTests : BaseIntegrationTest
public async Task GetAllAsync_ReturnsSuccess()
{
//act
var response = await adminDepositClient.GetAllAsync();
var response = await client.GetAllAsync();
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -156,12 +165,12 @@ public class AdminDepositControllerTests : BaseIntegrationTest
public async Task DeleteAsync_ReturnsSuccess_WhenIdIsValid()
{
//arrange
var dto = DepositTestFaker.GetDeposit().Adapt<DepositBaseDto>();
var insertResponse = await adminDepositClient.InsertAsync(dto);
var dto = EntitiesFaker.Deposit.Generate().Adapt<DepositBaseDto>();
var insertResponse = await client.InsertAsync(dto);
var id = insertResponse.Content;
//act
var response = await adminDepositClient.DeleteAsync(id);
var response = await client.DeleteAsync(id);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -178,7 +187,7 @@ public class AdminDepositControllerTests : BaseIntegrationTest
const int id = 0;
//act
var response = await adminDepositClient.DeleteAsync(id);
var response = await client.DeleteAsync(id);
//assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);

View File

@ -0,0 +1,550 @@
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 Xunit;
namespace AsbCloudWebApi.IntegrationTests.Controllers;
public class ProcessMapPlanDrillingControllerTest: BaseIntegrationTest
{
private IProcessMapPlanDrillingClient client;
private readonly ProcessMapPlanDrillingDto dto = new (){
Id = 0,
IdAuthor = 0,
IdEditor = null,
Creation = new(),
Obsolete = null,
IdState = 0,
IdPrevious = null,
IdWell = 1,
IdWellSectionType = 1,
DepthStart = 0.5,
DepthEnd = 1.5,
IdMode = 1,
AxialLoadPlan = 2.718281,
AxialLoadLimitMax = 3.1415926,
DeltaPressurePlan = 4,
DeltaPressureLimitMax = 5,
TopDriveTorquePlan = 6,
TopDriveTorqueLimitMax = 7,
TopDriveSpeedPlan = 8,
TopDriveSpeedLimitMax = 9,
FlowPlan = 10,
FlowLimitMax = 11,
RopPlan = 12,
UsageSaub = 13,
UsageSpin = 14,
Comment = "это тестовая запись",
};
private readonly ProcessMapPlanDrilling entity = new ()
{
Id = 0,
IdAuthor = 1,
IdEditor = null,
Creation = DateTimeOffset.UtcNow,
Obsolete = null,
IdState = AsbCloudDb.Model.ChangeLogAbstract.IdStateActual,
IdPrevious = null,
IdWell = 1,
IdWellSectionType = 1,
DepthStart = 0.5,
DepthEnd = 1.5,
IdMode = 1,
AxialLoadPlan = 2.718281,
AxialLoadLimitMax = 3.1415926,
DeltaPressurePlan = 4,
DeltaPressureLimitMax = 5,
TopDriveTorquePlan = 6,
TopDriveTorqueLimitMax = 7,
TopDriveSpeedPlan = 8,
TopDriveSpeedLimitMax = 9,
FlowPlan = 10,
FlowLimitMax = 11,
RopPlan = 12,
UsageSaub = 13,
UsageSpin = 14,
Comment = "это тестовая запись",
};
public ProcessMapPlanDrillingControllerTest(WebAppFactoryFixture factory) : base(factory)
{
client = factory.GetAuthorizedHttpClient<IProcessMapPlanDrillingClient>();
}
[Fact]
public async Task InsertRange_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var expected = dto.Adapt<ProcessMapPlanDrillingDto>();
//act
var response = await client.InsertRange(dto.IdWell, new[] { expected });
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(1, response.Content);
var entity = dbContext.Set<ProcessMapPlanDrilling>()
.Where(p => p.AxialLoadPlan == dto.AxialLoadPlan)
.Where(p => p.AxialLoadLimitMax == dto.AxialLoadLimitMax)
.Where(p => p.Comment == dto.Comment)
.Where(p => p.IdWell == dto.IdWell)
.FirstOrDefault();
Assert.NotNull(entity);
var actual = entity.Adapt<ProcessMapPlanDrillingDto>();
Assert.Equal(ProcessMapPlanBase.IdStateActual, actual.IdState);
var excludeProps = new[] {
nameof(ProcessMapPlanDrillingDto.Id),
nameof(ProcessMapPlanDrillingDto.IdState),
nameof(ProcessMapPlanDrillingDto.IdAuthor),
nameof(ProcessMapPlanDrillingDto.Creation),
};
MatchHelper.Match(expected, actual, excludeProps);
}
[Fact]
public async Task InsertRange_returns_BadRequest_for_IdWellSectionType()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var badDto = dto.Adapt<ProcessMapPlanDrillingDto>();
badDto.IdWellSectionType = int.MaxValue;
//act
var response = await client.InsertRange(dto.IdWell, new[] { badDto });
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task InsertRange_returns_BadRequest_for_IdMode()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var badDto = dto.Adapt<ProcessMapPlanDrillingDto>();
badDto.IdMode = int.MaxValue;
//act
var response = await client.InsertRange(dto.IdWell, new[] { badDto });
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task InsertRange_returns_BadRequest_for_IdWell()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var badDto = dto.Adapt<ProcessMapPlanDrillingDto>();
badDto.IdWell = int.MaxValue;
//act
var response = await client.InsertRange(dto.IdWell, new[] { badDto });
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task ClearAndInsertRange_returns_success()
{
// arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
var startTime = DateTimeOffset.UtcNow;
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var entry = dbset.Add(entity);
dbContext.SaveChanges();
entry.State = EntityState.Detached;
// act
var result = await client.ClearAndInsertRange(entity.IdWell, new ProcessMapPlanDrillingDto[] { dto });
// assert
var doneTime = DateTimeOffset.UtcNow;
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
var count = dbset.Count();
Assert.Equal(2, count);
var oldEntity = dbset.First(p => p.Id == entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdClearedOnImport, oldEntity.IdState);
Assert.Equal(1, oldEntity.IdEditor);
Assert.NotNull(oldEntity.Obsolete);
Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime);
var newEntity = dbset.First(p => p.Id != entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdStateActual, newEntity.IdState);
Assert.Equal(1, newEntity.IdAuthor);
Assert.Null(newEntity.IdEditor);
Assert.Null(newEntity.Obsolete);
Assert.Null(newEntity.IdPrevious);
Assert.InRange(newEntity.Creation, startTime, doneTime);
}
[Fact]
public async Task UpdateRange_returns_success()
{
// arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
var startTime = DateTimeOffset.UtcNow;
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var entry = dbset.Add(entity);
dbContext.SaveChanges();
entry.State = EntityState.Detached;
var dtoCopy = dto.Adapt<ProcessMapPlanDrillingDto>();
dtoCopy.Id = entry.Entity.Id;
dtoCopy.Comment = "nebuchadnezzar";
dtoCopy.DeltaPressureLimitMax ++;
dtoCopy.DeltaPressurePlan ++;
dtoCopy.FlowPlan ++;
dtoCopy.FlowLimitMax ++;
dtoCopy.RopPlan ++;
dtoCopy.AxialLoadPlan ++;
dtoCopy.AxialLoadLimitMax ++;
dtoCopy.DepthStart ++;
dtoCopy.DepthEnd ++;
dtoCopy.TopDriveSpeedPlan ++;
dtoCopy.TopDriveSpeedLimitMax ++;
dtoCopy.TopDriveTorquePlan ++;
dtoCopy.TopDriveTorqueLimitMax ++;
// act
var result = await client.UpdateRangeAsync(entity.IdWell, new ProcessMapPlanDrillingDto[] { dtoCopy });
// assert
var doneTime = DateTimeOffset.UtcNow;
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
var count = dbset.Count();
Assert.Equal(2, count);
var oldEntity = dbset.First(p => p.Id == entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdStateReplaced, oldEntity.IdState);
Assert.Equal(1, oldEntity.IdEditor);
Assert.NotNull(oldEntity.Obsolete);
Assert.InRange(oldEntity.Obsolete.Value, startTime, doneTime);
var newEntity = dbset.First(p => p.Id != entry.Entity.Id);
Assert.Equal(ProcessMapPlanBase.IdStateActual, newEntity.IdState);
Assert.Equal(1, newEntity.IdAuthor);
Assert.Null(newEntity.IdEditor);
Assert.Null(newEntity.Obsolete);
Assert.Equal(oldEntity.Id, newEntity.IdPrevious);
Assert.InRange(newEntity.Creation, startTime, doneTime);
var expected = dtoCopy.Adapt<ProcessMapPlanDrilling>();
var excludeProps = new[] {
nameof(ProcessMapPlanDrillingDto.Id),
nameof(ProcessMapPlanDrillingDto.IdAuthor),
nameof(ProcessMapPlanDrillingDto.Creation),
};
MatchHelper.Match(expected, newEntity!, excludeProps);
}
[Fact]
public async Task DeleteRange_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
var startTime = DateTimeOffset.UtcNow;
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var entry = dbset.Add(entity);
dbContext.SaveChanges();
entry.State = EntityState.Detached;
//act
var response = await client.DeleteRange(dto.IdWell, new[] { entry.Entity.Id });
//assert
var doneTime = DateTimeOffset.UtcNow;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(1, response.Content);
var actual = dbContext.Set<ProcessMapPlanDrilling>()
.Where(p => p.Id == entry.Entity.Id)
.FirstOrDefault();
Assert.NotNull(actual);
Assert.Equal(ProcessMapPlanBase.IdStateDeleted, actual.IdState);
Assert.Equal(1, actual.IdEditor);
Assert.NotNull(actual.Obsolete);
Assert.InRange(actual.Obsolete.Value, startTime, doneTime);
}
[Fact]
public async Task GetDatesChange_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var entity2 = entity.Adapt<ProcessMapPlanDrilling>();
entity2.Creation = entity.Creation.AddDays(1);
dbset.Add(entity);
dbset.Add(entity2);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
var dates = new[] { entity.Creation, entity2.Creation }
.Select(d => d.ToOffset(offset))
.Select(d => new DateOnly(d.Year, d.Month, d.Day));
//act
var response = await client.GetDatesChange(dto.IdWell);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Equal(2, response.Content.Count());
Assert.All(response.Content, d => dates.Contains(d));
}
[Fact]
public async Task Get_all_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
dbset.Add(entity);
var entityDeleted = entity.Adapt<ProcessMapPlanDrilling>();
entityDeleted.Creation = entity.Creation.AddDays(-1);
entityDeleted.Obsolete = entity.Creation;
entityDeleted.IdState = ProcessMapPlanBase.IdStateDeleted;
entityDeleted.IdEditor = 1;
dbset.Add(entityDeleted);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
//act
var request = new ProcessMapPlanBaseRequest
{
IdWell = dto.IdWell,
};
var response = await client.Get(dto.IdWell, request);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Equal(2, response.Content.Count());
}
[Fact]
public async Task Get_actual_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
dbset.Add(entity);
var entityDeleted = entity.Adapt<ProcessMapPlanDrilling>();
entityDeleted.Creation = entity.Creation.AddDays(-1);
entityDeleted.Obsolete = entity.Creation;
entityDeleted.IdState = ProcessMapPlanBase.IdStateDeleted;
entityDeleted.IdEditor = 1;
entityDeleted.Comment = "nothing";
dbset.Add(entityDeleted);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
//act
var request = new ProcessMapPlanBaseRequest {
IdWell = dto.IdWell,
Moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero)
};
var response = await client.Get(dto.IdWell, request);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Single(response.Content);
var actual = response.Content.First()!;
var expected = entity.Adapt<ProcessMapPlanDrillingDto>()!;
var excludeProps = new[] {
nameof(ProcessMapPlanDrillingDto.Id),
nameof(ProcessMapPlanDrillingDto.IdAuthor),
nameof(ProcessMapPlanDrillingDto.Creation),
};
MatchHelper.Match(expected, actual, excludeProps);
}
[Fact]
public async Task Get_at_moment_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
var now = DateTimeOffset.UtcNow;
var entityDeleted = entity.Adapt<ProcessMapPlanDrilling>();
entityDeleted.Creation = now;
entityDeleted.Obsolete = now.AddMinutes(1);
entityDeleted.IdState = ProcessMapPlanBase.IdStateDeleted;
entityDeleted.IdEditor = 1;
entityDeleted.Comment = "nothing";
dbset.Add(entityDeleted);
var entityDeleted2 = entity.Adapt<ProcessMapPlanDrilling>();
entityDeleted2.Creation = now.AddMinutes(1);
entityDeleted2.Obsolete = now.AddMinutes(2);
entityDeleted2.IdState = ProcessMapPlanBase.IdStateDeleted;
entityDeleted2.IdEditor = 1;
entityDeleted2.Comment = "nothing";
dbset.Add(entityDeleted2);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
//act
var request = new ProcessMapPlanBaseRequest
{
IdWell = dto.IdWell,
Moment = now.AddMinutes(0.5),
};
var response = await client.Get(dto.IdWell, request);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Equal(1, response.Content.Count());
}
[Fact]
public async Task Get_section_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
dbset.Add(entity);
var entity2 = entity.Adapt<ProcessMapPlanDrilling>();
entity2.IdWellSectionType = 2;
entity2.Comment = "IdWellSectionType = 2";
dbset.Add(entity2);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
//act
var request = new ProcessMapPlanBaseRequest
{
IdWell = dto.IdWell,
IdWellSectionType = 2,
};
var response = await client.Get(dto.IdWell, request);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Single(response.Content);
var actual = response.Content.First()!;
var expected = entity2.Adapt<ProcessMapPlanDrillingDto>()!;
var excludeProps = new[] {
nameof(ProcessMapPlanDrillingDto.Id),
nameof(ProcessMapPlanDrillingDto.IdAuthor),
nameof(ProcessMapPlanDrillingDto.Creation),
};
MatchHelper.Match(expected, actual, excludeProps);
}
[Fact]
public async Task Get_updated_returns_success()
{
//arrange
var dbset = dbContext.Set<ProcessMapPlanDrilling>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
dbset.Add(entity);
var entity2 = entity.Adapt<ProcessMapPlanDrilling>();
entity2.Creation = entity.Creation.AddHours(1);
entity2.Comment = "IdWellSectionType = 2";
dbset.Add(entity2);
var entity3 = entity.Adapt<ProcessMapPlanDrilling>();
entity3.Obsolete = entity.Creation.AddHours(1);
entity3.Comment = "IdWellSectionType = 3";
dbset.Add(entity3);
dbContext.SaveChanges();
var timezoneHours = Data.Defaults.Wells[0].Timezone.Hours;
var offset = TimeSpan.FromHours(timezoneHours);
var updateFrom = entity.Creation.ToOffset(offset).AddHours(0.5);
//act
var request = new ProcessMapPlanBaseRequest
{
IdWell = dto.IdWell,
UpdateFrom = updateFrom,
};
var response = await client.Get(dto.IdWell, request);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Equal(2, response.Content.Count());
var actual = response.Content
.Where(p=>p.Comment == entity2.Comment)
.First();
var expected = entity2.Adapt<ProcessMapPlanDrillingDto>();
var excludeProps = new[] {
nameof(ProcessMapPlanDrillingDto.Id),
nameof(ProcessMapPlanDrillingDto.IdAuthor),
nameof(ProcessMapPlanDrillingDto.Creation),
};
MatchHelper.Match(expected, actual, excludeProps);
}
}

View File

@ -0,0 +1,63 @@
using AsbCloudDb.Model;
namespace AsbCloudWebApi.IntegrationTests.Data
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2211:Поля, не являющиеся константами, не должны быть видимыми", Justification = "<Ожидание>")]
public static class Defaults
{
public static Deposit[] Deposits = new Deposit[] {
new()
{
Id = 1,
Caption = "Deposit1",
Latitude = 10,
Longitude = 20,
Timezone = new SimpleTimezone{
Hours = 1
}
}
};
public static Cluster[] Clusters = new Cluster[] {
new()
{
Id = 1,
IdDeposit = Deposits[0].Id,
Caption = "Cluster1",
Latitude = 10,
Longitude = 20,
Timezone = new SimpleTimezone{
Hours = 1
}
}
};
public static Well[] Wells = new Well[] {
new()
{
Id = 1,
IdCluster = Clusters[0].Id,
IdWellType = 1,
IdState = 1,
Caption = "Well1",
Latitude = 10,
Longitude = 20,
IdTelemetry = null,
Timezone = new SimpleTimezone{
Hours = 1
}
}
};
public static RelationCompanyWell[] RelationsCompanyWell = new RelationCompanyWell[]
{
new(){IdCompany= 1, IdWell = Wells[0].Id},
};
public static RelationUserUserRole[] RelationsUserUserRole = new RelationUserUserRole[]
{
new(){ IdUserRole= 1, IdUser = 1}
};
}
}

View File

@ -1,25 +0,0 @@
using AsbCloudDb.Model;
using Bogus;
namespace AsbCloudWebApi.IntegrationTests.TestFakers;
public static class DepositTestFaker
{
public static Deposit GetDeposit() =>
GetDepositFaker().Generate();
public static IEnumerable<Deposit> GetDeposits(int count) =>
GetDepositFaker().Generate(count);
private static Faker<Deposit> GetDepositFaker() =>
new Faker<Deposit>()
.RuleFor(d => d.Id, 0)
.RuleFor(d => d.Caption, f => f.Random.String2(1, 50))
.RuleFor(d => d.Latitude, f => f.Random.Int(-90, 90))
.RuleFor(d => d.Longitude, f => f.Random.Int(-180, 180))
.RuleFor(d => d.Timezone, f => new SimpleTimezone
{
Hours = f.Random.Int(1, 12),
IsOverride = f.Random.Bool()
});
}

View File

@ -0,0 +1,40 @@
using AsbCloudDb.Model;
using Bogus;
namespace AsbCloudWebApi.IntegrationTests.TestFakers;
public static class EntitiesFaker
{
public static Faker<Deposit> Deposit { get; } = new Faker<Deposit>()
.RuleFor(d => d.Id, 0)
.RuleFor(d => d.Caption, f => f.Random.String2(1, 50))
.RuleFor(d => d.Latitude, f => f.Random.Double(-90, 90))
.RuleFor(d => d.Longitude, f => f.Random.Double(-180, 180))
.RuleFor(d => d.Timezone, f => new SimpleTimezone
{
Hours = f.Random.Int(1, 12),
IsOverride = f.Random.Bool()
});
public static Faker<Cluster> Cluster { get; } = new Faker<Cluster>()
.RuleFor(d => d.Id, 0)
.RuleFor(d => d.Caption, f => f.Random.String2(1, 50))
.RuleFor(d => d.Latitude, f => f.Random.Double(-90, 90))
.RuleFor(d => d.Longitude, f => f.Random.Double(-180, 180))
.RuleFor(d => d.Timezone, f => new SimpleTimezone
{
Hours = f.Random.Int(1, 12),
IsOverride = f.Random.Bool()
});
public static Faker<Well> Well { get; } = new Faker<Well>()
.RuleFor(d => d.Id, 0)
.RuleFor(d => d.Caption, f => f.Random.String2(1, 50))
.RuleFor(d => d.Latitude, f => f.Random.Double(-90, 90))
.RuleFor(d => d.Longitude, f => f.Random.Double(-180, 180))
.RuleFor(d => d.Timezone, f => new SimpleTimezone
{
Hours = f.Random.Int(1, 12),
IsOverride = f.Random.Bool()
});
}

View File

@ -6,6 +6,10 @@ using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ProtoBuf.Serializers;
using Refit;
using System.Net.Http.Headers;
using System.Text.Json;
using Xunit;
namespace AsbCloudWebApi.IntegrationTests;
@ -13,12 +17,20 @@ namespace AsbCloudWebApi.IntegrationTests;
public class WebAppFactoryFixture : WebApplicationFactory<Startup>,
IAsyncLifetime
{
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
private static readonly RefitSettings refitSettings = new RefitSettings(new SystemTextJsonContentSerializer(jsonSerializerOptions));
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var connectionString = configuration.GetConnectionString("TestConnection");
var connectionString = configuration.GetConnectionString("TestConnection")!;
builder.ConfigureServices(services =>
{
@ -37,19 +49,41 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>,
using var scope = Services.CreateScope();
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<AsbCloudDbContext>();
dbContext.Database.EnsureCreatedAndMigrated();
dbContext.Deposits.AddRange(DepositTestFaker.GetDeposits(15));
await dbContext.SaveChangesAsync();
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreatedAndMigrated();
await FillBaseDatasets(dbContext);
}
public new async Task DisposeAsync()
private static async Task FillBaseDatasets(AsbCloudDbContext dbContext)
{
dbContext.AddRange(Data.Defaults.Deposits);
dbContext.AddRange(Data.Defaults.Clusters);
dbContext.AddRange(Data.Defaults.Wells);
dbContext.AddRange(Data.Defaults.RelationsCompanyWell);
await dbContext.SaveChangesAsync();
}
public new async Task DisposeAsync()
{
using var scope = Services.CreateScope();
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<AsbCloudDbContext>();
await dbContext.Database.EnsureDeletedAsync();
}
}
public HttpClient GetAuthorizedHttpClient()
{
var httpClient = CreateClient();
var jwtToken = ApiTokenHelper.GetAdminUserToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
return httpClient;
}
public T GetAuthorizedHttpClient<T>()
{
var httpClient = GetAuthorizedHttpClient();
return RestService.For<T>(httpClient, refitSettings);
}
}

View File

@ -0,0 +1,2 @@
Статья от ms https://learn.microsoft.com/ru-ru/aspnet/core/test/integration-tests?view=aspnetcore-6.0
refit https://github.com/reactiveui/refit

View File

@ -0,0 +1,198 @@
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Http;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Requests;
using System;
using AsbCloudApp.Services;
using System.Linq;
namespace AsbCloudWebApi.Controllers.ProcessMapPlan;
/// <summary>
/// РТК план
/// </summary>
[ApiController]
[Route("api/well/{idWell}/[controller]")]
[Authorize]
public abstract class ProcessMapPlanBaseController<TDto> : ControllerBase
where TDto : ProcessMapPlanBaseDto
{
private readonly IProcessMapPlanBaseRepository<TDto> repository;
private readonly IWellService wellService;
public ProcessMapPlanBaseController(IProcessMapPlanBaseRepository<TDto> repository, IWellService wellService)
{
this.repository = repository;
this.wellService = wellService;
}
/// <summary>
/// Добавление
/// </summary>
/// <param name="idWell"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> InsertRange([FromRoute] int idWell, IEnumerable<TDto> dtos, CancellationToken token)
{
if (idWell == 0 || dtos.Any(d => d.IdWell != idWell))
return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell");
var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.InsertRange(idUser, dtos, token);
return Ok(result);
}
/// <summary>
/// Удалить все по скважине и добавить новые
/// </summary>
/// <param name="idWell"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("replace")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ClearAndInsertRange([FromRoute] int idWell, IEnumerable<TDto> dtos, CancellationToken token)
{
if (idWell == 0 || dtos.Any(d => d.IdWell != idWell))
return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell");
var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.ClearAndInsertRange(idUser, idWell, dtos, token);
return Ok(result);
}
/// <summary>
/// Удаление
/// </summary>
/// <param name="idWell"></param>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> DeleteRange([FromRoute]int idWell, IEnumerable<int> ids, CancellationToken token)
{
var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.DeleteRange(idUser, ids, token);
return Ok(result);
}
/// <summary>
/// Получение
/// </summary>
/// <param name="idWell"></param>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<TDto>>> Get([FromRoute] int idWell, [FromQuery]ProcessMapPlanBaseRequest request, CancellationToken token)
{
request.IdWell = idWell;
await AssertUserHasAccessToWell(request.IdWell, token);
var result = await repository.Get(request, token);
return Ok(result);
}
/// <summary>
/// Изменения за определенную дату
/// </summary>
/// <param name="idWell"></param>
/// <param name="date"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("changeLog")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<TDto>>> GetChangeLog([FromRoute] int idWell, [FromQuery] DateOnly? date, CancellationToken token)
{
await AssertUserHasAccessToWell(idWell, token);
var result = await repository.GetChangeLog(idWell, date, token);
return Ok(result);
}
/// <summary>
/// Даты за которые есть изменения
/// </summary>
/// <param name="idWell"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("dates")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<IEnumerable<DateOnly>>> GetDatesChange([FromRoute] int idWell, CancellationToken token)
{
await AssertUserHasAccessToWell(idWell, token);
var result = await repository.GetDatesChange(idWell, token);
return Ok(result);
}
/// <summary>
/// Редактирование
/// </summary>
/// <param name="idWell"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPut]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdateRange([FromRoute] int idWell, IEnumerable<TDto> dtos, CancellationToken token)
{
var first = dtos.FirstOrDefault();
if(first is null)
return NoContent();
if (idWell == 0 || dtos.Any(d => d.IdWell != idWell))
return this.ValidationBadRequest(nameof(dtos), "all dtos should contain same idWell");
var idUser = await AssertUserHasAccessToWell(idWell, token);
var result = await repository.UpdateRange(idUser, dtos, token);
return Ok(result);
}
/// <summary>
/// returns user id, if he has access to well
/// </summary>
/// <param name="idWell"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="ForbidException"></exception>
private async Task<int> AssertUserHasAccessToWell(int idWell, CancellationToken token)
{
var idUser = GetUserId();
var idCompany = User.GetCompanyId();
if (!idCompany.HasValue)
throw new ForbidException("Нет доступа к скважине");
if (!await wellService.IsCompanyInvolvedInWellAsync(idCompany.Value, idWell, token))
throw new ForbidException("Нет доступа к скважине");
return idUser;
}
/// <summary>
/// returns user id or throw
/// </summary>
/// <returns></returns>
/// <exception cref="ForbidException"></exception>
private int GetUserId()
{
var idUser = User.GetUserId() ?? throw new ForbidException("Неизвестный пользователь");
return idUser;
}
}

View File

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

View File

@ -0,0 +1,132 @@
@baseUrl = http://127.0.0.1:5000
@contentType = application/json
@contentTypeForFiles = application/octet-stream
@auth = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2OTc0MzcwMzEsImV4cCI6MTcyODk5NDYzMSwiaXNzIjoiYSIsImF1ZCI6ImEifQ.vB7Qb3K9gG77iP8y25zB3RcZIQk9cHkq3I1SkcooYJs
@uid = 20210101_000000000
@id = 1
@idWell = 1
### получение данных drill test с панели и сохранение их в ЕЦП
POST {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling
Content-Type: {{contentType}}
accept: */*
Authorization: {{auth}}
[
{
"id": 0,
"idAuthor": 0,
"idEditor": 0,
"creation": "2024-01-18T12:45:58.205Z",
"obsolete": "2024-01-18T12:45:58.205Z",
"idState": 0,
"idPrevious": 0,
"idWell": 1,
"idWellSectionType": 1,
"depthStart": 0,
"depthEnd": 10,
"idMode": 2,
"axialLoad": {
"plan": 0,
"limitMax": 0
},
"deltaPressure": {
"plan": 0,
"limitMax": 0
},
"topDriveTorque": {
"plan": 0,
"limitMax": 0
},
"topDriveSpeed": {
"plan": 0,
"limitMax": 0
},
"flow": {
"plan": 0,
"limitMax": 0
},
"ropPlan": 99999.9,
"usageSaub": 100,
"usageSpin": 100,
"comment": ""
}
]
### Получение всех
GET {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling
accept: */*
Authorization: {{auth}}
### замена старых записей новыми
POST {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling/replace
Content-Type: {{contentType}}
accept: */*
Authorization: {{auth}}
[
{
"id": 0,
"idAuthor": 0,
"idEditor": 0,
"creation": "2024-01-19T05:31:54.762Z",
"obsolete": "2024-01-19T05:31:54.762Z",
"idState": 0,
"idPrevious": 0,
"idWell": 1,
"idWellSectionType": 1500,
"depthStart": 0,
"depthEnd": 10,
"idMode": 2,
"axialLoad": {
"plan": 0,
"limitMax": 0
},
"deltaPressure": {
"plan": 0,
"limitMax": 0
},
"topDriveTorque": {
"plan": 0,
"limitMax": 0
},
"topDriveSpeed": {
"plan": 0,
"limitMax": 0
},
"flow": {
"plan": 0,
"limitMax": 0
},
"ropPlan": 99999.9,
"usageSaub": 100,
"usageSpin": 100,
"comment": "There is no spoon"
}
]
### Получение только актуальных
GET {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling?Moment=3000-01-01
accept: */*
Authorization: {{auth}}
### Получение
GET {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling/dates
accept: */*
Authorization: {{auth}}
### Получение изменений за дату
GET {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling/changeLog?date=2024-01-19
accept: */*
Authorization: {{auth}}
### удаление
DELETE {{baseUrl}}/api/well/{{idWell}}/ProcessMapPlanDrilling
Content-Type: {{contentType}}
accept: */*
Authorization: {{auth}}
[
1
]