FEAT: Implement deceased/givenaway enclosure visibility rules, preserve external clan name, and hide receiver fields for deceased gerbils

This commit is contained in:
2026-06-13 01:40:19 +02:00
parent 5732398e60
commit 396d8f05d8
53 changed files with 7431 additions and 274 deletions

View File

@@ -213,6 +213,35 @@
"genotype": "AA CC DD ee Gg pp Spsp",
"farbschlag": "Goldfuchs Starkschecke",
"source": "Julian 2026-06-07 — HUMANQUESTION D7 (Sohn-Korrektur, nicht ursprüngliche D7-Liste)"
},
{
"name": "Joghurt von Privat",
"dob": "06.09.2013",
"decision": "Genotype aa Cc[-] D- ee UwUw PP spsp as confirmed by Julian.",
"genotype": "aa Cc[-] D- ee UwUw PP spsp",
"source": "Julian 2026-06-12"
},
{
"name": "Ken'ichi",
"dob": "01.03.2015",
"decision": "Genotype AA CC DD Ee GG PP spsp is correct, farbschlag is Agouti.",
"genotype": "AA CC DD Ee GG PP spsp",
"farbschlag": "Agouti",
"source": "Julian 2026-06-12"
},
{
"name": "Harumi",
"dob": "21.02.2015",
"decision": "Death date is 11.01.2018 (died in 2018).",
"dateOfDeath": "11.01.2018",
"source": "Julian 2026-06-12"
},
{
"name": "Kleiner Warnowrenner Elieus gen. Eragon",
"dob": "18.05.2016",
"decision": "Genotype is aa CC D- Ee Gg pp Spsp [DP] (C locus is CC, full color, duplicate colourpoint record resolved).",
"genotype": "aa CC D- Ee Gg pp Spsp [DP]",
"source": "Julian 2026-06-12"
}
]
}

View File

@@ -35,19 +35,20 @@ DEATH = re.compile(r"\+\s?(\d{1,2}\.\d{1,2}\.(?:\d{4}|\d{2})|\d{4})")
# ---------------------------------------------------------------- helpers ----
def gen_of(colnum):
def gen_of(colnum, offset=0):
"""Map a column number to a generation band (0=proband ... 5=deepest)."""
if colnum <= 6:
return 0 # E band (proband / "Kids")
if colnum <= 9:
return 1 # H band (parents)
if colnum <= 12:
return 2 # K band (grandparents)
if colnum <= 15:
return 3 # N band (great-grandparents)
if colnum <= 17:
return 4 # Q band (gg-grandparents)
return 5 # R/S band (name-pairs)
effective_col = colnum - offset
if effective_col <= 3:
return 0 # Column B (2) -> proband
if effective_col <= 6:
return 1 # Column E (5) -> parents
if effective_col <= 9:
return 2 # Column H (8) -> grandparents
if effective_col <= 12:
return 3 # Column K (11) -> great-grandparents
if effective_col <= 15:
return 4 # Column N (14) -> gg-grandparents
return 5 # Column Q (17) or deeper -> ggg-grandparents
def norm_name(name):
@@ -189,6 +190,9 @@ def extract_stammbaum(path):
cells = xu.read_cells(z, sheets[0], ss)
fillsex = xu.cell_fill_sex(z, sheets[0]) # box colour -> sex (blue=male, white=female)
has_col2 = any(c == 2 for (c, r) in cells)
col_offset = 0 if has_col2 else 3
# group cells by column for block reconstruction
by_col = {}
for (c, r), t in cells.items():
@@ -230,7 +234,7 @@ def extract_stammbaum(path):
# WITH a Farbschlag cell; deep bands (gen >= 2, cols K/N/Q...) are 3-cell blocks
# (Name/DOB/Genotype) with NO Farbschlag — colour is derived from the genotype. So in
# deep bands we must NOT grab the next block's name or a stray health note as Farbschlag.
deep_band = gen_of(c) >= 2
deep_band = gen_of(c, col_offset) >= 2
for rr in range(r + 1, r + 4):
cell = cells.get((c, rr))
if not cell:
@@ -269,7 +273,7 @@ def extract_stammbaum(path):
"parentRefs": [],
"photos": [],
"sourceFiles": [fname],
"_gen": gen_of(c),
"_gen": gen_of(c, col_offset),
"_col": c,
"_row": r,
"_file": fname,
@@ -280,7 +284,7 @@ def extract_stammbaum(path):
for (c, r), t in cells.items():
if (c, r) in used:
continue
if " & " in t and not DOB.search(t) and len(t) < 90 and gen_of(c) >= 4:
if " & " in t and not DOB.search(t) and len(t) < 90 and gen_of(c, col_offset) >= 4:
for part in t.split(" & "):
part = clean_name(part)
if part:
@@ -291,7 +295,7 @@ def extract_stammbaum(path):
"genotype": gt.parse(""), "deaf": None, "tags": [],
"breeder": "", "zucht": zraw,
"parentRefs": [], "photos": [], "sourceFiles": [fname],
"_gen": gen_of(c), "_col": c, "_row": r, "_file": fname,
"_gen": gen_of(c, col_offset), "_col": c, "_row": r, "_file": fname,
"_zucht": norm_zucht(zraw),
})
@@ -337,8 +341,9 @@ def _attach_photos(z, sheets, animals, fname):
for a in animals:
by_gen.setdefault(a["_gen"], []).append(a)
media_dir = os.path.join(OUT, "photos")
col_offset = 0 if any(a["_col"] == 2 for a in animals) else 3
for i, (sp, col, row, media) in enumerate(anchors):
g = gen_of(col)
g = gen_of(col, col_offset)
cands = by_gen.get(g, [])
if not cands:
# fall back to nearest animal by row across all gens
@@ -1026,6 +1031,9 @@ def main():
print(f" {len(got):4d} {os.path.basename(path)}")
raw_animals.extend(got)
# Skip pseudo-animal records (like "DD-Tumor bei Geschwister") that are actually notes
raw_animals = [a for a in raw_animals if "DD-Tumor" not in a["name"]]
litters = []
if os.path.isfile(args.wurfchronik):
litters = extract_wurfchronik(args.wurfchronik)

