people tags #387
This commit is contained in:
parent
1614b6e7ca
commit
5003f92f00
100
RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs
Normal file
100
RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RMuseum.Models.Ganjoor.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// deserialized shape of GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson - a brand new
|
||||
/// person (optionally linked to other people, new or existing) suggested in one go alongside a
|
||||
/// poem's geo/date/person tag. See that field's doc comment for the full JSON shape.
|
||||
/// </summary>
|
||||
public class PersonGraphSuggestion
|
||||
{
|
||||
/// <summary>
|
||||
/// the node that ends up assigned to the tag's PersonId once approved
|
||||
/// </summary>
|
||||
public PersonGraphNode Person { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// other people this submission also introduces or links to, referenced from Relations by
|
||||
/// their LocalKey
|
||||
/// </summary>
|
||||
public List<PersonGraphNode> RelatedPeople { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// kinship and/or non-family edges between any of Person/RelatedPeople's local keys
|
||||
/// </summary>
|
||||
public List<PersonGraphRelationEntry> Relations { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// one person node in a PersonGraphSuggestion - either a link to an existing, already approved
|
||||
/// GanjoorRelatedPerson (ExistingPersonId set, the rest ignored) or a brand new person to create
|
||||
/// (ExistingPersonId null, the rest describing them)
|
||||
/// </summary>
|
||||
public class PersonGraphNode
|
||||
{
|
||||
/// <summary>
|
||||
/// key used only to resolve Relations entries within this same submission - never stored
|
||||
/// beyond approval time
|
||||
/// </summary>
|
||||
public string LocalKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// set to link to an existing, already approved person instead of creating a new one - when
|
||||
/// set, every other field on this node is ignored
|
||||
/// </summary>
|
||||
public int? ExistingPersonId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string WikiUrl { get; set; }
|
||||
public int BirthYearInLHijri { get; set; }
|
||||
public int DeathYearInLHijri { get; set; }
|
||||
public bool ValidBirthDate { get; set; }
|
||||
public bool ValidDeathDate { get; set; }
|
||||
public int? BirthLocationId { get; set; }
|
||||
public int? DeathLocationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// see GanjoorRelatedPerson.FamilyTreeCaption - only meaningful for whichever node is meant
|
||||
/// to be a tree's named root
|
||||
/// </summary>
|
||||
public string FamilyTreeCaption { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// one edge in a PersonGraphSuggestion, between two nodes referenced by their LocalKey (Person1/
|
||||
/// Person2 - matching PersonGraphNode.LocalKey, not a real database id yet). Kind picks which
|
||||
/// enum RelationType/AffiliationType is parsed against and which live table the edge is
|
||||
/// materialized into on approval.
|
||||
/// </summary>
|
||||
public class PersonGraphRelationEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// "family" (default, materialized as GanjoorPersonRelation - RelationType against
|
||||
/// PersonRelationType) or "affiliation" (materialized as GanjoorPersonAffiliation -
|
||||
/// AffiliationType against PersonAffiliationType)
|
||||
/// </summary>
|
||||
public string Kind { get; set; }
|
||||
|
||||
public string Person1 { get; set; }
|
||||
public string Person2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// parsed against PersonRelationType when Kind is "family"
|
||||
/// </summary>
|
||||
public string RelationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// parsed against PersonAffiliationType when Kind is "affiliation"
|
||||
/// </summary>
|
||||
public string AffiliationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only meaningful for a family relation of type Ancestor - see GanjoorPersonRelation.DegreeHint
|
||||
/// </summary>
|
||||
public int? DegreeHint { get; set; }
|
||||
|
||||
public string Note { get; set; }
|
||||
}
|
||||
}
|
||||
@ -719,6 +719,20 @@ namespace RMuseum.Services.Implementation
|
||||
approvedLocationId = newLocation.Id;
|
||||
}
|
||||
|
||||
int? approvedPersonId = dbGeoDateTag.PersonId;
|
||||
if (approvedPersonId == null && !string.IsNullOrWhiteSpace(dbGeoDateTag.SuggestedPersonGraphJson))
|
||||
{
|
||||
// a brand new person (and possibly their relatives/relations, some of which may
|
||||
// also be brand new) - resolve/create everything the JSON describes and use
|
||||
// whichever person node it designates as the one actually being tagged here
|
||||
var personGraphResult = await _MaterializePersonGraphAsync(dbGeoDateTag.SuggestedPersonGraphJson);
|
||||
if (!string.IsNullOrEmpty(personGraphResult.Item2))
|
||||
{
|
||||
return new RServiceResult<GanjoorPoemCorrectionViewModel>(null, personGraphResult.Item2);
|
||||
}
|
||||
approvedPersonId = personGraphResult.Item1;
|
||||
}
|
||||
|
||||
var newTag = new PoemGeoDateTag()
|
||||
{
|
||||
PoemId = dbCorrection.PoemId,
|
||||
@ -728,7 +742,7 @@ namespace RMuseum.Services.Implementation
|
||||
LunarYear = dbGeoDateTag.LunarYear,
|
||||
LunarMonth = dbGeoDateTag.LunarMonth,
|
||||
LunarDay = dbGeoDateTag.LunarDay,
|
||||
PersonId = dbGeoDateTag.PersonId,
|
||||
PersonId = approvedPersonId,
|
||||
IgnoreInCategory = dbGeoDateTag.IgnoreInCategory,
|
||||
VerifiedDate = false,
|
||||
MachineGenerated = false,
|
||||
@ -822,5 +836,134 @@ namespace RMuseum.Services.Implementation
|
||||
return new RServiceResult<GanjoorPoemCorrectionViewModel>(null, exp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// materializes a GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson payload: creates
|
||||
/// any brand new person nodes it describes (or reuses an existing one when a node names
|
||||
/// ExistingPersonId), then creates the kinship (GanjoorPersonRelation) and/or non-family
|
||||
/// (GanjoorPersonAffiliation) edges between them. Returns the resolved id of the "person"
|
||||
/// node (the one that ends up assigned to the tag's PersonId), or an error message.
|
||||
/// </summary>
|
||||
/// <param name="json">SuggestedPersonGraphJson value - see its doc comment for the expected shape</param>
|
||||
private async Task<Tuple<int, string>> _MaterializePersonGraphAsync(string json)
|
||||
{
|
||||
PersonGraphSuggestion graph;
|
||||
try
|
||||
{
|
||||
graph = Newtonsoft.Json.JsonConvert.DeserializeObject<PersonGraphSuggestion>(json);
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
return new Tuple<int, string>(0, $"برچسب فرد پیشنهادی قابل تفسیر نیست: {exp.Message}");
|
||||
}
|
||||
|
||||
if (graph?.Person == null || string.IsNullOrWhiteSpace(graph.Person.LocalKey))
|
||||
{
|
||||
return new Tuple<int, string>(0, "برچسب فرد پیشنهادی ناقص است.");
|
||||
}
|
||||
|
||||
// first pass: resolve/create every node (the tagged person plus anyone else referenced),
|
||||
// building a localKey -> real database id map for the relations pass below. A node
|
||||
// referencing another not-yet-existing node has no real id to point at until this pass
|
||||
// finishes, which is exactly why this travels as one JSON blob instead of separate rows.
|
||||
var allNodes = new List<PersonGraphNode>() { graph.Person };
|
||||
if (graph.RelatedPeople != null)
|
||||
{
|
||||
allNodes.AddRange(graph.RelatedPeople);
|
||||
}
|
||||
|
||||
var localKeyToPersonId = new Dictionary<string, int>();
|
||||
foreach (var node in allNodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(node.LocalKey))
|
||||
{
|
||||
return new Tuple<int, string>(0, "یکی از افراد برچسب پیشنهادی بدون کلید محلی است.");
|
||||
}
|
||||
if (localKeyToPersonId.ContainsKey(node.LocalKey))
|
||||
{
|
||||
continue; // same local key reused (e.g. Person also listed in RelatedPeople) - already resolved
|
||||
}
|
||||
|
||||
if (node.ExistingPersonId != null)
|
||||
{
|
||||
var existingPerson = await _context.GanjoorRelatedPersons.Where(p => p.Id == node.ExistingPersonId).AnyAsync();
|
||||
if (!existingPerson)
|
||||
{
|
||||
return new Tuple<int, string>(0, $"فرد موجود با کد {node.ExistingPersonId} پیدا نشد.");
|
||||
}
|
||||
localKeyToPersonId[node.LocalKey] = node.ExistingPersonId.Value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(node.Name))
|
||||
{
|
||||
return new Tuple<int, string>(0, "نام یکی از افراد پیشنهادی وارد نشده است.");
|
||||
}
|
||||
|
||||
var newPerson = new GanjoorRelatedPerson()
|
||||
{
|
||||
Name = node.Name.Trim(),
|
||||
Description = node.Description,
|
||||
WikiUrl = node.WikiUrl,
|
||||
BirthYearInLHijri = node.BirthYearInLHijri,
|
||||
DeathYearInLHijri = node.DeathYearInLHijri,
|
||||
ValidBirthDate = node.ValidBirthDate,
|
||||
ValidDeathDate = node.ValidDeathDate,
|
||||
BirthLocationId = node.BirthLocationId,
|
||||
DeathLocationId = node.DeathLocationId,
|
||||
FamilyTreeCaption = string.IsNullOrWhiteSpace(node.FamilyTreeCaption) ? null : node.FamilyTreeCaption.Trim(),
|
||||
MachineGenerated = false,
|
||||
};
|
||||
_context.GanjoorRelatedPersons.Add(newPerson);
|
||||
await _context.SaveChangesAsync(); // need its Id below, other nodes/relations may reference it
|
||||
localKeyToPersonId[node.LocalKey] = newPerson.Id;
|
||||
}
|
||||
|
||||
// second pass: now that every node has a real id, create the edges between them
|
||||
if (graph.Relations != null)
|
||||
{
|
||||
foreach (var relation in graph.Relations)
|
||||
{
|
||||
if (!localKeyToPersonId.TryGetValue(relation.Person1 ?? "", out int person1Id) ||
|
||||
!localKeyToPersonId.TryGetValue(relation.Person2 ?? "", out int person2Id))
|
||||
{
|
||||
return new Tuple<int, string>(0, "یکی از روابط پیشنهادی به فردی خارج از این پیشنهاد اشاره میکند.");
|
||||
}
|
||||
|
||||
if (string.Equals(relation.Kind, "affiliation", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!Enum.TryParse<PersonAffiliationType>(relation.AffiliationType, out var affiliationType))
|
||||
{
|
||||
return new Tuple<int, string>(0, $"نوع رابطهٔ غیرخویشاوندی «{relation.AffiliationType}» نامعتبر است.");
|
||||
}
|
||||
_context.GanjoorPersonAffiliations.Add(new GanjoorPersonAffiliation()
|
||||
{
|
||||
Person1Id = person1Id,
|
||||
Person2Id = person2Id,
|
||||
AffiliationType = affiliationType,
|
||||
Note = relation.Note,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<PersonRelationType>(relation.RelationType, out var relationType))
|
||||
{
|
||||
return new Tuple<int, string>(0, $"نوع رابطهٔ خویشاوندی «{relation.RelationType}» نامعتبر است.");
|
||||
}
|
||||
_context.GanjoorPersonRelations.Add(new GanjoorPersonRelation()
|
||||
{
|
||||
Person1Id = person1Id,
|
||||
Person2Id = person2Id,
|
||||
RelationType = relationType,
|
||||
DegreeHint = relation.DegreeHint,
|
||||
Note = relation.Note,
|
||||
});
|
||||
}
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return new Tuple<int, string>(localKeyToPersonId[graph.Person.LocalKey], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user