people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-28 08:40:25 +03:30
parent 14461c362f
commit da4a0f7c14
7 changed files with 269 additions and 1 deletions

View File

@ -123,6 +123,37 @@ namespace RMuseum.DbContext
.HasIndex(m => m.Name) .HasIndex(m => m.Name)
.IsUnique(); .IsUnique();
// GanjoorPersonRelation has two required FKs to the same table (GanjoorRelatedPerson) -
// left at their EF Core default (Cascade, since both are required/non-nullable), SQL
// Server refuses to create the second FK with "may cause cycles or multiple cascade
// paths". Restricting one side (Person2) is enough to break the ambiguity; deleting a
// person that's still referenced by a relation should be prevented at the application
// level anyway (via a "still has family tree entries" check), not silently cascaded.
builder.Entity<GanjoorPersonRelation>()
.HasOne(r => r.Person1)
.WithMany()
.HasForeignKey(r => r.Person1Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorPersonRelation>()
.HasOne(r => r.Person2)
.WithMany()
.HasForeignKey(r => r.Person2Id)
.OnDelete(DeleteBehavior.Restrict);
// same two-required-FKs-to-the-same-table situation as GanjoorPersonRelation above
builder.Entity<GanjoorPersonAffiliation>()
.HasOne(a => a.Person1)
.WithMany()
.HasForeignKey(a => a.Person1Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorPersonAffiliation>()
.HasOne(a => a.Person2)
.WithMany()
.HasForeignKey(a => a.Person2Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorUserBookmark>() builder.Entity<GanjoorUserBookmark>()
.HasIndex(b => new { b.UserId, b.PoemId, b.CoupletIndex }); .HasIndex(b => new { b.UserId, b.PoemId, b.CoupletIndex });
@ -578,6 +609,16 @@ namespace RMuseum.DbContext
/// </summary> /// </summary>
public DbSet<GanjoorRelatedPerson> GanjoorRelatedPersons { get; set; } public DbSet<GanjoorRelatedPerson> GanjoorRelatedPersons { get; set; }
/// <summary>
/// approved kinship edges between people (family tree) - see GanjoorPersonRelation
/// </summary>
public DbSet<GanjoorPersonRelation> GanjoorPersonRelations { get; set; }
/// <summary>
/// approved non-family ties between people (e.g. minister-to-king) - see GanjoorPersonAffiliation
/// </summary>
public DbSet<GanjoorPersonAffiliation> GanjoorPersonAffiliations { get; set; }
/// <summary> /// <summary>
/// Books (PDF Library) /// Books (PDF Library)
/// </summary> /// </summary>

View File

@ -0,0 +1,50 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// an approved non-family tie between two people (GanjoorRelatedPerson rows) - e.g. a minister
/// serving a king. Kept separate from GanjoorPersonRelation (kinship) rather than folded into
/// it: this is what lets two unrelated family trees show up as adjacent to each other (e.g.
/// browsing the Barmakid tree surfaces a link out to the Abbasid tree via a shared minister)
/// without treating "family tree" as its own entity to be linked - the tie is between the two
/// people, and the tree-to-tree adjacency is just what falls out of rendering it that way.
/// </summary>
public class GanjoorPersonAffiliation
{
/// <summary>
/// record id
/// </summary>
public int Id { get; set; }
/// <summary>
/// first person in the tie - for a directional type (Minister, Advisor, Courtier, Patron)
/// this is the one in the subordinate/serving role; for a symmetric one (Ally, Rival) order
/// doesn't matter
/// </summary>
public int Person1Id { get; set; }
/// <summary>
/// first person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person1 { get; set; }
/// <summary>
/// second person in the tie - for a directional type this is the one being served
/// </summary>
public int Person2Id { get; set; }
/// <summary>
/// second person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person2 { get; set; }
/// <summary>
/// the kind of tie between Person1 and Person2
/// </summary>
public PersonAffiliationType AffiliationType { get; set; }
/// <summary>
/// free-text note (e.g. sourcing/reasoning, or what the tie actually is when AffiliationType is Other)
/// </summary>
public string Note { get; set; }
}
}

View File

@ -0,0 +1,55 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// an approved kinship edge between two people (GanjoorRelatedPerson rows) - the live/materialized
/// counterpart of a pending suggestion, which travels as JSON on GanjoorPoemGeoDateTagCorrection's
/// SuggestedPersonGraphJson until it's approved and turned into rows here. The graph as a whole
/// (all rows in this table) is a general kinship graph, not a strict tree - see PersonRelationType.
/// </summary>
public class GanjoorPersonRelation
{
/// <summary>
/// record id
/// </summary>
public int Id { get; set; }
/// <summary>
/// first person in the relation - for a directional relation type (Parent, Ancestor) this is
/// the parent/ancestor side; for a symmetric one (Sibling, Spouse) order doesn't matter
/// </summary>
public int Person1Id { get; set; }
/// <summary>
/// first person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person1 { get; set; }
/// <summary>
/// second person in the relation - for a directional relation type (Parent, Ancestor) this is
/// the child/descendant side
/// </summary>
public int Person2Id { get; set; }
/// <summary>
/// second person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person2 { get; set; }
/// <summary>
/// the kind of relation between Person1 and Person2
/// </summary>
public PersonRelationType RelationType { get; set; }
/// <summary>
/// for RelationType == Ancestor, an optional known exact degree (e.g. 2 for "grandparent",
/// 3 for "great-grandparent") - left null when only the relative order is known, not the
/// exact number of generations in between. Not used for other relation types.
/// </summary>
public int? DegreeHint { get; set; }
/// <summary>
/// free-text note (e.g. sourcing/reasoning for this relation)
/// </summary>
public string Note { get; set; }
}
}

View File

@ -56,7 +56,8 @@
public int? LunarDay { get; set; } public int? LunarDay { get; set; }
/// <summary> /// <summary>
/// related person id (existing, approved GanjoorRelatedPerson only - no suggestion path for new people yet) /// related person id - an existing, already approved GanjoorRelatedPerson. Set this OR
/// SuggestedPersonGraphJson below, not both (same pattern as LocationId/Suggested* above).
/// </summary> /// </summary>
public int? PersonId { get; set; } public int? PersonId { get; set; }
@ -65,6 +66,35 @@
/// </summary> /// </summary>
public virtual GanjoorRelatedPerson Person { get; set; } public virtual GanjoorRelatedPerson Person { get; set; }
/// <summary>
/// a brand new, not yet approved person (and optionally that person's relatives/relations,
/// which may themselves be new people) - serialized JSON rather than its own set of
/// correction tables, because a single suggestion can introduce several interlinked new
/// people at once (e.g. "add this person, and their father, and the relation between them")
/// and a new person referencing another not-yet-existing new person has no real id to point
/// at until the whole graph is approved together. Expected shape (local keys are only used
/// to resolve relations within this same submission and never stored beyond approval time):
/// {
/// "person": { "localKey": "p1", "existingPersonId": null, "name": "...", "description": "...",
/// "wikiUrl": "...", "birthYearInLHijri": null, "deathYearInLHijri": null,
/// "validBirthDate": false, "validDeathDate": false,
/// "birthLocationId": null, "deathLocationId": null,
/// "familyTreeCaption": null },
/// "relatedPeople": [ { "localKey": "p2", "existingPersonId": 42, ... } ],
/// "relations": [ { "kind": "family", "person1": "p1", "person2": "p2", "relationType": "Parent",
/// "degreeHint": null, "note": "..." },
/// { "kind": "affiliation", "person1": "p2", "person2": "p3",
/// "affiliationType": "Minister", "note": "..." } ]
/// }
/// "person" is the node that ends up assigned to PersonId once approved. Only set when
/// PersonId above is null. Each entry in "relations" carries a "kind" discriminator so one
/// submission can suggest both kinship edges (materialized as GanjoorPersonRelation,
/// "relationType" against PersonRelationType) and non-family ties (materialized as
/// GanjoorPersonAffiliation, "affiliationType" against PersonAffiliationType) at once - e.g.
/// introducing a person along with both their father and the king they served.
/// </summary>
public string SuggestedPersonGraphJson { get; set; }
/// <summary> /// <summary>
/// if true, this tag is excluded from category/poet-level map aggregation (e.g. a place mentioned only /// if true, this tag is excluded from category/poet-level map aggregation (e.g. a place mentioned only
/// for comparison, not actually visited/relevant to the poet's own path) - default false, matching /// for comparison, not actually visited/relevant to the poet's own path) - default false, matching

View File

@ -69,5 +69,14 @@
/// AI generated /// AI generated
/// </summary> /// </summary>
public bool MachineGenerated { get; set; } public bool MachineGenerated { get; set; }
/// <summary>
/// optional caption for the family tree this person is treated as the root of (e.g.
/// "ساسانیان", "آل برمک") - purely a display label for whoever a tree is being browsed
/// from; nothing enforces that this person actually has no recorded ancestors themselves,
/// and most people will leave this null (only whichever person a tree is "named after"
/// needs one set).
/// </summary>
public string FamilyTreeCaption { get; set; }
} }
} }

