people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-28 08:49:48 +03:30
parent da4a0f7c14
commit 1614b6e7ca
5 changed files with 6832 additions and 2 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "9.0.2", "version": "10.0.12",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
], ],

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RMuseum.Migrations
{
/// <inheritdoc />
public partial class FamilyTreeModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FamilyTreeCaption",
table: "GanjoorRelatedPersons",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SuggestedPersonGraphJson",
table: "GanjoorPoemGeoDateTagCorrection",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateTable(
name: "GanjoorPersonAffiliations",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Person1Id = table.Column<int>(type: "int", nullable: false),
Person2Id = table.Column<int>(type: "int", nullable: false),
AffiliationType = table.Column<int>(type: "int", nullable: false),
Note = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GanjoorPersonAffiliations", x => x.Id);
table.ForeignKey(
name: "FK_GanjoorPersonAffiliations_GanjoorRelatedPersons_Person1Id",
column: x => x.Person1Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_GanjoorPersonAffiliations_GanjoorRelatedPersons_Person2Id",
column: x => x.Person2Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "GanjoorPersonRelations",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Person1Id = table.Column<int>(type: "int", nullable: false),
Person2Id = table.Column<int>(type: "int", nullable: false),
RelationType = table.Column<int>(type: "int", nullable: false),
DegreeHint = table.Column<int>(type: "int", nullable: true),
Note = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GanjoorPersonRelations", x => x.Id);
table.ForeignKey(
name: "FK_GanjoorPersonRelations_GanjoorRelatedPersons_Person1Id",
column: x => x.Person1Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_GanjoorPersonRelations_GanjoorRelatedPersons_Person2Id",
column: x => x.Person2Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonAffiliations_Person1Id",
table: "GanjoorPersonAffiliations",
column: "Person1Id");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonAffiliations_Person2Id",
table: "GanjoorPersonAffiliations",
column: "Person2Id");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelations_Person1Id",
table: "GanjoorPersonRelations",
column: "Person1Id");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelations_Person2Id",
table: "GanjoorPersonRelations",
column: "Person2Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GanjoorPersonAffiliations");
migrationBuilder.DropTable(
name: "GanjoorPersonRelations");
migrationBuilder.DropColumn(
name: "FamilyTreeCaption",
table: "GanjoorRelatedPersons");
migrationBuilder.DropColumn(
name: "SuggestedPersonGraphJson",
table: "GanjoorPoemGeoDateTagCorrection");
}
}
}

View File