View File

@@ -68,11 +68,18 @@ def _sls_alleles(token):
def _deaf_value(token):
"""dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None. Case-sensitive for Dea/dea."""
"""dea/taub -> True (deaf); Dea/hörend -> False (hearing); else None."""
t = token.strip("()[]")
if t == "dea" or t.lower() == "taub":
t_lower = t.lower()
if "taub" in t_lower:
return True
if t == "Dea" or t.lower() in ("hörend", "hoerend"):
if "hörend" in t_lower or "hoerend" in t_lower:
return False
if "dea/dea" in t_lower or "dea/*" in t_lower:
return True
if t == "dea":
return True
if t == "Dea":
return False
return None

File diff suppressed because it is too large Load Diff

View File

@@ -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: **2451**
- Nach Zusammenführung (eindeutige Tiere): **1007**
- davon mit Geburtsdatum: 682
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 461
- Konflikte zur Klärung: **7**
- Rohe Tier-Einträge aus den Stammbäumen: **2449**
- Nach Zusammenführung (eindeutige Tiere): **1006**
- davon mit Geburtsdatum: 681
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 460
- Konflikte zur Klärung: **2**
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **342**
- Fotos zugeordnet: **417**
- Würfe aus der Wurfchronik: **752**
- Tiere mit Wurf verknüpft: **274** (davon über Geburtsdatum **und** Eltern: 175, nur über Geburtsdatum: 99; mehrdeutig: 14)
- Tiere mit Wurf verknüpft: **270** (davon über Geburtsdatum **und** Eltern: 167, nur über Geburtsdatum: 103; mehrdeutig: 17)
- Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema)
## Zusammenführungs-Schlüssel
@@ -34,12 +34,7 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
| Tier | Geburtsdatum | abweichende Genotypen | abweichende Farbschläge | Sterbedaten | Dateien |
|---|---|---|---|---|---|
| Joghurt von Privat | 06.09.2013 | aa CC D- ee GG PP spsp // aa Cc[-] D- ee UwUw PP spsp | Privat | 06.03.2017 | Stammbaum von Akio Kids, Stammbaum von Jin, Stammbaum von Kentucky |
| Ken'ichi | 01.03.2015 | AA CC DD Ee GG PP spsp | Agouti // DD-Tumor | 02.11.2018 | Stammbaum von Chesnut, Stammbaum von Emi, Stammbaum von Hana, Stammbaum von Kentucky, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi |
| DD-Tumor bei Geschwister | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp // aa Cc[chm] D- Ee uw[d]uw[d] P- spsp | — | 02.01.2020 // 12.12.2019 | Stammbaum von Chesnut, Stammbaum von Emi |
| Kleiner Warnowrenner Elieus gen. Eragon | 18.05.2016 | aa CC D- Ee Gg pp Spsp [DP] // aa c[chm]c[chm] D- Ee Gg pp Spsp | — | 09.10.2019 | Stammbaum von Chesnut, Stammbaum von Jeremy, Stammbaum von Kentucky, Stammbaum von Martin, Stammbaum von Quied Soldier of Black Forest |
| Osamu | 10.12.2015 | AA CC DD ee gg P- spsp // AA CC DD ee gg PP spsp // AA CC DD ee uw[d]uw[d] PP spsp | Polarfuchs | 01.10.2020 // 18.12.2020 | Stammbaum von Danako, Stammbaum von Ella, Stammbaum von Jin, Stammbaum von Kazuya, Stammbaum von Kentucky, Stammbaum von Martin, Stammbaum von Rainny, Stammbaum von Ren, Stammbaum von South Dakota, Stammbaum von Stella Kids, Stammbaum von Tennessee, Stammbaum von Zenon von Elea |
| Harumi | 21.02.2015 | aa Cc[chm] DD EE GG PP Spsp | — | 03.06.2017 // 11.01.2018 | Stammbaum von Danako, Stammbaum von Kalea, Stammbaum von Yurikas und Pintos Sohn |
| Osamu | 10.12.2015 | AA CC DD ee gg P- spsp // AA CC DD ee gg PP spsp // AA CC DD ee uw[d]uw[d] PP spsp | — | 01.10.2020 // 18.12.2020 | Stammbaum von Danako, Stammbaum von Ella, Stammbaum von Jin, Stammbaum von Kazuya, Stammbaum von Kentucky, Stammbaum von Martin, Stammbaum von Rainny, Stammbaum von Ren, Stammbaum von South Dakota, Stammbaum von Stella Kids, Stammbaum von Tennessee, Stammbaum von Zenon von Elea |
| Hanami | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp // aa Cc[chm] D- Ee uw[d]uw[d] P- spsp | — | 02.01.2020 // 12.12.2019 // 14.01.2020 | Stammbaum von Hana, Stammbaum von Kentucky, Stammbaum von Rainny, Stammbaum von Ren, Stammbaum von Stella Kids, Stammbaum von Vance, Stammbaum von Zac (Vance.Dorie) |
## Mehrdeutige / unvollständige Einträge
@@ -160,7 +155,6 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
|---|---|---|
| `/+` | 7 | ? |
| `[meliert]` | 4 | ? |
| `[Dea/dea]` | 4 | ? |
| `(Kragen)` | 3 | ? |
| `/` | 3 | ? |
| `-g` | 2 | ? |
@@ -168,7 +162,6 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
| `Tumor` | 2 | ? |
| `!` | 2 | ? |
| `C(C)` | 2 | Schreibweise (C trägt c) |
| `[dea/*]` | 2 | ? |
| `(KW` | 2 | ? |
| `Cc[]` | 1 | ? |
| `[WFNZ-Maroon]` | 1 | ? |
@@ -176,13 +169,15 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
| `[meliert]-[DP]` | 1 | ? |
| `!Knickschwanz-Gen!` | 1 | ? |
| `-09.09.2018` | 1 | ? |
| `[dea/dea]` | 1 | ? |
| `[DP?]` | 1 | ? |
| `/+Dezember'2014` | 1 | ? |
| `(Ansatz)` | 1 | ? |
| `[WFNZ][dea/*]` | 1 | ? |
| `-psp` | 1 | ? |
| `G(G)` | 1 | ? |
| `!Niereninsuffizienz!` | 1 | ? |
| `+09.07.2017` | 1 | ? |
| `+2018` | 1 | ? |
| `AAA` | 1 | ? |
## Wurfchronik — Datenqualitäts-Hinweise

