From 2f089a902d8e5837e001753fb7ff6900a4d4b86d Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 10:26:01 +0200 Subject: [PATCH] GEN-3b: import notation normalization (Uw=G, Sls, deaf flag, tags) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit genotype.py: - Uw/uw aliased to G/g (same locus) so the D2 conflict group + pure-Uw cases stop being conflicts (Gg == Uwuw). - Sls/WP recognized as a SECOND spotting locus (S(l)s(l)=WP het); carried into mapped8locus alongside Sp (Sp+Sls = Superschecke). - dea/Dea/taub/hörend -> hearing/deaf phenotype FLAG (not a locus). - WFNZ/RV/GV/DP -> provenance/breeding tags (not genotype, not conflicts). - test_genotype.py: zero-dep unit tests for all four. extract.py: surface deaf+tags on animals; dedup conflict detection now compares the NORMALIZED genotype key (mapped8locus) instead of the raw string, so Uw=G no longer triggers a conflict. Result: Konflikte 32 -> 27, Zucht-Splits stays 0. Dedup identity = name + DOB + Zucht. Backend: Gerbil.IsDeaf (bool?) + additive migration AddGerbilDeafFlag (has-pending-model-changes clean) + GerbilDto/GerbilInput round-trip. ImportService sets IsDeaf from animal.deaf and preserves Sls + tags + deaf in RawImportData (kept out of the 8-locus compact Genotype contract until GEN-3a adopts them). Co-Authored-By: Claude Opus 4.8 (1M context) --- GerbilManager.Tests/ImportServiceTests.cs | 20 +- GerbilManagerWebAPI/Dtos/ApiDtos.cs | 6 +- .../Endpoints/GerbilEndpoints.cs | 3 +- GerbilManagerWebAPI/Import/ImportModels.cs | 5 + GerbilManagerWebAPI/Import/ImportService.cs | 6 + ...260606082045_AddGerbilDeafFlag.Designer.cs | 1381 +++++++++++++++++ .../20260606082045_AddGerbilDeafFlag.cs | 28 + .../ApplicationContextModelSnapshot.cs | 3 + GerbilManagerWebAPI/Models/Gerbil.cs | 6 + tools/import/extract.py | 32 +- tools/import/genotype.py | 106 +- tools/import/output/review-report.md | 40 +- tools/import/test_genotype.py | 71 + 13 files changed, 1645 insertions(+), 62 deletions(-) create mode 100644 GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.cs create mode 100644 tools/import/test_genotype.py diff --git a/GerbilManager.Tests/ImportServiceTests.cs b/GerbilManager.Tests/ImportServiceTests.cs index 5be8ea1..f3da78e 100644 --- a/GerbilManager.Tests/ImportServiceTests.cs +++ b/GerbilManager.Tests/ImportServiceTests.cs @@ -102,6 +102,23 @@ namespace GerbilManager.Tests Assert.Equal(2, await db.Litters.CountAsync()); } + [Fact] + public async Task Execute_persists_deaf_flag_and_preserves_sls_and_tags() + { + using var db = NewDb(); + await new ImportService(db, _dir, _dir).RunAsync(execute: true); + + var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1"); + // GEN-3b: deafness is a persisted phenotype flag (NOT a genotype locus). + Assert.True(a1.IsDeaf); + // Sls (2nd spotting locus) + provenance tags are preserved in RawImportData + // (kept out of the 8-locus compact Genotype contract until GEN-3a adopts them). + Assert.Contains("Sls", a1.RawImportData!); + Assert.Contains("WFNZ", a1.RawImportData!); + // and Sls must NOT leak into the compact 8-locus genotype string + Assert.DoesNotContain("Sl", a1.Genotype!); + } + [Fact] public void ComposeGenotype_strips_carets_and_fills_missing_loci() { @@ -139,7 +156,8 @@ namespace GerbilManager.Tests [ {"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":[]}, + "genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"],"Sls":["Sl","sl"]},"rawGenotype":"aa CC D- ee[f] WP dea WFNZ","unmappedTokens":[]}, + "deaf":true,"tags":["WFNZ"], "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, diff --git a/GerbilManagerWebAPI/Dtos/ApiDtos.cs b/GerbilManagerWebAPI/Dtos/ApiDtos.cs index 8208080..54c38be 100644 --- a/GerbilManagerWebAPI/Dtos/ApiDtos.cs +++ b/GerbilManagerWebAPI/Dtos/ApiDtos.cs @@ -27,7 +27,8 @@ namespace GerbilManagerWebAPI.Dtos string? ExternalRef, string? OriginBreeder, List CharacterTraits, - string? CharacterNote); + string? CharacterNote, + bool? IsDeaf); public record LitterDto( Guid Id, @@ -75,7 +76,8 @@ namespace GerbilManagerWebAPI.Dtos string? ExternalRef, string? OriginBreeder, List? CharacterTraits, - string? CharacterNote); + string? CharacterNote, + bool? IsDeaf); public record LitterInput( string Name, diff --git a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs index efeaac9..bee78af 100644 --- a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs @@ -97,12 +97,13 @@ namespace GerbilManagerWebAPI.Endpoints g.OriginBreeder = i.OriginBreeder; g.CharacterTraits = i.CharacterTraits ?? new List(); g.CharacterNote = i.CharacterNote; + g.IsDeaf = i.IsDeaf; } internal static GerbilDto ToDto(Gerbil g) => new( g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId, g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath, g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder, - g.CharacterTraits, g.CharacterNote); + g.CharacterTraits, g.CharacterNote, g.IsDeaf); } } diff --git a/GerbilManagerWebAPI/Import/ImportModels.cs b/GerbilManagerWebAPI/Import/ImportModels.cs index adecdb4..1aad635 100644 --- a/GerbilManagerWebAPI/Import/ImportModels.cs +++ b/GerbilManagerWebAPI/Import/ImportModels.cs @@ -21,6 +21,11 @@ namespace GerbilManagerWebAPI.Import public List SourceFiles { get; set; } = new(); public bool Conflict { get; set; } public SourceLitterRef? LitterRef { get; set; } + + // GEN-3b normalization: hearing/deaf phenotype flag (null = not stated) and + // provenance/breeding tags (WFNZ/RV/GV/DP) — neither is genotype. + public bool? Deaf { get; set; } + public List Tags { get; set; } = new(); } public sealed class SourceGenotype diff --git a/GerbilManagerWebAPI/Import/ImportService.cs b/GerbilManagerWebAPI/Import/ImportService.cs index e3eb906..4142c90 100644 --- a/GerbilManagerWebAPI/Import/ImportService.cs +++ b/GerbilManagerWebAPI/Import/ImportService.cs @@ -174,6 +174,7 @@ namespace GerbilManagerWebAPI.Import LitterId = litterId, ColorVarietyId = colorVarietyId, Genotype = ComposeGenotype(a.Genotype), + IsDeaf = a.Deaf, ImportSource = ImportSourceTag, ExternalRef = a.Id, OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(), @@ -181,6 +182,11 @@ namespace GerbilManagerWebAPI.Import { a.Genotype.RawGenotype, a.Genotype.UnmappedTokens, + // GEN-3b: Sls (2nd spotting locus) preserved here until Kevin's GEN-3a + // parser adopts it into the compact Genotype contract; tags + deaf too. + Sls = a.Genotype.Mapped8locus.TryGetValue("Sls", out var sls) ? sls : null, + a.Tags, + a.Deaf, a.Zucht, a.SourceFiles, FarbschlagRaw = a.Farbschlag, diff --git a/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs b/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs new file mode 100644 index 0000000..7336cea --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.Designer.cs @@ -0,0 +1,1381 @@ +// +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("20260606082045_AddGerbilDeafFlag")] + partial class AddGerbilDeafFlag + { + /// + 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.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSearch") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + 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"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.cs b/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.cs new file mode 100644 index 0000000..0b54beb --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260606082045_AddGerbilDeafFlag.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddGerbilDeafFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDeaf", + table: "Gerbils", + type: "boolean", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDeaf", + table: "Gerbils"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index 177faf8..ab08ac7 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -781,6 +781,9 @@ namespace GerbilManagerWebAPI.Migrations b.Property("ImportSource") .HasColumnType("text"); + b.Property("IsDeaf") + .HasColumnType("boolean"); + b.Property("LitterId") .HasColumnType("uuid"); diff --git a/GerbilManagerWebAPI/Models/Gerbil.cs b/GerbilManagerWebAPI/Models/Gerbil.cs index b1c75ca..1c39c8b 100644 --- a/GerbilManagerWebAPI/Models/Gerbil.cs +++ b/GerbilManagerWebAPI/Models/Gerbil.cs @@ -67,6 +67,12 @@ namespace GerbilManagerWebAPI.Models /// FEAT-14: free-text character note; feeds the AI Verkaufstext. public string? CharacterNote { get; set; } + + /// GEN-3b: hearing/deaf phenotype flag (NOT a genotype locus — it's the + /// downstream effect of high white load / Sp×Sls). null = not stated, true = deaf + /// (dea/taub), false = hearing (Dea/hörend). Set by the FEAT-8 import from the + /// after-spsp deafness annotation; see hive/agents/god/GENETIK-notation.md. + public bool? IsDeaf { get; set; } } /// Shared normalisation for the separator-insensitive name search. diff --git a/tools/import/extract.py b/tools/import/extract.py index 4cfabcf..460519b 100644 --- a/tools/import/extract.py +++ b/tools/import/extract.py @@ -227,6 +227,8 @@ def extract_stammbaum(path): "gender": None, "farbschlag": farbschlag, "genotype": genodict, + "deaf": genodict.get("deaf"), + "tags": genodict.get("tags", []), "breeder": breeder, "zucht": zraw, "parentRefs": [], @@ -251,7 +253,8 @@ def extract_stammbaum(path): animals.append({ "id": None, "name": part, "nameVariants": [], "dob": "", "death": "", "gender": None, "farbschlag": "", - "genotype": gt.parse(""), "breeder": "", "zucht": zraw, + "genotype": gt.parse(""), "deaf": None, "tags": [], + "breeder": "", "zucht": zraw, "parentRefs": [], "photos": [], "sourceFiles": [fname], "_gen": gen_of(c), "_col": c, "_row": r, "_file": fname, "_zucht": norm_zucht(zraw), @@ -472,10 +475,17 @@ def _to_int(s): # ------------------------------------------------------------- stage 2: dedup +def _geno_key(genodict): + """Canonical, order-independent key of a genotype's mapped loci — used for conflict + detection so Uw==G (and allele ordering) no longer count as a conflict.""" + m = genodict.get("mapped8locus", {}) + return "|".join(f"{locus}:{','.join(sorted(m[locus]))}" for locus in sorted(m)) + + def dedup(animals): """Merge by normalise(call-name)+DOB, with the canonical Zucht as - DISCRIMINATOR (Julian: same name+DOB but different Zucht = different - animal). Returns (merged, conflicts, orphans, zucht_splits).""" + DISCRIMINATOR (Julian: same name+DOB+Zucht = same animal; different Zucht = + different animal). Returns (merged, conflicts, orphans, zucht_splits).""" groups = {} orphans = [] for a in animals: @@ -521,19 +531,26 @@ def dedup(animals): photos = list(base["photos"]) parent_refs = list(base["parentRefs"]) genos = set() + geno_keys = set() # GEN-3b: conflict on NORMALIZED genotype (Uw==G) not raw text farb = set() deaths = set() + deaf_seen = set() + tags_set = set() for a in grp: variants.add(a["name"]) files.update(a["sourceFiles"]) photos.extend(a["photos"]) parent_refs.extend(a["parentRefs"]) - if a["genotype"]["rawGenotype"]: + if a["genotype"]["mapped8locus"]: genos.add(a["genotype"]["rawGenotype"]) + geno_keys.add(_geno_key(a["genotype"])) if a["farbschlag"]: farb.add(a["farbschlag"]) if a["death"]: deaths.add(norm_dob(a["death"])) + if a.get("deaf") is not None: + deaf_seen.add(a["deaf"]) + tags_set.update(a.get("tags", [])) # pick the richest genotype (most mapped loci, then longest raw) best = max((a["genotype"] for a in grp), key=lambda gd: (len(gd["mapped8locus"]), len(gd["rawGenotype"]))) @@ -554,13 +571,16 @@ def dedup(animals): "photos": sorted(set(photos)), "sourceFiles": sorted(files), "mentions": len(grp), + # GEN-3b: hearing/deaf phenotype flag (deaf wins if any mention says so) + tags. + "deaf": (True if True in deaf_seen else (False if False in deaf_seen else None)), + "tags": sorted(tags_set), # 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) - # conflict: same animal, disagreeing genotype or farbschlag or death - if len(genos) > 1 or len(farb) > 1 or len(deaths) > 1: + # conflict: same animal, disagreeing NORMALIZED genotype (Uw==G) or farbschlag or death + if len(geno_keys) > 1 or len(farb) > 1 or len(deaths) > 1: out["conflict"] = True conflicts.append({ "id": out["id"], "name": base["name"], "dob": out["dob"], diff --git a/tools/import/genotype.py b/tools/import/genotype.py index 3807e53..d196ec7 100644 --- a/tools/import/genotype.py +++ b/tools/import/genotype.py @@ -1,20 +1,26 @@ -"""Parse the breeder's free-text genotype notation into our frozen 8-locus -contract while losing nothing (FEAT-8b ruling from god): +"""Parse the breeder's free-text genotype notation into our locus model while +losing nothing (FEAT-8b + GEN-3b normalization, per hive/agents/god/GENETIK-notation.md): - - mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re - - rawGenotype : the verbatim source string - - unmappedTokens: tokens we couldn't map (Uw/Sls/Dea, markers like WFNZ/WP/DP, …) + - mapped8locus : {locus: [allele1, allele2]} for A C D E G P Sp Re (+ Sls when present) + - rawGenotype : the verbatim source string + - unmappedTokens: tokens we still couldn't place + - deaf : True (dea/taub) | False (Dea/hörend) | None (not stated) — phenotype FLAG, not a locus + - tags : provenance/breeding markers (WFNZ/RV/GV/DP/extern …) — never genotype + +GEN-3b normalizations (wife + research confirmed): + - Uw/uw == G/g (international vs German notation for the SAME locus) -> aliased to G/g. + - Sls/WP is a SECOND spotting locus (S(l)s(l) = WP/Minimalschecke het). WP -> Sls het. + - Dea/dea/taub -> hearing/deaf flag (written after spsp), NOT a Punnett locus. + - WFNZ/RV/GV -> provenance/breeding tags, NOT genotype, NOT conflict-bearing. Conventions in the source data: - allele superscripts are bracketed: c[chm] -> c^chm, c[h] -> c^h, e[f] -> e^f - a single '-' for the second allele means "unknown" -> mapped to '?' - (frozen-contract wildcard; assumption pending the wife's confirmation) """ import re LOCI = ["A", "C", "D", "E", "G", "P", "Sp", "Re"] -# locus -> regex that matches that locus's token (longest alternatives first) _LOCUS_TOKEN = { "Sp": re.compile(r"^(Sp|sp)(Sp|sp|-)?$"), "Re": re.compile(r"^(Re|re)(Re|re|-)?$"), @@ -25,12 +31,7 @@ _LOCUS_TOKEN = { "G": re.compile(r"^(G|g)(G|g|-)?$"), "P": re.compile(r"^(P|p)(P|p|-)?$"), } -# loci our model does NOT have but the data uses -_KNOWN_UNMAPPED = re.compile(r"^(Uw|uw)(\[d\])?(Uw|uw)?(\[d\])?$|^(Sls|sls|Dea|dea)$", re.I) -_MARKER = re.compile(r"^\[?(WFNZ|WP|DP|GV|RV)\]?$|^\((taub|hörend|hoerend|RV|GV|extern[^)]*)\)$", re.I) - -# one allele unit per locus (longest-match alternatives first); '-' = unknown _ALLELE_UNIT = { "Sp": re.compile(r"Sp|sp|-"), "Re": re.compile(r"Re|re|-"), @@ -42,10 +43,41 @@ _ALLELE_UNIT = { "P": re.compile(r"[Pp]|-"), } +# Provenance/breeding tags (never genotype): Wildfangnachzucht, Rückverpaarung, +# Geschwisterverpaarung, DarkPatch, external origin. +_TAG = re.compile(r"^\[?(WFNZ|RV|GV|DP)\]?$|^\((RV|GV|extern[^)]*)\)$", re.I) + + +def _rewrite_uw(token): + """Uw/uw notation -> G/g (same locus). 'Uwuw[d]' -> 'Gg', 'UwUw' -> 'GG', 'uw[d]uw[d]' -> 'gg'.""" + if "uw" not in token.lower(): + return token + return token.replace("uw[d]", "g").replace("Uw", "G").replace("uw", "g") + + +def _sls_alleles(token): + """Sls (second spotting locus) alleles, or None. WP == Sls het (Minimalschecke); + S(l)S(l) homozygous = lethal. Allele symbols: 'Sl' / 'sl'.""" + n = token.strip("[]").replace("(l)", "l").replace("(L)", "l") + if n in ("WP", "Sls"): + return ["Sl", "sl"] # heterozygous (WP phenotype) + if n.lower() == "sls": + return ["sl", "sl"] # wild-type (no extra spotting) + units = re.findall(r"Sl|sl", n) + return units if len(units) == 2 else None + + +def _deaf_value(token): + """dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None. Case-sensitive for Dea/dea.""" + t = token.strip("()[]") + if t == "dea" or t.lower() == "taub": + return True + if t == "Dea" or t.lower() in ("hörend", "hoerend"): + return False + return None + def _alleles_for(locus, token): - """Extract the (allele1, allele2) pair from a single locus token, handling - two-letter alleles (Sp/Re) and bracketed superscripts (c[chm] -> c^chm).""" pat = _ALLELE_UNIT.get(locus) units = pat.findall(token) if pat else re.findall(r"[A-Za-z](?:\[[a-z]+\])?|-", token) alleles = [] @@ -55,7 +87,6 @@ def _alleles_for(locus, token): else: m = re.match(r"([A-Za-z]+)\[([a-z\-]+)\]", u) if m: - # [-] = sub-allele unknown -> keep the base letter only alleles.append(m.group(1) if m.group(2) == "-" else f"{m.group(1)}^{m.group(2)}") else: alleles.append(u) @@ -65,37 +96,60 @@ def _alleles_for(locus, token): def parse(raw): - """raw: a genotype string (may include trailing free text/markers). + """raw: a genotype string (may include trailing markers/flags). - Returns dict {mapped8locus, rawGenotype, unmappedTokens}. + Returns {mapped8locus, rawGenotype, unmappedTokens, deaf, tags}. """ raw = (raw or "").strip() mapped = {} unmapped = [] - # tokenise on whitespace; keep order + tags = [] + deaf = None + for tok in raw.split(): t = tok.strip().rstrip(",") if not t: continue + + # GEN-3b: Uw/uw is an alias of the G locus — rewrite before matching. + t = _rewrite_uw(t) + + # 8 standard loci matched = False for locus in LOCI: pat = _LOCUS_TOKEN.get(locus) if pat and pat.match(t): - if locus not in mapped: # first occurrence wins - mapped[locus] = _alleles_for(locus, t) + mapped.setdefault(locus, _alleles_for(locus, t)) # first occurrence wins matched = True break if matched: continue - if _KNOWN_UNMAPPED.match(t) or _MARKER.match(t): - unmapped.append(t) - else: - # anything else (stray notes, malformed tokens) -> unmapped, nothing lost - unmapped.append(t) + + # Sls (second spotting locus); WP is its heterozygous phenotype + sls = _sls_alleles(t) + if sls is not None: + mapped.setdefault("Sls", sls) + continue + + # deafness flag (after spsp): dea/taub vs Dea/hörend + d = _deaf_value(t) + if d is not None: + deaf = d + continue + + # provenance/breeding tags + if _TAG.match(t): + tags.append(re.sub(r"[()\[\]]", "", t).upper()) + continue + + unmapped.append(t) + return { "mapped8locus": mapped, "rawGenotype": raw, "unmappedTokens": unmapped, + "deaf": deaf, + "tags": tags, } @@ -103,7 +157,7 @@ def looks_like_genotype(text): """Heuristic: does this cell text contain >=3 recognisable locus tokens?""" n = 0 for tok in text.split(): - t = tok.rstrip(",") + t = _rewrite_uw(tok.rstrip(",")) if any(p.match(t) for p in _LOCUS_TOKEN.values()): n += 1 return n >= 3 diff --git a/tools/import/output/review-report.md b/tools/import/output/review-report.md index 89f06d6..18ab88f 100644 --- a/tools/import/output/review-report.md +++ b/tools/import/output/review-report.md @@ -4,15 +4,15 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date ## Überblick -- Rohe Tier-Einträge aus den Stammbäumen: **889** -- Nach Zusammenführung (eindeutige Tiere): **574** - - davon mit Geburtsdatum: 279 - - in mehreren Dateien gefunden (Dubletten zusammengeführt): 146 -- Konflikte zur Klärung: **32** +- Rohe Tier-Einträge aus den Stammbäumen: **950** +- Nach Zusammenführung (eindeutige Tiere): **622** + - davon mit Geburtsdatum: 327 + - in mehreren Dateien gefunden (Dubletten zusammengeführt): 158 +- Konflikte zur Klärung: **27** - Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310** -- Fotos zugeordnet: **123** +- Fotos zugeordnet: **137** - Würfe aus der Wurfchronik: **752** - - Tiere mit Wurf verknüpft: **135** (davon über Geburtsdatum **und** Eltern: 95, nur über Geburtsdatum: 40; mehrdeutig: 9) + - Tiere mit Wurf verknüpft: **159** (davon über Geburtsdatum **und** Eltern: 110, nur über Geburtsdatum: 49; mehrdeutig: 10) - Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema) ## Zusammenführungs-Schlüssel @@ -28,19 +28,15 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date | Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids | | ZoneFire | 07.12.2020 | Aa c[chm]c[chm] D- Ee Gg P- Spsp | CP-Agouti Kragenschecke // Kalea von den Kleinen Chaoten | — | Stammbaum von Akio Kids | | Louis von den Kleinen Chaoten | 15.07.2017 | Aa Cc[] D- Ee Gg P- spsp // Aa Cc[chm] D- Ee Uwuw[d] P- spsp | Roswitha von den Kleinen Chaoten | 01.07.2020 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity | -| Roswitha von den Kleinen Chaoten | 10.09.2018 | aa CC D- ee[f] Gg P- spsp // aa CC D- ee[f] Uwuw[d] P- spsp | — | 05.08.2021 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity | | Firefly von den Kleinen Chaoten | 18.12.2019 | /+, Aa c[chm]c[chm] D- Ee Gg PP Spsp // Aa c[chm]c[chm] DD Ee Gg PP Spsp | — | 2024 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids | | Zuleika von den Kleinen Chaoten | 24.10.2015 | aa c[chm]c[h] D- E G P- spsp // aa c[chm]c[h] D- Ee Gg P- spsp // aa c[chm]c[h] DD Ee Gg P- spsp | — | 24.02.2019 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids | | WildFire von den Kleinen Chaoten | 05.10.2017 | aa c[chm]c[chm] D- Ee gg P- spsp // aa c[chm]c[chm] D- Ee gg PP spsp | — | — | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids | | Vestra von den Schlossmäusen | 08.02.2019 | Aa Cc[chm] D- EE GG PP Spsp [WP] // Aa Cc[chm] DD EE GG PP Spsp [WP] | — | 26.05.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Valentino Firehearts Kids | | Flint von den Kleinen Chaoten | 23.12.2017 | aa Cc[chm] D- ee Gg P- spsp | — | 10.05.2021 // 10.05.2022 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity | -| Silenos gen. Adonis v.d. Kleinen Chaoten | 11.10.2015 | aa Cc[chm] D- Ee Gg PP spsp // aa Cc[chm] D- Ee Uwuw[d] PP spsp | — | 18.07.2019 | Stammbaum von Akio Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Watarus Kids | +| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance | | Bruno of Black Forest | 01.06.2022 | aa C- dd Ee Gg P- spsp | Blau // Mystique of Black Forest | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids | | Milka of LennyLengo | 09.12.2018 | aa C- dd E- Gg P- Spsp // aa Cc[h] dd EE Gg P- Spsp | — | 22.12.2021 | Stammbaum von Alberto Kids, Stammbaum von Stella Kids | -| Hedwig of BGB | 30.10.2019 | aa CC DD E- G- P- Spsp WP // aa CC DD E- G- P- Spsp WP DP (hörend) | — | 30.08.2023 | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids | | Silvain von den Kleinen Chaoten | 27.03.2022 | aa c[chm]c[chm] Dd Ee[-] Gg P- Spsp // aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp | — | 31.12.2024 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity | -| Pitari gen. Piti von den Kleinen Chaoten | 16.05.2021 | Aa CC dd ee Gg P- Spsp DP // Aa CC dd ee Gg P- Spsp [DP] | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Stella Kids | -| Brandon Stark von den Kleinen Chaoten | 13.12.2017 | aa Cc[chm] D- Ee Gg P- spsp // aa Cc[chm] D- Ee Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids | | Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids | | Little Hero of Black Forest | 22.02.2018 | AA CC DD EE GG PP [WFNZ] // AA CC DD EE GG PP spsp [WFNZ] | — | 18.06.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids, Stammbaum von Valentino Firehearts Kids | | Molly of Black Forest | 13.09.2021 | /+, Aa Cc[chm] D- Ee gg P- spsp // Aa Cc[chm] Dd Ee gg Pp spsp | — | 03.05.2021 | Stammbaum von Alberto Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity | @@ -52,11 +48,10 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date | Chayton v.d. Kleinen Chaoten (extern SC) | 04.02.2022 | aa Cc[-] D- e[f]e[f] Gg Pp spsp | Orangeschimmel, hell // Victoria Welby gen. Welby v.d. Kleinen Chaoten | 30.04.2024 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids | | Victoria Welby gen. Welby v.d. Kleinen Chaoten | 16.01.2023 | Aa CC D- Ee[f] Gg pp Spsp [DP] // Aa CC D- ee[f] Gg pp Spsp [DP] | Goldfuchsschimmel Punktschecke DP | 17.02.2026 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids | | Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | Belica gen. Emi von den Kleinen Chaoten | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids | -| Chelsea von den Kleinen Chaoten | 02.04.2021 | /+, Aa CC Dd ee gg Pp spsp // Aa CC Dd ee gg Pp spsp | — | — | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Valentino Firehearts Kids | | Chesnut | 13.11.2019 | aa C- D- ee[f] GG PP spsp | Kohlfuchsschimmel // Tennessee von den Kleinen Chaoten | 22.11.2023 | Stammbaum von Kentucky | | Ethan von den Kleinen Chaoten | 09.07.2020 | Aa Cc[chm] D- ee[f] Gg Pp Spsp | Ichika von den Kleinen Chaoten // Orangeschimmel, hell Kragenschecke | 30.07.2024 | Stammbaum von Kentucky, Stammbaum von Watarus Kids | -| Quied Soldier of Black Forest | 07.06.2018 | /+, Aa C- D- ee[f] GG Pp Spsp [DP] // Aa C- D- ee[f] GG Pp Spsp DP | Hoshi von den Kleinen Chaoten | — | Stammbaum von Kentucky | | Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids | +| Skarlett v.d. Kleinen Chaoten | 14.07.2013 | / +2018, Aa Cc[chm] DD ee uw[d]uw[d] PP spsp // Aa Cc[chm] DD ee uw[d]uw[d] PP spsp | — | 17.04.2016 // 2018 | Stammbaum von Vance | ## Mehrdeutige / unvollständige Einträge @@ -105,7 +100,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date ## Wahrscheinliche Zuordnungen unvollständiger Einträge -38 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung): +39 namenlose/datenlose Einträge tragen denselben Namen wie ein vollständiges Tier — vermutlich dasselbe Tier (zur Bestätigung): - „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019) - „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019) @@ -137,6 +132,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date - „Zadar from Zeko i ptica, Croatia“ → Zadar from Zeko i ptica, Croatia (*12.04.2019) - „Living Force's Vally“ → Living Force's Vally (*01.11.2014) - „Pinto of Fiomi“ → Pinto of Fiomi (*28.08.2016) +- „Hanse Renner's Poseidon“ → Hanse Renner's Poseidon (*14.08.2014) - „Oscar of Black Forest“ → Oscar of Black Forest (*12.06.2019) - „Hagrid Rubeus of Black Forest“ → Hagrid Rubeus of Black Forest (*18.07.2019) - „Lilo of LennyLengo“ → Lilo of LennyLengo (*04.11.2018) @@ -152,29 +148,21 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M | Token | Vorkommen | Bedeutung (Vermutung) | |---|---|---| -| `[DP]` | 15 | Marker (Dunkelpigment?) | -| `[WFNZ]` | 13 | Marker | -| `DP` | 9 | Marker | | `/+` | 8 | ? | -| `WP` | 7 | Marker | -| `Uwuw[d]` | 4 | 9. Locus Uw (nicht im Modell) | | `-g` | 2 | ? | | `C(C)` | 2 | Schreibweise (C trägt c) | -| `[WP]` | 2 | Marker | | `chmchm` | 2 | Schreibweise (c[chm]c[chm]) | | `Cc[]` | 1 | ? | | `-psp` | 1 | ? | | `G(G)` | 1 | ? | -| `UwUw` | 1 | 9. Locus Uw | -| `uw[d]uw[d]` | 1 | ? | -| `[DP` | 1 | ? | +| `/` | 1 | ? | +| `+2018` | 1 | ? | +| `c[chm]chm]` | 1 | ? | | `Dea/dea]` | 1 | ? | | `DD-Tumor` | 1 | ? | | `bei` | 1 | ? | | `Geschwistern` | 1 | ? | | `C-D-` | 1 | ? | -| `Sls` | 1 | ? | -| `(hörend)` | 1 | ? | | `-DD` | 1 | ? | ## Wurfchronik — Datenqualitäts-Hinweise diff --git a/tools/import/test_genotype.py b/tools/import/test_genotype.py new file mode 100644 index 0000000..7d9ba44 --- /dev/null +++ b/tools/import/test_genotype.py @@ -0,0 +1,71 @@ +"""Zero-dep tests for genotype.py GEN-3b normalization. + +Run: python test_genotype.py (exit 0 = all pass) +Covers: Uw/uw -> G/g alias, Sls/WP second spotting locus, dea/Dea/taub +hearing-deaf flag, WFNZ/RV/GV/DP provenance tags. Per hive/agents/god/GENETIK-notation.md. +""" +import sys +import genotype as g + + +def check(name, cond): + if not cond: + print(f"FAIL: {name}") + check.failed += 1 + else: + print(f"ok: {name}") +check.failed = 0 + + +# --- Uw/uw == G/g (same locus) --- +r = g.parse("aa Cc Dd Ee Uwuw Pp spsp rere") +check("Uw->G: G locus mapped", r["mapped8locus"].get("G") == ["G", "g"]) +check("Uw->G: nothing left in unmapped", r["unmappedTokens"] == []) + +r = g.parse("UwUw") +check("UwUw -> GG", r["mapped8locus"].get("G") == ["G", "G"]) +r = g.parse("uwuw") +check("uwuw -> gg", r["mapped8locus"].get("G") == ["g", "g"]) +r = g.parse("uw[d]uw[d]") +check("uw[d]uw[d] -> gg (dense underwhite)", r["mapped8locus"].get("G") == ["g", "g"]) + +# Gg and Uwuw must produce the SAME mapped locus (so they stop being a conflict) +check("Gg identical to Uwuw at G locus", + g.parse("Gg")["mapped8locus"]["G"] == g.parse("Uwuw")["mapped8locus"]["G"]) + +# --- Sls / WP second spotting locus --- +check("WP -> Sls het", g.parse("WP")["mapped8locus"].get("Sls") == ["Sl", "sl"]) +check("[WP] (bracketed) -> Sls het", g.parse("[WP]")["mapped8locus"].get("Sls") == ["Sl", "sl"]) +check("Sls token -> Sls het", g.parse("Sls")["mapped8locus"].get("Sls") == ["Sl", "sl"]) +check("sls -> Sls wild", g.parse("sls")["mapped8locus"].get("Sls") == ["sl", "sl"]) +check("S(l)s(l) -> Sl,sl", g.parse("S(l)s(l)")["mapped8locus"].get("Sls") == ["Sl", "sl"]) +# Sp and Sls are TWO distinct loci on the same animal (Superschecke) +r = g.parse("spsp WP") +check("Sp + Sls coexist (two spotting loci)", + r["mapped8locus"].get("Sp") == ["sp", "sp"] and r["mapped8locus"].get("Sls") == ["Sl", "sl"]) + +# --- deafness flag (after spsp), case-sensitive --- +check("dea (lower) -> deaf True", g.parse("spsp dea")["deaf"] is True) +check("taub -> deaf True", g.parse("taub")["deaf"] is True) +check("Dea (upper) -> hearing False", g.parse("spsp Dea")["deaf"] is False) +check("(hörend) -> hearing False", g.parse("(hörend)")["deaf"] is False) +check("no deaf token -> None", g.parse("aa Cc")["deaf"] is None) +# deafness is NOT a genotype locus and must not pollute mapped/unmapped silently +check("deaf flag not in unmapped", "dea" not in g.parse("spsp dea")["unmappedTokens"]) + +# --- provenance / breeding tags (never genotype, never conflict) --- +check("WFNZ -> tag", g.parse("aa WFNZ")["tags"] == ["WFNZ"]) +check("RV -> tag", g.parse("(RV)")["tags"] == ["RV"]) +check("GV -> tag", g.parse("(GV)")["tags"] == ["GV"]) +check("DP -> tag", g.parse("[DP]")["tags"] == ["DP"]) +check("tag not in genotype loci", g.parse("WFNZ")["mapped8locus"] == {}) +check("tag not in unmapped", g.parse("aa WFNZ")["unmappedTokens"] == []) + +# --- looks_like_genotype recognizes Uw-bearing cells --- +check("looks_like_genotype sees Uw as G", + g.looks_like_genotype("aa Cc Uwuw") is True) + +if check.failed: + print(f"\n{check.failed} test(s) FAILED") + sys.exit(1) +print("\nALL PASS")