Merge feature/feat-8c: Excel import loader — /import/dry-run + gated /import/execute, quarantine policy, provenance + idempotency [god-QA: 43/43]
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
|||||||
161
GerbilManager.Tests/ImportServiceTests.cs
Normal file
161
GerbilManager.Tests/ImportServiceTests.cs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
using GerbilManagerWebAPI.Import;
|
||||||
|
using GerbilManagerWebAPI.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GerbilManager.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FEAT-8c loader tests against a small JSON fixture + an in-memory DB (never Julian's
|
||||||
|
/// instance). Covers categorisation, the quarantine policy, genotype composition,
|
||||||
|
/// Farbschlag matching, high-confidence linking, and idempotency.
|
||||||
|
/// </summary>
|
||||||
|
public class ImportServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _dir;
|
||||||
|
|
||||||
|
public ImportServiceTests()
|
||||||
|
{
|
||||||
|
_dir = Path.Combine(Path.GetTempPath(), "feat8c-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_dir);
|
||||||
|
File.WriteAllText(Path.Combine(_dir, "litters.json"), LittersJson);
|
||||||
|
File.WriteAllText(Path.Combine(_dir, "animals.json"), AnimalsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApplicationContext NewDb()
|
||||||
|
{
|
||||||
|
var opts = new DbContextOptionsBuilder<ApplicationContext>()
|
||||||
|
.UseInMemoryDatabase("feat8c-" + Guid.NewGuid().ToString("N"))
|
||||||
|
.Options;
|
||||||
|
var db = new ApplicationContext(opts);
|
||||||
|
db.Database.EnsureCreated(); // applies the 73-variety HasData seed
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DryRun_categorises_without_writing()
|
||||||
|
{
|
||||||
|
using var db = NewDb();
|
||||||
|
var report = await new ImportService(db, _dir, _dir).RunAsync(execute: false);
|
||||||
|
|
||||||
|
Assert.False(report.Executed);
|
||||||
|
Assert.Equal(2, report.Litters.InSource);
|
||||||
|
Assert.Equal(2, report.Litters.Created);
|
||||||
|
// 4 animals: a1 (hoch) + a4 (date-only) loadable; a2 conflict, a3 stub quarantined
|
||||||
|
Assert.Equal(4, report.Animals.InSource);
|
||||||
|
Assert.Equal(2, report.Animals.Created);
|
||||||
|
Assert.Equal(1, report.Animals.LinkedToLitter); // only a1 (hoch)
|
||||||
|
Assert.Equal(1, report.Animals.FarbschlagMatched); // a1 -> Agouti
|
||||||
|
Assert.Equal(1, report.Animals.Quarantined.Conflicts);
|
||||||
|
Assert.Equal(1, report.Animals.Quarantined.Stubs);
|
||||||
|
Assert.Equal(1, report.Animals.Quarantined.DateOnlyLinks);
|
||||||
|
|
||||||
|
// nothing written in dry-run
|
||||||
|
Assert.Equal(0, await db.Gerbils.CountAsync());
|
||||||
|
Assert.Equal(0, await db.Litters.CountAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_loads_conflict_free_and_links_high_confidence()
|
||||||
|
{
|
||||||
|
using var db = NewDb();
|
||||||
|
var report = await new ImportService(db, _dir, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.True(report.Executed);
|
||||||
|
Assert.Equal(2, await db.Litters.CountAsync());
|
||||||
|
Assert.Equal(2, await db.Gerbils.CountAsync()); // a1, a4 only
|
||||||
|
|
||||||
|
// conflict + stub never loaded
|
||||||
|
Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a2"));
|
||||||
|
Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a3"));
|
||||||
|
|
||||||
|
var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1");
|
||||||
|
Assert.NotNull(a1.LitterId); // high-confidence link
|
||||||
|
Assert.Equal("FEAT-8 Stammbaum/Wurfchronik", a1.ImportSource);
|
||||||
|
Assert.NotNull(a1.ColorVarietyId); // Agouti matched
|
||||||
|
Assert.Contains("aa CC", a1.Genotype); // composed from mapped8locus
|
||||||
|
Assert.Contains("??", a1.Genotype!); // missing loci -> ??
|
||||||
|
Assert.Contains("eef", a1.Genotype!); // e^f caret stripped
|
||||||
|
Assert.Contains("RawGenotype", a1.RawImportData!); // raw verbatim preserved
|
||||||
|
|
||||||
|
var a4 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a4");
|
||||||
|
Assert.Null(a4.LitterId); // date-only link quarantined
|
||||||
|
|
||||||
|
// PairingCode carried through
|
||||||
|
Assert.True(await db.Litters.AnyAsync(l => l.PairingCode == "G01/ZdkC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_is_idempotent()
|
||||||
|
{
|
||||||
|
using var db = NewDb();
|
||||||
|
await new ImportService(db, _dir, _dir).RunAsync(execute: true);
|
||||||
|
var second = await new ImportService(db, _dir, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.Equal(0, second.Animals.Created);
|
||||||
|
Assert.Equal(0, second.Litters.Created);
|
||||||
|
Assert.Equal(2, await db.Gerbils.CountAsync());
|
||||||
|
Assert.Equal(2, await db.Litters.CountAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComposeGenotype_strips_carets_and_fills_missing_loci()
|
||||||
|
{
|
||||||
|
var g = new SourceGenotype
|
||||||
|
{
|
||||||
|
Mapped8locus = new()
|
||||||
|
{
|
||||||
|
["A"] = new() { "a", "a" },
|
||||||
|
["C"] = new() { "C", "c^chm" },
|
||||||
|
["E"] = new() { "e", "e^f" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// order A C D E G P Sp Re ; missing -> ??
|
||||||
|
Assert.Equal("aa Ccchm ?? eef ?? ?? ?? ??", ImportService.ComposeGenotype(g));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("01.02.2020", 2020, 2, 1)]
|
||||||
|
[InlineData("5.3.21", 2021, 3, 5)]
|
||||||
|
public void ParseDate_handles_german_dates(string s, int y, int m, int d)
|
||||||
|
{
|
||||||
|
var date = ImportService.ParseDate(s);
|
||||||
|
Assert.Equal(new DateOnly(y, m, d), date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- fixtures ----
|
||||||
|
private const string LittersJson = """
|
||||||
|
[
|
||||||
|
{"id":"L1","litterId":"A","date":"01.02.2020","damName":"Mama [X]","sireName":"Papa of Y","totalBorn":4,"zuchtnummer":"G01/ZdkC","note":"erster Wurf"},
|
||||||
|
{"id":"L2","litterId":"B","date":"05.03.2021","damName":"Oma [Z]","sireName":"Opa of W","totalBorn":2,"zuchtnummer":"G02/ZdkC","note":""}
|
||||||
|
]
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string AnimalsJson = """
|
||||||
|
[
|
||||||
|
{"id":"a1","name":"Kind Eins","dob":"01.02.2020","death":"","gender":null,
|
||||||
|
"farbschlag":"Agouti","farbschlagVariants":["Agouti"],
|
||||||
|
"genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"]},"rawGenotype":"aa CC D- ee[f]","unmappedTokens":[]},
|
||||||
|
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
|
||||||
|
"litterRef":{"litterId":"L1","method":"geburtsdatum+eltern","confidence":"hoch"}},
|
||||||
|
{"id":"a2","name":"Streit","dob":"01.01.2019","death":"","gender":null,
|
||||||
|
"farbschlag":"Schwarz","farbschlagVariants":["Schwarz"],
|
||||||
|
"genotype":{"mapped8locus":{"A":["a","a"]},"rawGenotype":"aa","unmappedTokens":[]},
|
||||||
|
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1","f2"],"conflict":true},
|
||||||
|
{"id":"a3","name":"Namenlos","dob":"","death":"","gender":null,
|
||||||
|
"farbschlag":"","farbschlagVariants":[],
|
||||||
|
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},
|
||||||
|
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false},
|
||||||
|
{"id":"a4","name":"Unsicher","dob":"05.03.2021","death":"","gender":null,
|
||||||
|
"farbschlag":"","farbschlagVariants":[],
|
||||||
|
"genotype":{"mapped8locus":{"A":["A","a"]},"rawGenotype":"Aa","unmappedTokens":[]},
|
||||||
|
"zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
|
||||||
|
"litterRef":{"litterId":"L2","method":"geburtsdatum","confidence":"niedrig"}}
|
||||||
|
]
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
35
GerbilManagerWebAPI/Endpoints/ImportEndpoints.cs
Normal file
35
GerbilManagerWebAPI/Endpoints/ImportEndpoints.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using GerbilManagerWebAPI.Import;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Endpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FEAT-8 stage 3 — spreadsheet import loader. Consumes tools/import/output.
|
||||||
|
/// POST /import/dry-run -> report what WOULD be created/linked/quarantined (no writes)
|
||||||
|
/// POST /import/execute -> load the conflict-free subset (idempotent). GATED: only run
|
||||||
|
/// against Julian's DB under god-supervision after he approves the dry-run.
|
||||||
|
/// </summary>
|
||||||
|
public static class ImportEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapImportEndpoints(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
var group = app.MapGroup("/import").WithTags("Import");
|
||||||
|
|
||||||
|
group.MapPost("/dry-run", async Task<Ok<ImportReport>> (
|
||||||
|
ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
|
||||||
|
{
|
||||||
|
var report = await new ImportService(db, config, env).RunAsync(execute: false);
|
||||||
|
return TypedResults.Ok(report);
|
||||||
|
});
|
||||||
|
|
||||||
|
group.MapPost("/execute", async Task<Ok<ImportReport>> (
|
||||||
|
ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
|
||||||
|
{
|
||||||
|
var report = await new ImportService(db, config, env).RunAsync(execute: true);
|
||||||
|
return TypedResults.Ok(report);
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
GerbilManagerWebAPI/Import/ImportModels.cs
Normal file
90
GerbilManagerWebAPI/Import/ImportModels.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Import
|
||||||
|
{
|
||||||
|
// ---- Source shapes (tools/import/output JSON, produced by extract.py) ----
|
||||||
|
|
||||||
|
public sealed class SourceAnimal
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public List<string> NameVariants { get; set; } = new();
|
||||||
|
public string Dob { get; set; } = "";
|
||||||
|
public string Death { get; set; } = "";
|
||||||
|
public string? Gender { get; set; }
|
||||||
|
public string Farbschlag { get; set; } = "";
|
||||||
|
public List<string> FarbschlagVariants { get; set; } = new();
|
||||||
|
public SourceGenotype Genotype { get; set; } = new();
|
||||||
|
public string Zucht { get; set; } = "";
|
||||||
|
public List<SourceParentRef> ParentRefs { get; set; } = new();
|
||||||
|
public List<string> Photos { get; set; } = new();
|
||||||
|
public List<string> SourceFiles { get; set; } = new();
|
||||||
|
public bool Conflict { get; set; }
|
||||||
|
public SourceLitterRef? LitterRef { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SourceGenotype
|
||||||
|
{
|
||||||
|
public Dictionary<string, List<string>> Mapped8locus { get; set; } = new();
|
||||||
|
public string RawGenotype { get; set; } = "";
|
||||||
|
public List<string> UnmappedTokens { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SourceParentRef
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Dob { get; set; } = "";
|
||||||
|
public string RoleGuess { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SourceLitterRef
|
||||||
|
{
|
||||||
|
public string LitterId { get; set; } = "";
|
||||||
|
public string Method { get; set; } = "";
|
||||||
|
public string Confidence { get; set; } = ""; // "hoch" | "niedrig" | (ambiguous => candidates)
|
||||||
|
public List<string>? Candidates { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SourceLitter
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string LitterId { get; set; } = "";
|
||||||
|
public string Date { get; set; } = "";
|
||||||
|
public string DamName { get; set; } = "";
|
||||||
|
public string SireName { get; set; } = "";
|
||||||
|
public int? TotalBorn { get; set; }
|
||||||
|
public string Zuchtnummer { get; set; } = "";
|
||||||
|
public string Note { get; set; } = "";
|
||||||
|
public List<string> Warnings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Report shapes (Julian-readable: counts per category + samples) ----
|
||||||
|
|
||||||
|
public sealed record ImportReport(
|
||||||
|
bool Executed,
|
||||||
|
LitterSummary Litters,
|
||||||
|
AnimalSummary Animals,
|
||||||
|
PhotoSummary Photos,
|
||||||
|
IReadOnlyList<string> Samples,
|
||||||
|
IReadOnlyList<string> Notes);
|
||||||
|
|
||||||
|
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported);
|
||||||
|
|
||||||
|
public sealed record AnimalSummary(
|
||||||
|
int InSource,
|
||||||
|
int Created,
|
||||||
|
int LinkedToLitter,
|
||||||
|
int FarbschlagMatched,
|
||||||
|
int FarbschlagUnmatched,
|
||||||
|
int AlreadyImported,
|
||||||
|
QuarantineSummary Quarantined);
|
||||||
|
|
||||||
|
public sealed record QuarantineSummary(
|
||||||
|
int Conflicts,
|
||||||
|
int Stubs,
|
||||||
|
int DateOnlyLinks,
|
||||||
|
int AmbiguousLinks,
|
||||||
|
int Total);
|
||||||
|
|
||||||
|
public sealed record PhotoSummary(int Attached, int SourceFilesMissing);
|
||||||
|
}
|
||||||
301
GerbilManagerWebAPI/Import/ImportService.cs
Normal file
301
GerbilManagerWebAPI/Import/ImportService.cs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using GerbilManagerWebAPI.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Import
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FEAT-8c import loader. Consumes tools/import/output (animals.json + litters.json
|
||||||
|
/// produced by extract.py) and loads conflict-free data into the database:
|
||||||
|
/// litters first (Wurfchronik = authoritative), then animals matched onto them.
|
||||||
|
///
|
||||||
|
/// Load policy:
|
||||||
|
/// - Litters: all created (idempotent by ExternalRef = source litter id).
|
||||||
|
/// - Animals: created when they have a DOB and are NOT in conflict. The birth-litter
|
||||||
|
/// link is set only for HIGH-confidence matches (litterRef.confidence == "hoch");
|
||||||
|
/// date-only/ambiguous links are quarantined (animal loads with LitterId = null).
|
||||||
|
/// - QUARANTINED (never loaded): conflicts + stubs (no DOB) — await the wife's review.
|
||||||
|
/// Idempotent: re-running matches on ExternalRef and skips existing rows.
|
||||||
|
/// Execute is gated by the endpoint; this service only acts when asked.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ImportService
|
||||||
|
{
|
||||||
|
private static readonly string[] LocusOrder = { "A", "C", "D", "E", "G", "P", "Sp", "Re" };
|
||||||
|
private static readonly JsonSerializerOptions Json = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
private const string ImportSourceTag = "FEAT-8 Stammbaum/Wurfchronik";
|
||||||
|
|
||||||
|
private readonly ApplicationContext _db;
|
||||||
|
private readonly string _sourceDir;
|
||||||
|
private readonly string _photoRoot;
|
||||||
|
|
||||||
|
/// <summary>Test-friendly constructor with explicit paths.</summary>
|
||||||
|
public ImportService(ApplicationContext db, string sourceDir, string photoRoot)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_sourceDir = sourceDir;
|
||||||
|
_photoRoot = photoRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportService(ApplicationContext db, IConfiguration config, IWebHostEnvironment env)
|
||||||
|
: this(db,
|
||||||
|
config["Import:SourcePath"]
|
||||||
|
?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "tools", "import", "output")),
|
||||||
|
config["Photos:RootPath"] ?? Path.Combine(env.ContentRootPath, "photo-storage"))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImportReport> RunAsync(bool execute)
|
||||||
|
{
|
||||||
|
var notes = new List<string>();
|
||||||
|
var samples = new List<string>();
|
||||||
|
|
||||||
|
var animals = Load<List<SourceAnimal>>("animals.json") ?? new();
|
||||||
|
var litters = Load<List<SourceLitter>>("litters.json") ?? new();
|
||||||
|
if (animals.Count == 0 && litters.Count == 0)
|
||||||
|
notes.Add($"Keine Quelldaten gefunden in {_sourceDir} (animals.json/litters.json). extract.py zuerst ausführen.");
|
||||||
|
|
||||||
|
// ---- categorise animals ----
|
||||||
|
var loadable = new List<SourceAnimal>();
|
||||||
|
int conflicts = 0, stubs = 0, dateOnly = 0, ambiguous = 0;
|
||||||
|
foreach (var a in animals)
|
||||||
|
{
|
||||||
|
if (a.Conflict) { conflicts++; continue; }
|
||||||
|
if (string.IsNullOrEmpty(a.Dob)) { stubs++; continue; }
|
||||||
|
loadable.Add(a);
|
||||||
|
var conf = a.LitterRef?.Confidence;
|
||||||
|
if (a.LitterRef?.Candidates is { Count: > 0 }) ambiguous++;
|
||||||
|
else if (conf == "niedrig") dateOnly++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// existing rows (idempotency). Gerbils carry ExternalRef; Litters have no such
|
||||||
|
// column, so we key litter idempotency on the stable (Name + Date) pair instead.
|
||||||
|
var existingGerbilExtRefs = await _db.Gerbils
|
||||||
|
.Where(g => g.ExternalRef != null)
|
||||||
|
.Select(g => g.ExternalRef!).ToListAsync();
|
||||||
|
var existingGerbilSet = existingGerbilExtRefs.ToHashSet();
|
||||||
|
|
||||||
|
var existingLitterKeys = await _db.Litters
|
||||||
|
.Select(l => new { l.Name, l.Date }).ToListAsync();
|
||||||
|
var existingLitterKeySet = existingLitterKeys
|
||||||
|
.Select(x => $"{x.Name}|{x.Date:yyyy-MM-dd}").ToHashSet();
|
||||||
|
|
||||||
|
// colour-variety name -> id (case-insensitive)
|
||||||
|
var varieties = await _db.ColorVarieties.Select(v => new { v.Id, v.Name }).ToListAsync();
|
||||||
|
var varietyByName = varieties
|
||||||
|
.GroupBy(v => v.Name.Trim().ToLowerInvariant())
|
||||||
|
.ToDictionary(g => g.Key, g => g.First().Id);
|
||||||
|
|
||||||
|
// gender inference from litter roles (sire -> male, dam -> female; both -> unknown)
|
||||||
|
var sireNames = litters.Select(l => Normalize(StripZucht(l.SireName))).Where(s => s.Length > 0).ToHashSet();
|
||||||
|
var damNames = litters.Select(l => Normalize(StripZucht(l.DamName))).Where(s => s.Length > 0).ToHashSet();
|
||||||
|
|
||||||
|
// ---- litters: create map source.id -> Litter (for high-confidence animal links) ----
|
||||||
|
int littersCreated = 0, littersExisting = 0;
|
||||||
|
var litterIdMap = new Dictionary<string, Guid>(); // source litter id -> Litter.Id
|
||||||
|
foreach (var sl in litters)
|
||||||
|
{
|
||||||
|
var date = ParseDate(sl.Date);
|
||||||
|
var name = $"Wurf {sl.LitterId}".Trim();
|
||||||
|
var key = $"{name}|{date:yyyy-MM-dd}";
|
||||||
|
if (existingLitterKeySet.Contains(key)) { littersExisting++; continue; }
|
||||||
|
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
litterIdMap[sl.Id] = id;
|
||||||
|
littersCreated++;
|
||||||
|
if (execute && date is DateOnly d)
|
||||||
|
{
|
||||||
|
_db.Litters.Add(new Litter
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
Date = d,
|
||||||
|
TotalBorn = sl.TotalBorn,
|
||||||
|
Notes = string.IsNullOrWhiteSpace(sl.Note) ? null : sl.Note,
|
||||||
|
PairingCode = string.IsNullOrWhiteSpace(sl.Zuchtnummer) ? null : sl.Zuchtnummer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (samples.Count < 8 && date is not null)
|
||||||
|
samples.Add($"Wurf: {name} ({sl.Date}) — {sl.DamName} × {sl.SireName}");
|
||||||
|
}
|
||||||
|
if (execute) await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// ---- animals ----
|
||||||
|
int animalsCreated = 0, linked = 0, fbMatched = 0, fbUnmatched = 0, animalsExisting = 0;
|
||||||
|
int photosAttached = 0, photosMissing = 0;
|
||||||
|
var createdAnimalByName = new Dictionary<string, Guid>(); // normalized name -> gerbil id (for litter back-link)
|
||||||
|
|
||||||
|
foreach (var a in loadable)
|
||||||
|
{
|
||||||
|
if (existingGerbilSet.Contains(a.Id)) { animalsExisting++; continue; }
|
||||||
|
animalsCreated++;
|
||||||
|
|
||||||
|
Guid? litterId = null;
|
||||||
|
if (a.LitterRef?.Confidence == "hoch" && a.LitterRef.Candidates is not { Count: > 0 }
|
||||||
|
&& litterIdMap.TryGetValue(a.LitterRef.LitterId, out var lid))
|
||||||
|
{
|
||||||
|
litterId = lid;
|
||||||
|
linked++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid? colorVarietyId = null;
|
||||||
|
var fbCandidates = new[] { a.Farbschlag }.Concat(a.FarbschlagVariants)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||||
|
foreach (var fb in fbCandidates)
|
||||||
|
{
|
||||||
|
if (varietyByName.TryGetValue(fb.Trim().ToLowerInvariant(), out var vid))
|
||||||
|
{ colorVarietyId = vid; break; }
|
||||||
|
}
|
||||||
|
if (colorVarietyId is null) fbUnmatched++; else fbMatched++;
|
||||||
|
|
||||||
|
var gender = InferGender(a, sireNames, damNames);
|
||||||
|
var gid = Guid.NewGuid();
|
||||||
|
var norm = Normalize(StripZucht(a.Name));
|
||||||
|
if (norm.Length > 0) createdAnimalByName.TryAdd(norm, gid);
|
||||||
|
|
||||||
|
if (samples.Count < 16)
|
||||||
|
samples.Add($"Tier: {a.Name} (*{a.Dob}), Genotyp {ComposeGenotype(a.Genotype)}"
|
||||||
|
+ (litterId is not null ? ", Wurf-verknüpft" : "")
|
||||||
|
+ (colorVarietyId is not null ? $", Farbschlag „{a.Farbschlag}\"" : ""));
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
_db.Gerbils.Add(new Gerbil
|
||||||
|
{
|
||||||
|
Id = gid,
|
||||||
|
Name = a.Name,
|
||||||
|
Gender = gender,
|
||||||
|
Status = GerbilStatus.Active,
|
||||||
|
DateOfBirth = ParseDate(a.Dob),
|
||||||
|
DateOfDeath = ParseDate(a.Death),
|
||||||
|
LitterId = litterId,
|
||||||
|
ColorVarietyId = colorVarietyId,
|
||||||
|
Genotype = ComposeGenotype(a.Genotype),
|
||||||
|
ImportSource = ImportSourceTag,
|
||||||
|
ExternalRef = a.Id,
|
||||||
|
RawImportData = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
a.Genotype.RawGenotype,
|
||||||
|
a.Genotype.UnmappedTokens,
|
||||||
|
a.Zucht,
|
||||||
|
a.SourceFiles,
|
||||||
|
FarbschlagRaw = a.Farbschlag,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// photos
|
||||||
|
foreach (var rel in a.Photos)
|
||||||
|
{
|
||||||
|
var src = Path.Combine(_sourceDir, rel.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
if (!File.Exists(src)) { photosMissing++; continue; }
|
||||||
|
photosAttached++;
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_photoRoot);
|
||||||
|
var fileName = $"{Guid.NewGuid():N}{Path.GetExtension(src)}";
|
||||||
|
File.Copy(src, Path.Combine(_photoRoot, fileName), overwrite: true);
|
||||||
|
_db.GerbilPhotos.Add(new GerbilPhoto
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
GerbilId = gid,
|
||||||
|
FileName = fileName,
|
||||||
|
SortOrder = 0,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (execute) await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// ---- back-link litter parents by name (best effort) ----
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
foreach (var sl in litters)
|
||||||
|
{
|
||||||
|
if (!litterIdMap.TryGetValue(sl.Id, out var lid)) continue;
|
||||||
|
var litter = await _db.Litters.FirstOrDefaultAsync(l => l.Id == lid);
|
||||||
|
if (litter is null) continue;
|
||||||
|
if (createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.SireName)), out var fId))
|
||||||
|
litter.FatherId = fId;
|
||||||
|
if (createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.DamName)), out var mId))
|
||||||
|
litter.MotherId = mId;
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
||||||
|
if (!execute) notes.Add("DRY-RUN: nichts gespeichert. /import/execute lädt die konfliktfreien Daten.");
|
||||||
|
|
||||||
|
return new ImportReport(
|
||||||
|
Executed: execute,
|
||||||
|
Litters: new LitterSummary(litters.Count, littersCreated, littersExisting),
|
||||||
|
Animals: new AnimalSummary(
|
||||||
|
animals.Count, animalsCreated, linked, fbMatched, fbUnmatched, animalsExisting,
|
||||||
|
new QuarantineSummary(conflicts, stubs, dateOnly, ambiguous, conflicts + stubs)),
|
||||||
|
Photos: new PhotoSummary(photosAttached, photosMissing),
|
||||||
|
Samples: samples,
|
||||||
|
Notes: notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? Load<T>(string file)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_sourceDir, file);
|
||||||
|
if (!File.Exists(path)) return default;
|
||||||
|
using var fs = File.OpenRead(path);
|
||||||
|
return JsonSerializer.Deserialize<T>(fs, Json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ComposeGenotype(SourceGenotype g)
|
||||||
|
{
|
||||||
|
var tokens = LocusOrder.Select(locus =>
|
||||||
|
{
|
||||||
|
if (g.Mapped8locus.TryGetValue(locus, out var pair) && pair.Count == 2)
|
||||||
|
return StripCaret(pair[0]) + StripCaret(pair[1]);
|
||||||
|
return "??";
|
||||||
|
});
|
||||||
|
return string.Join(' ', tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripCaret(string allele) => allele.Replace("^", "");
|
||||||
|
|
||||||
|
private static Gender InferGender(SourceAnimal a, HashSet<string> sires, HashSet<string> dams)
|
||||||
|
{
|
||||||
|
var n = Normalize(StripZucht(a.Name));
|
||||||
|
bool isSire = sires.Contains(n), isDam = dams.Contains(n);
|
||||||
|
if (isSire && !isDam) return Gender.male;
|
||||||
|
if (isDam && !isSire) return Gender.female;
|
||||||
|
return Gender.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateOnly? ParseDate(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
||||||
|
var m = Regex.Match(s, @"(\d{1,2})\.(\d{1,2})\.(\d{2,4})");
|
||||||
|
if (!m.Success) return null;
|
||||||
|
int d = int.Parse(m.Groups[1].Value), mo = int.Parse(m.Groups[2].Value);
|
||||||
|
int y = int.Parse(m.Groups[3].Value);
|
||||||
|
if (y < 100) y += 2000;
|
||||||
|
try { return new DateOnly(y, mo, d); }
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripZucht(string name)
|
||||||
|
{
|
||||||
|
var n = Regex.Replace(name ?? "", @"\[.*?\]", " ");
|
||||||
|
n = Regex.Replace(n, @"\s+(?:of|von\s+den|von\s+der|v\.\s?d\.|von)\s+.*$", "", RegexOptions.IgnoreCase);
|
||||||
|
return n.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string name)
|
||||||
|
{
|
||||||
|
var n = (name ?? "").ToLowerInvariant();
|
||||||
|
n = Regex.Replace(n, @"\bgen\.\b", " ");
|
||||||
|
n = Regex.Replace(n, @"[^a-z0-9äöüß]", "");
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
897
GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.Designer.cs
generated
Normal file
897
GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.Designer.cs
generated
Normal file
@@ -0,0 +1,897 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationContext))]
|
||||||
|
[Migration("20260606052952_AddImportProvenanceFields")]
|
||||||
|
partial class AddImportProvenanceFields
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.8")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CanonicalGenotype")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ColorVarieties");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000001"),
|
||||||
|
CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
|
||||||
|
Name = "Pink Eyed White (PEW)",
|
||||||
|
SortOrder = 0
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000002"),
|
||||||
|
CanonicalGenotype = "aa chch DD EE GG PP spsp rere",
|
||||||
|
Name = "Hermelin",
|
||||||
|
SortOrder = 1
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000003"),
|
||||||
|
CanonicalGenotype = "AA chch DD EE GG PP spsp rere",
|
||||||
|
Name = "Himalaya",
|
||||||
|
SortOrder = 2
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000004"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
|
||||||
|
Name = "Zobel",
|
||||||
|
SortOrder = 3
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000005"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
|
||||||
|
Name = "Schwarzschimmel",
|
||||||
|
SortOrder = 4
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000006"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
|
||||||
|
Name = "Rotaugenschimmel",
|
||||||
|
SortOrder = 5
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000007"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE GG PP spsp rere",
|
||||||
|
Name = "Agouti",
|
||||||
|
SortOrder = 6
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000008"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE GG PP spsp rere",
|
||||||
|
Name = "Schwarz",
|
||||||
|
SortOrder = 7
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000009"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE gg PP spsp rere",
|
||||||
|
Name = "Silberagouti",
|
||||||
|
SortOrder = 8
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000010"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE gg PP spsp rere",
|
||||||
|
Name = "Anthrazit",
|
||||||
|
SortOrder = 9
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000011"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
|
||||||
|
Name = "Algierfuchs",
|
||||||
|
SortOrder = 10
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000012"),
|
||||||
|
CanonicalGenotype = "aa CC dd EE GG PP spsp rere",
|
||||||
|
Name = "Blau",
|
||||||
|
SortOrder = 11
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000013"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Gold",
|
||||||
|
SortOrder = 12
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000014"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Platin",
|
||||||
|
SortOrder = 13
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000015"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
|
||||||
|
Name = "Goldfuchs",
|
||||||
|
SortOrder = 14
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000016"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
|
||||||
|
Name = "Rotfuchs",
|
||||||
|
SortOrder = 15
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000017"),
|
||||||
|
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
||||||
|
Name = "dd Gold",
|
||||||
|
SortOrder = 16
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000018"),
|
||||||
|
CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
|
||||||
|
Name = "dd Platin",
|
||||||
|
SortOrder = 17
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000019"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE gg pp spsp rere",
|
||||||
|
Name = "Altweiss (REW)",
|
||||||
|
SortOrder = 18
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000020"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee gg pp spsp rere",
|
||||||
|
Name = "Apricot (Blassfuchs)",
|
||||||
|
SortOrder = 19
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000021"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
|
||||||
|
Name = "Blaufuchs",
|
||||||
|
SortOrder = 20
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000022"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee gg pp spsp rere",
|
||||||
|
Name = "C-Separator",
|
||||||
|
SortOrder = 21
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000023"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE gg pp spsp rere",
|
||||||
|
Name = "Elfenbein",
|
||||||
|
SortOrder = 22
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000024"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs",
|
||||||
|
SortOrder = 23
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000025"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
|
||||||
|
Name = "Marder",
|
||||||
|
SortOrder = 24
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000026"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
|
||||||
|
Name = "Siam (Marder-Hell)",
|
||||||
|
SortOrder = 25
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000027"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
|
||||||
|
Name = "Polarfuchs",
|
||||||
|
SortOrder = 26
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000028"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Saphir",
|
||||||
|
SortOrder = 27
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000029"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
|
||||||
|
Name = "Schimmel (Orangeschimmel)",
|
||||||
|
SortOrder = 28
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000030"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Topas",
|
||||||
|
SortOrder = 29
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000031"),
|
||||||
|
CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Platin-Hell",
|
||||||
|
SortOrder = 30
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000032"),
|
||||||
|
CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
|
||||||
|
Name = "Agouti dd",
|
||||||
|
SortOrder = 31
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000033"),
|
||||||
|
CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
|
||||||
|
Name = "Silberagouti dd",
|
||||||
|
SortOrder = 32
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000034"),
|
||||||
|
CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs dd",
|
||||||
|
SortOrder = 33
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000035"),
|
||||||
|
CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
|
||||||
|
Name = "Anthrazit dd",
|
||||||
|
SortOrder = 34
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000036"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
|
||||||
|
Name = "Agouti CP-Hell",
|
||||||
|
SortOrder = 35
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000037"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD ee gg PP spsp rere",
|
||||||
|
Name = "Blaufuchs CP",
|
||||||
|
SortOrder = 36
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000038"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
||||||
|
Name = "Polarfuchsschimmel",
|
||||||
|
SortOrder = 37
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000039"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
||||||
|
Name = "Silberschimmel",
|
||||||
|
SortOrder = 38
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000040"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
|
||||||
|
Name = "Algierfuchsschimmel",
|
||||||
|
SortOrder = 39
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000041"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
|
||||||
|
Name = "Polarfuchs-Hell CP",
|
||||||
|
SortOrder = 40
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000042"),
|
||||||
|
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchsschimmel",
|
||||||
|
SortOrder = 41
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000043"),
|
||||||
|
CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
|
||||||
|
Name = "Blaufuchsschimmel",
|
||||||
|
SortOrder = 42
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000044"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs, hell",
|
||||||
|
SortOrder = 43
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000045"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
|
||||||
|
Name = "Goldfuchs, hell",
|
||||||
|
SortOrder = 44
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000046"),
|
||||||
|
CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
|
||||||
|
Name = "Goldfuchsschimmel",
|
||||||
|
SortOrder = 45
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000047"),
|
||||||
|
CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
|
||||||
|
Name = "Gold-Hell",
|
||||||
|
SortOrder = 46
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000048"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
|
||||||
|
Name = "Siam (Marder-Hell) dd",
|
||||||
|
SortOrder = 47
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000049"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
|
||||||
|
Name = "Marder dd",
|
||||||
|
SortOrder = 48
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000050"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
|
||||||
|
Name = "Zobel-Hell",
|
||||||
|
SortOrder = 49
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000051"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
|
||||||
|
Name = "Silberagouti dd CP",
|
||||||
|
SortOrder = 50
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000052"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
|
||||||
|
Name = "Silberagouti-Hell dd CP",
|
||||||
|
SortOrder = 51
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000053"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
|
||||||
|
Name = "Agouti dd CP",
|
||||||
|
SortOrder = 52
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000054"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
|
||||||
|
Name = "Agouti-Hell dd CP",
|
||||||
|
SortOrder = 53
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000055"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
|
||||||
|
Name = "Blaufuchs, hell",
|
||||||
|
SortOrder = 54
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000056"),
|
||||||
|
CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
|
||||||
|
Name = "Rotfuchsschimmel",
|
||||||
|
SortOrder = 55
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000057"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
|
||||||
|
Name = "Polarfuchs, hell",
|
||||||
|
SortOrder = 56
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000058"),
|
||||||
|
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchsschimmel, hell",
|
||||||
|
SortOrder = 57
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000059"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
|
||||||
|
Name = "Rotfuchs, hell",
|
||||||
|
SortOrder = 58
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000060"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
|
||||||
|
Name = "Zobel dd",
|
||||||
|
SortOrder = 59
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000061"),
|
||||||
|
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs-Hell",
|
||||||
|
SortOrder = 60
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000062"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs CP",
|
||||||
|
SortOrder = 61
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000063"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
|
||||||
|
Name = "Algierfuchs CP",
|
||||||
|
SortOrder = 62
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000064"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
|
||||||
|
Name = "Silberagouti CP",
|
||||||
|
SortOrder = 63
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000065"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
|
||||||
|
Name = "Agouti CP",
|
||||||
|
SortOrder = 64
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000066"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
|
||||||
|
Name = "Algierfuchs-Hell CP",
|
||||||
|
SortOrder = 65
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000067"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchs,hell CP",
|
||||||
|
SortOrder = 66
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000068"),
|
||||||
|
CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
|
||||||
|
Name = "Polarfuchs CP",
|
||||||
|
SortOrder = 67
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000069"),
|
||||||
|
CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
|
||||||
|
Name = "Algierfuchs, hell",
|
||||||
|
SortOrder = 68
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000070"),
|
||||||
|
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
||||||
|
Name = "Topas dd",
|
||||||
|
SortOrder = 69
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000071"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
|
||||||
|
Name = "Zobel-Hell dd",
|
||||||
|
SortOrder = 70
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000072"),
|
||||||
|
CanonicalGenotype = "aa cchmcchm DD efef GG PP spsp rere",
|
||||||
|
Name = "Kohlfuchsschimmel CP",
|
||||||
|
SortOrder = 71
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000073"),
|
||||||
|
CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
|
||||||
|
Name = "Blaufuchs dd",
|
||||||
|
SortOrder = 72
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Contacts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Enclosures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CauseOfDeath")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ColorVarietyId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("DateOfBirth")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("DateOfDeath")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<Guid?>("EnclosureId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalRef")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Genotype")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("GoHomeDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("ImportSource")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("LitterId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OriginContactId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("RawImportData")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ReceiverContactId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ColorVarietyId");
|
||||||
|
|
||||||
|
b.HasIndex("EnclosureId");
|
||||||
|
|
||||||
|
b.HasIndex("LitterId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginContactId");
|
||||||
|
|
||||||
|
b.HasIndex("ReceiverContactId");
|
||||||
|
|
||||||
|
b.ToTable("Gerbils");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Caption")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("GerbilId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GerbilId");
|
||||||
|
|
||||||
|
b.ToTable("GerbilPhotos");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("GerbilId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Veterinarian")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GerbilId");
|
||||||
|
|
||||||
|
b.ToTable("HealthRecords");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("ExpectedGoHomeDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FatherId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("MotherId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PairingCode")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("TotalBorn")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FatherId");
|
||||||
|
|
||||||
|
b.HasIndex("MotherId");
|
||||||
|
|
||||||
|
b.ToTable("Litters");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<Guid>("GerbilId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("WeightGrams")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GerbilId");
|
||||||
|
|
||||||
|
b.ToTable("WeightRecords");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ColorVarietyId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure")
|
||||||
|
.WithMany("Gerbils")
|
||||||
|
.HasForeignKey("EnclosureId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("LitterId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OriginContactId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ReceiverContactId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("ColorVariety");
|
||||||
|
|
||||||
|
b.Navigation("Enclosure");
|
||||||
|
|
||||||
|
b.Navigation("Litter");
|
||||||
|
|
||||||
|
b.Navigation("OriginContact");
|
||||||
|
|
||||||
|
b.Navigation("ReceiverContact");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GerbilId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GerbilId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FatherId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MotherId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Father");
|
||||||
|
|
||||||
|
b.Navigation("Mother");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GerbilId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Gerbils");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddImportProvenanceFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PairingCode",
|
||||||
|
table: "Litters",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "RawImportData",
|
||||||
|
table: "Gerbils",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PairingCode",
|
||||||
|
table: "Litters");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RawImportData",
|
||||||
|
table: "Gerbils");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -650,6 +650,9 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
b.Property<Guid?>("OriginContactId")
|
b.Property<Guid?>("OriginContactId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("RawImportData")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<Guid?>("ReceiverContactId")
|
b.Property<Guid?>("ReceiverContactId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -759,6 +762,9 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
b.Property<string>("Notes")
|
b.Property<string>("Notes")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PairingCode")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<int?>("TotalBorn")
|
b.Property<int?>("TotalBorn")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
|||||||
@@ -46,5 +46,9 @@ namespace GerbilManagerWebAPI.Models
|
|||||||
// Provenance (for the FEAT-8 spreadsheet import).
|
// Provenance (for the FEAT-8 spreadsheet import).
|
||||||
public string? ImportSource { get; set; }
|
public string? ImportSource { get; set; }
|
||||||
public string? ExternalRef { get; set; }
|
public string? ExternalRef { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
|
||||||
|
/// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.</summary>
|
||||||
|
public string? RawImportData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,9 @@ namespace GerbilManagerWebAPI.Models
|
|||||||
/// <summary>Computed ~35 days after Date by default; editable.</summary>
|
/// <summary>Computed ~35 days after Date by default; editable.</summary>
|
||||||
public DateOnly? ExpectedGoHomeDate { get; set; }
|
public DateOnly? ExpectedGoHomeDate { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Zuchtnummer der Verpaarung (Wurfchronik col H) — pairing-level code;
|
||||||
|
/// litters sharing it are the same Zuchtpaar. Set by the FEAT-8 import.</summary>
|
||||||
|
public string? PairingCode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,5 +67,6 @@ app.MapWeightRecordEndpoints();
|
|||||||
app.MapInbreedingEndpoints();
|
app.MapInbreedingEndpoints();
|
||||||
app.MapPhotoEndpoints();
|
app.MapPhotoEndpoints();
|
||||||
app.MapSaleAdEndpoints();
|
app.MapSaleAdEndpoints();
|
||||||
|
app.MapImportEndpoints();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -554,10 +554,14 @@ def dedup(animals):
|
|||||||
"photos": sorted(set(photos)),
|
"photos": sorted(set(photos)),
|
||||||
"sourceFiles": sorted(files),
|
"sourceFiles": sorted(files),
|
||||||
"mentions": len(grp),
|
"mentions": len(grp),
|
||||||
|
# FEAT-8c: machine-readable quarantine marker so the API loader can skip
|
||||||
|
# conflicting records without parsing the German review report.
|
||||||
|
"conflict": False,
|
||||||
}
|
}
|
||||||
merged.append(out)
|
merged.append(out)
|
||||||
# conflict: same animal, disagreeing genotype or farbschlag or death
|
# conflict: same animal, disagreeing genotype or farbschlag or death
|
||||||
if len(genos) > 1 or len(farb) > 1 or len(deaths) > 1:
|
if len(genos) > 1 or len(farb) > 1 or len(deaths) > 1:
|
||||||
|
out["conflict"] = True
|
||||||
conflicts.append({
|
conflicts.append({
|
||||||
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
"id": out["id"], "name": base["name"], "dob": out["dob"],
|
||||||
"genotypes": sorted(genos), "farbschlaege": sorted(farb),
|
"genotypes": sorted(genos), "farbschlaege": sorted(farb),
|
||||||
|
|||||||
Reference in New Issue
Block a user