View File

@@ -344,13 +344,28 @@ def main():
# Attach Photos
for idx, photo_rel in enumerate(a.get("photos", [])):
photo_guid = generate_guid(f"photo-{photo_rel}")
ext = os.path.splitext(photo_rel)[1] or ".jpeg"
fn_guid = photo_guid.replace("-", "")
resolved_photos.append({
"Id": photo_guid,
"GerbilId": a_guid,
"FileName": os.path.basename(photo_rel),
"SortOrder": idx
"FileName": f"{fn_guid}{ext}",
"SortOrder": idx,
"_source_path": photo_rel
})
# Set IsBreeder and IsReceiver flags on contacts
breeder_ids = {g["OriginContactId"] for g in resolved_gerbils if g.get("OriginContactId")}
receiver_ids = {g["ReceiverContactId"] for g in resolved_gerbils if g.get("ReceiverContactId")}
for c in resolved_contacts:
c_id = c["Id"]
is_breeder = c_id in breeder_ids
is_receiver = c_id in receiver_ids
if not is_breeder and not is_receiver:
is_receiver = True
c["IsBreeder"] = is_breeder
c["IsReceiver"] = is_receiver
# Save resolved data
resolved_data = {
"Contacts": resolved_contacts,

View File

@@ -0,0 +1,65 @@
# run_app_and_import.ps1
# This script starts the .NET Aspire AppHost and automatically runs the database import once the WebAPI is ready.
Write-Host "Starting GerbilManager AppHost (this will boot PostgreSQL and the WebAPI)..." -ForegroundColor Cyan
# Start AppHost in the background
$appProcess = Start-Process dotnet -ArgumentList "run --project GerbilManager.AppHost" -PassThru -NoNewWindow
# Function to cleanup process on exit
function Cleanup {
Write-Host "`nStopping AppHost..." -ForegroundColor Yellow
if ($appProcess -and -not $appProcess.HasExited) {
Stop-Process -Id $appProcess.Id -Force
}
}
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup }
try {
# Poll the import endpoint until it becomes responsive (max 60 seconds)
$url = "http://localhost:5179/import/ingest-resolved"
$success = $false
Write-Host "Waiting for WebAPI to become responsive at $url..." -ForegroundColor Cyan
for ($i = 0; $i -lt 30; $i++) {
try {
# Send a test request or check port
$response = Invoke-WebRequest -Uri "http://localhost:5179/openapi/v1.json" -Method Get -TimeoutSec 2 -ErrorAction Stop
if ($response.StatusCode -eq 200) {
$success = $true
break
}
}
catch {
# WebAPI not ready yet
Start-Sleep -Seconds 2
}
}
if (-not $success) {
Write-Host "Error: WebAPI did not become ready within 60 seconds." -ForegroundColor Red
Cleanup
exit 1
}
Write-Host "WebAPI is up! Triggering the database import..." -ForegroundColor Green
# Run the import POST request
try {
$importResponse = Invoke-RestMethod -Uri $url -Method Post -ContentType "application/json"
Write-Host "Import Result: $importResponse" -ForegroundColor Green
Write-Host "`nThe application is now fully running and imported!" -ForegroundColor Cyan
Write-Host "Press Ctrl+C in this terminal to stop the application." -ForegroundColor Yellow
# Keep script running to keep the app process alive
while ($true) {
Start-Sleep -Seconds 1
}
}
catch {
Write-Host "Error during database import: $_" -ForegroundColor Red
}
}
finally {
Cleanup
}

View File

@@ -68,8 +68,8 @@ finally:
except OSError: pass
gen = e.gen_of
check("gen_of: early bands < 2 (E,H)", gen(5) < 2 and gen(8) < 2)
check("gen_of: deep bands >= 2 (K,N,Q)", gen(11) >= 2 and gen(14) >= 2)
check("gen_of: early bands B, E, H", gen(2) == 0 and gen(5) == 1 and gen(8) == 2)
check("gen_of: deep bands K, N, Q", gen(11) == 3 and gen(14) == 4 and gen(17) == 5)
# --- conflict-decisions consumption (HUMANQUESTION D / C6) ---
dec_path = os.path.join(tempfile.gettempdir(), "conflict-decisions-test.json")