@ -1399,6 +1399,67 @@ namespace RMuseum.Migrations
b.ToTable("GanjoorPageSnapshots"); b.ToTable("GanjoorPageSnapshots");
}); });
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonAffiliation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AffiliationType")
.HasColumnType("int");
b.Property<string>("Note")
.HasColumnType("nvarchar(max)");
b.Property<int>("Person1Id")
.HasColumnType("int");
b.Property<int>("Person2Id")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Person1Id");
b.HasIndex("Person2Id");
b.ToTable("GanjoorPersonAffiliations");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("DegreeHint")
.HasColumnType("int");
b.Property<string>("Note")
.HasColumnType("nvarchar(max)");
b.Property<int>("Person1Id")
.HasColumnType("int");
b.Property<int>("Person2Id")
.HasColumnType("int");
b.Property<int>("RelationType")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Person1Id");
b.HasIndex("Person2Id");
b.ToTable("GanjoorPersonRelations");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b => modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1667,6 +1728,9 @@ namespace RMuseum.Migrations
b.Property<double?>("SuggestedLongitude") b.Property<double?>("SuggestedLongitude")
.HasColumnType("float"); .HasColumnType("float");
b.Property<string>("SuggestedPersonGraphJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SuggestionNote") b.Property<string>("SuggestionNote")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@ -2205,6 +2269,9 @@ namespace RMuseum.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("FamilyTreeCaption")
.HasColumnType("nvarchar(max)");
b.Property<bool>("MachineGenerated") b.Property<bool>("MachineGenerated")
.HasColumnType("bit"); .HasColumnType("bit");
@ -5219,6 +5286,44 @@ namespace RMuseum.Migrations
b.Navigation("MadeObsoleteByUser"); b.Navigation("MadeObsoleteByUser");
}); });
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonAffiliation", b =>
{
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person1")
.WithMany()
.HasForeignKey("Person1Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person2")
.WithMany()
.HasForeignKey("Person2Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Person1");
b.Navigation("Person2");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelation", b =>
{
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person1")
.WithMany()
.HasForeignKey("Person1Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person2")
.WithMany()
.HasForeignKey("Person2Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Person1");
b.Navigation("Person2");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b => modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b =>
{ {
b.HasOne("RMuseum.Models.Ganjoor.GanjoorCat", "Cat") b.HasOne("RMuseum.Models.Ganjoor.GanjoorCat", "Cat")

View File

@ -3925,6 +3925,16 @@
People tags People tags
</summary> </summary>
</member> </member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.GanjoorPersonRelations">
<summary>
approved kinship edges between people (family tree) - see GanjoorPersonRelation
</summary>
</member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.GanjoorPersonAffiliations">
<summary>
approved non-family ties between people (e.g. minister-to-king) - see GanjoorPersonAffiliation
</summary>
</member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.Books"> <member name="P:RMuseum.DbContext.RMuseumDbContext.Books">
<summary> <summary>
Books (PDF Library) Books (PDF Library)
@ -4748,6 +4758,18 @@
<member name="M:RMuseum.Migrations.AddPoemGeoSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)"> <member name="M:RMuseum.Migrations.AddPoemGeoSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc /> <inheritdoc />
</member> </member>
<member name="T:RMuseum.Migrations.FamilyTreeModels">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.FamilyTreeModels.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.FamilyTreeModels.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.FamilyTreeModels.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Models.Accounting.DonationExpenditure"> <member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary> <summary>
donation expenditures donation expenditures
@ -9119,6 +9141,105 @@
Prosody and Statistics Prosody and Statistics
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Id">
<summary>
record id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Person1Id">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Person1">
<summary>
first person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Person2Id">
<summary>
second person in the tie - for a directional type this is the one being served
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Person2">
<summary>
second person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.AffiliationType">
<summary>
the kind of tie between Person1 and Person2
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonAffiliation.Note">
<summary>
free-text note (e.g. sourcing/reasoning, or what the tie actually is when AffiliationType is Other)
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPersonRelation">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Id">
<summary>
record id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Person1Id">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Person1">
<summary>
first person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Person2Id">
<summary>
second person in the relation - for a directional relation type (Parent, Ancestor) this is
the child/descendant side
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Person2">
<summary>
second person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.RelationType">
<summary>
the kind of relation between Person1 and Person2
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.DegreeHint">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelation.Note">
<summary>
free-text note (e.g. sourcing/reasoning for this relation)
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPoem"> <member name="T:RMuseum.Models.Ganjoor.GanjoorPoem">
<summary> <summary>
Ganjoor Poem Ganjoor Poem
@ -9646,7 +9767,8 @@
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.PersonId"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.PersonId">
<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>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.Person"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.Person">
@ -9654,6 +9776,35 @@
related person (navigation) related person (navigation)
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson">
<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>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.IgnoreInCategory"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPoemGeoDateTagCorrection.IgnoreInCategory">
<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
@ -10631,6 +10782,15 @@
AI generated AI generated
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorRelatedPerson.FamilyTreeCaption">
<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>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner"> <member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner">
<summary> <summary>
ganjoor.net site banner ganjoor.net site banner
@ -11123,6 +11283,83 @@
summary review result summary review result
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.PersonAffiliationType">
<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>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Minister">
<summary>
Person1 served as minister/vizier to Person2
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Advisor">
<summary>
Person1 was an advisor/counselor to Person2, without holding a formal ministerial post
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Courtier">
<summary>
Person1 was a courtier/attendant/servant of Person2 (a catch-all for court-affiliated
roles not covered by a more specific type)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Patron">
<summary>
Person1 was a patron/sponsor of Person2 (e.g. a king patronizing a poet)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Ally">
<summary>
Person1 and Person2 were allies (symmetric - order doesn't matter)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Rival">
<summary>
Person1 and Person2 were rivals/enemies (symmetric - order doesn't matter)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Other">
<summary>
doesn't fit any of the above - rely on Note for what the tie actually is
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationType">
<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>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationType.Parent">
<summary>
Person1 is a parent of Person2 (directional - the mirror "child" relation is implied,
not stored as a second row)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationType.Sibling">
<summary>
Person1 and Person2 are siblings (symmetric - order doesn't matter). Doesn't require
either parent to be known/recorded.
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationType.Spouse">
<summary>
Person1 and Person2 are spouses (symmetric - order doesn't matter)
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationType.Ancestor">
<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>
</member>
<member name="T:RMuseum.Models.Ganjoor.PoemGeoDateTag"> <member name="T:RMuseum.Models.Ganjoor.PoemGeoDateTag">
<summary> <summary>
Geo + date tags for poems Geo + date tags for poems