View File

@ -0,0 +1,48 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// the kind of non-family tie a GanjoorPersonAffiliation represents between two people - e.g.
/// a minister serving a king, an advisor, a patron. Unlike PersonRelationType this has nothing
/// to do with kinship; it's what lets two otherwise unrelated family trees show up as adjacent
/// (e.g. "some Barmakids served the Abbasid court") without merging them into one tree. New
/// values can be appended safely later (stored as int) as more cases come up.
/// </summary>
public enum PersonAffiliationType
{
/// <summary>
/// Person1 served as minister/vizier to Person2
/// </summary>
Minister = 0,
/// <summary>
/// Person1 was an advisor/counselor to Person2, without holding a formal ministerial post
/// </summary>
Advisor = 1,
/// <summary>
/// Person1 was a courtier/attendant/servant of Person2 (a catch-all for court-affiliated
/// roles not covered by a more specific type)
/// </summary>
Courtier = 2,
/// <summary>
/// Person1 was a patron/sponsor of Person2 (e.g. a king patronizing a poet)
/// </summary>
Patron = 3,
/// <summary>
/// Person1 and Person2 were allies (symmetric - order doesn't matter)
/// </summary>
Ally = 4,
/// <summary>
/// Person1 and Person2 were rivals/enemies (symmetric - order doesn't matter)
/// </summary>
Rival = 5,
/// <summary>
/// doesn't fit any of the above - rely on Note for what the tie actually is
/// </summary>
Other = 99,
}
}

View File

@ -0,0 +1,35 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// the kind of kinship edge a GanjoorPersonRelation represents between two GanjoorRelatedPerson
/// rows. The graph is not a strict tree - siblings can be known without their parents, an
/// ancestor can be known without the exact number of generations in between, etc. - so this is
/// a small, deliberately open set rather than a rigid parent/child-only model.
/// </summary>
public enum PersonRelationType
{
/// <summary>
/// Person1 is a parent of Person2 (directional - the mirror "child" relation is implied,
/// not stored as a second row)
/// </summary>
Parent = 0,
/// <summary>
/// Person1 and Person2 are siblings (symmetric - order doesn't matter). Doesn't require
/// either parent to be known/recorded.
/// </summary>
Sibling = 1,
/// <summary>
/// Person1 and Person2 are spouses (symmetric - order doesn't matter)
/// </summary>
Spouse = 2,
/// <summary>
/// Person1 is a known ancestor of Person2 (directional) but the exact number of generations
/// between them is not known/recorded - see GanjoorPersonRelation.DegreeHint for the case
/// where the exact degree (e.g. "grandparent") IS known
/// </summary>
Ancestor = 3,
}
}