people tags #387
This commit is contained in:
parent
94859046a3
commit
a5f26d1381
@ -11952,6 +11952,15 @@
|
||||
use one of the other types instead whenever an actual relationship is known
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:RMuseum.Models.Ganjoor.PersonAffiliationType.Killer">
|
||||
<summary>
|
||||
Person1 killed Person2 (کشتن) - directional. Deliberately separate from Rival: an act of
|
||||
killing is usually a single event (e.g. in battle, by treachery, in a duel), and doesn't by
|
||||
itself mean the two were ongoing enemies/rivals beforehand - they could have been allies,
|
||||
kin-by-marriage, or strangers. Use Rival instead (additionally, if applicable) when there
|
||||
was a standing enmity, and Note for how/why the killing happened
|
||||
</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
|
||||
@ -22206,6 +22215,64 @@
|
||||
<param name="rootId"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._FindOtherFamilyTreeCaptionHolderInComponentAsync(System.Int32)">
|
||||
<summary>
|
||||
finds another person already carrying a non-null FamilyTreeCaption within the same
|
||||
connected kinship component as personId (if any). Captions are resolved per requested
|
||||
person, not stored on the tree itself, so nothing stops two different people in one
|
||||
connected component from each getting their own caption - which would silently produce
|
||||
two separate "family tree" list entries (GetFamilyTreeRootsAsync) that both open to the
|
||||
exact same graph. This is used to catch that at approval time rather than let it happen
|
||||
silently; it only ever flags an OTHER person, so re-saving/editing the caption a person
|
||||
already uniquely holds in their own tree is never blocked by it.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._GetAncestorEdgesAsync(System.Nullable{System.Int32})">
|
||||
<summary>
|
||||
loads every directed ancestor-type kinship edge (RelationType Parent or Ancestor,
|
||||
Person1 = ancestor, Person2 = descendant) currently in the live graph, optionally
|
||||
excluding one relation row by id (used so a Modify suggestion can be checked against
|
||||
every OTHER edge without tripping on the very row it's about to replace)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._WouldCreateAncestryCycleAsync(System.Int32,System.Int32,System.Nullable{System.Int32})">
|
||||
<summary>
|
||||
true if adding a directed ancestor-type edge ancestorId -> descendantId (ancestorId
|
||||
becomes a parent/ancestor of descendantId) would create a cycle in the kinship graph -
|
||||
i.e. descendantId is already (directly or transitively) an ancestor of ancestorId, or
|
||||
they're literally the same person. Without this, nothing stops e.g. approving "A is
|
||||
parent of B" and later "B is parent of A", which familytree.js's unguarded recursive
|
||||
layout() would then infinite-loop on when rendering that tree.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._CountParentsAsync(System.Int32,System.Nullable{System.Int32})">
|
||||
<summary>
|
||||
counts this child's distinct existing Parent-type edges (biological parents), optionally
|
||||
excluding one relation row by id - used to cap a person at two recorded parents, since
|
||||
familytree.js's buildLayout only ever attaches the first two it sorts to the front and
|
||||
silently drops any further ones with no error
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._GetConflictingRelationAsync(System.Int32,System.Int32,RMuseum.Models.Ganjoor.PersonRelationType,System.Nullable{System.Int32})">
|
||||
<summary>
|
||||
returns an existing kinship edge between this unordered pair (if any), other than
|
||||
excludeRelationId, whose RelationType differs from proposedType - used to stop a pair
|
||||
from simultaneously carrying two contradictory family relations (e.g. Parent AND Spouse,
|
||||
or Parent AND Sibling, between the very same two people)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService._ValidateFamilyRelationAsync(System.Int32,System.Int32,RMuseum.Models.Ganjoor.PersonRelationType,System.Nullable{System.Int32})">
|
||||
<summary>
|
||||
runs every family-relation data-integrity check (self-reference is checked separately by
|
||||
the caller for Add) that applies to adding/changing a kinship edge of relationType between
|
||||
person1Id and person2Id: ancestry cycles, the two-parents cap, and contradictory relation
|
||||
types already existing between the same pair. Returns a Persian error message, or null if
|
||||
the edge is fine to create/apply. Shared by SuggestPersonRelationEditAsync (so a
|
||||
contradictory suggestion is rejected up front) and ModeratePersonRelationEditSuggestionAsync
|
||||
(so it's still caught even if another suggestion was approved in the meantime, or the
|
||||
submission-time check is ever bypassed).
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.SuggestPersonEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion)">
|
||||
<summary>
|
||||
submit a suggested edit to an already-approved person's own fields
|
||||
@ -22935,6 +23002,17 @@
|
||||
</summary>
|
||||
<param name="json">SuggestedPersonGraphJson value - see its doc comment for the expected shape</param>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService._WouldCreateAncestryCycleInGraphAsync(System.Int32,System.Int32,System.Collections.Generic.List{System.ValueTuple{System.Int32,System.Int32}})">
|
||||
<summary>
|
||||
true if adding a directed ancestor-type edge ancestorId -> descendantId (ancestorId
|
||||
becomes a parent/ancestor of descendantId) would create a cycle, counting both the
|
||||
already-live kinship graph AND any ancestor-type edges added earlier in the same
|
||||
in-progress _MaterializePersonGraphAsync batch (extraEdges) - mirrors
|
||||
GanjoorRelatedPersonService's own _WouldCreateAncestryCycleAsync, duplicated here since
|
||||
this runs in a different service/DbContext and also has to account for not-yet-saved,
|
||||
same-batch edges that the other copy never needs to.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RMuseum.Services.Implementation.GanjoorService.ModeratePoemSectionCorrection(System.Guid,RMuseum.Models.Ganjoor.ViewModels.GanjoorPoemSectionCorrectionViewModel)">
|
||||
<summary>
|
||||
moderate poem section correction
|
||||
|
||||
@ -280,6 +280,203 @@ namespace RMuseum.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// finds another person already carrying a non-null FamilyTreeCaption within the same
|
||||
/// connected kinship component as personId (if any). Captions are resolved per requested
|
||||
/// person, not stored on the tree itself, so nothing stops two different people in one
|
||||
/// connected component from each getting their own caption - which would silently produce
|
||||
/// two separate "family tree" list entries (GetFamilyTreeRootsAsync) that both open to the
|
||||
/// exact same graph. This is used to catch that at approval time rather than let it happen
|
||||
/// silently; it only ever flags an OTHER person, so re-saving/editing the caption a person
|
||||
/// already uniquely holds in their own tree is never blocked by it.
|
||||
/// </summary>
|
||||
private async Task<GanjoorRelatedPerson> _FindOtherFamilyTreeCaptionHolderInComponentAsync(int personId)
|
||||
{
|
||||
var allRelations = await _context.GanjoorPersonRelations.ToListAsync();
|
||||
var edgesByPersonId = new Dictionary<int, List<GanjoorPersonRelation>>();
|
||||
void IndexEdge(int pid, GanjoorPersonRelation edge)
|
||||
{
|
||||
if (!edgesByPersonId.TryGetValue(pid, out var list))
|
||||
{
|
||||
list = new List<GanjoorPersonRelation>();
|
||||
edgesByPersonId[pid] = list;
|
||||
}
|
||||
list.Add(edge);
|
||||
}
|
||||
foreach (var edge in allRelations)
|
||||
{
|
||||
IndexEdge(edge.Person1Id, edge);
|
||||
IndexEdge(edge.Person2Id, edge);
|
||||
}
|
||||
|
||||
var visited = new HashSet<int>() { personId };
|
||||
var queue = new Queue<int>();
|
||||
queue.Enqueue(personId);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (!edgesByPersonId.TryGetValue(current, out var touchingEdges))
|
||||
continue;
|
||||
foreach (var edge in touchingEdges)
|
||||
{
|
||||
var otherPersonId = edge.Person1Id == current ? edge.Person2Id : edge.Person1Id;
|
||||
if (visited.Add(otherPersonId))
|
||||
{
|
||||
queue.Enqueue(otherPersonId);
|
||||
}
|
||||
}
|
||||
}
|
||||
visited.Remove(personId);
|
||||
|
||||
if (visited.Count == 0)
|
||||
return null;
|
||||
|
||||
return await _context.GanjoorRelatedPersons
|
||||
.Where(p => visited.Contains(p.Id) && !string.IsNullOrEmpty(p.FamilyTreeCaption))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// loads every directed ancestor-type kinship edge (RelationType Parent or Ancestor,
|
||||
/// Person1 = ancestor, Person2 = descendant) currently in the live graph, optionally
|
||||
/// excluding one relation row by id (used so a Modify suggestion can be checked against
|
||||
/// every OTHER edge without tripping on the very row it's about to replace)
|
||||
/// </summary>
|
||||
private async Task<List<GanjoorPersonRelation>> _GetAncestorEdgesAsync(int? excludeRelationId)
|
||||
{
|
||||
var query = _context.GanjoorPersonRelations
|
||||
.Where(r => r.RelationType == PersonRelationType.Parent || r.RelationType == PersonRelationType.Ancestor);
|
||||
if (excludeRelationId != null)
|
||||
{
|
||||
query = query.Where(r => r.Id != excludeRelationId.Value);
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true if adding a directed ancestor-type edge ancestorId -> descendantId (ancestorId
|
||||
/// becomes a parent/ancestor of descendantId) would create a cycle in the kinship graph -
|
||||
/// i.e. descendantId is already (directly or transitively) an ancestor of ancestorId, or
|
||||
/// they're literally the same person. Without this, nothing stops e.g. approving "A is
|
||||
/// parent of B" and later "B is parent of A", which familytree.js's unguarded recursive
|
||||
/// layout() would then infinite-loop on when rendering that tree.
|
||||
/// </summary>
|
||||
private async Task<bool> _WouldCreateAncestryCycleAsync(int ancestorId, int descendantId, int? excludeRelationId)
|
||||
{
|
||||
if (ancestorId == descendantId)
|
||||
return true;
|
||||
|
||||
var edges = await _GetAncestorEdgesAsync(excludeRelationId);
|
||||
var childrenOf = new Dictionary<int, List<int>>();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
if (!childrenOf.TryGetValue(edge.Person1Id, out var list))
|
||||
{
|
||||
list = new List<int>();
|
||||
childrenOf[edge.Person1Id] = list;
|
||||
}
|
||||
list.Add(edge.Person2Id);
|
||||
}
|
||||
|
||||
// walk forward from descendantId: if it can already reach ancestorId through existing
|
||||
// edges, descendantId is already an ancestor of ancestorId, so the new edge would close a loop
|
||||
var visited = new HashSet<int>() { descendantId };
|
||||
var queue = new Queue<int>();
|
||||
queue.Enqueue(descendantId);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (current == ancestorId)
|
||||
return true;
|
||||
if (!childrenOf.TryGetValue(current, out var children))
|
||||
continue;
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (visited.Add(child))
|
||||
{
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// counts this child's distinct existing Parent-type edges (biological parents), optionally
|
||||
/// excluding one relation row by id - used to cap a person at two recorded parents, since
|
||||
/// familytree.js's buildLayout only ever attaches the first two it sorts to the front and
|
||||
/// silently drops any further ones with no error
|
||||
/// </summary>
|
||||
private async Task<int> _CountParentsAsync(int childId, int? excludeRelationId)
|
||||
{
|
||||
var query = _context.GanjoorPersonRelations
|
||||
.Where(r => r.RelationType == PersonRelationType.Parent && r.Person2Id == childId);
|
||||
if (excludeRelationId != null)
|
||||
{
|
||||
query = query.Where(r => r.Id != excludeRelationId.Value);
|
||||
}
|
||||
return await query.Select(r => r.Person1Id).Distinct().CountAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns an existing kinship edge between this unordered pair (if any), other than
|
||||
/// excludeRelationId, whose RelationType differs from proposedType - used to stop a pair
|
||||
/// from simultaneously carrying two contradictory family relations (e.g. Parent AND Spouse,
|
||||
/// or Parent AND Sibling, between the very same two people)
|
||||
/// </summary>
|
||||
private async Task<GanjoorPersonRelation> _GetConflictingRelationAsync(int person1Id, int person2Id, PersonRelationType proposedType, int? excludeRelationId)
|
||||
{
|
||||
var query = _context.GanjoorPersonRelations
|
||||
.Include(r => r.Person1)
|
||||
.Include(r => r.Person2)
|
||||
.Where(r =>
|
||||
((r.Person1Id == person1Id && r.Person2Id == person2Id) || (r.Person1Id == person2Id && r.Person2Id == person1Id))
|
||||
&& r.RelationType != proposedType);
|
||||
if (excludeRelationId != null)
|
||||
{
|
||||
query = query.Where(r => r.Id != excludeRelationId.Value);
|
||||
}
|
||||
return await query.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// runs every family-relation data-integrity check (self-reference is checked separately by
|
||||
/// the caller for Add) that applies to adding/changing a kinship edge of relationType between
|
||||
/// person1Id and person2Id: ancestry cycles, the two-parents cap, and contradictory relation
|
||||
/// types already existing between the same pair. Returns a Persian error message, or null if
|
||||
/// the edge is fine to create/apply. Shared by SuggestPersonRelationEditAsync (so a
|
||||
/// contradictory suggestion is rejected up front) and ModeratePersonRelationEditSuggestionAsync
|
||||
/// (so it's still caught even if another suggestion was approved in the meantime, or the
|
||||
/// submission-time check is ever bypassed).
|
||||
/// </summary>
|
||||
private async Task<string> _ValidateFamilyRelationAsync(int person1Id, int person2Id, PersonRelationType relationType, int? excludeRelationId)
|
||||
{
|
||||
if (relationType == PersonRelationType.Parent || relationType == PersonRelationType.Ancestor)
|
||||
{
|
||||
if (await _WouldCreateAncestryCycleAsync(person1Id, person2Id, excludeRelationId))
|
||||
{
|
||||
return "این نسبت باعث ایجاد حلقهٔ تناقضآمیز در شجرهنامه میشود (مثلاً فردی نیای خود شناخته میشود). لطفاً نسبتهای موجود بین این دو نفر و نیاکان/نوادگان آنها را بررسی کنید.";
|
||||
}
|
||||
}
|
||||
|
||||
if (relationType == PersonRelationType.Parent)
|
||||
{
|
||||
var existingParentsCount = await _CountParentsAsync(person2Id, excludeRelationId);
|
||||
if (existingParentsCount >= 2)
|
||||
{
|
||||
return "این نامبرده هماکنون دو پدر/مادر ثبتشده دارد. برای افزودن سومی، نخست یکی از نسبتهای پدر/مادری موجود را ویرایش یا حذف کنید.";
|
||||
}
|
||||
}
|
||||
|
||||
var conflictingRelation = await _GetConflictingRelationAsync(person1Id, person2Id, relationType, excludeRelationId);
|
||||
if (conflictingRelation != null)
|
||||
{
|
||||
return $"هماکنون نسبت خویشاوندی دیگری بین «{conflictingRelation.Person1?.Name}» و «{conflictingRelation.Person2?.Name}» ثبت شده که با نوع جدید پیشنهادی در تناقض است. لطفاً نخست آن را ویرایش یا حذف کنید.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// submit a suggested edit to an already-approved person's own fields
|
||||
/// </summary>
|
||||
@ -411,6 +608,16 @@ namespace RMuseum.Services.Implementation
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(suggestion.SuggestedFamilyTreeCaption))
|
||||
{
|
||||
var otherCaptionHolder = await _FindOtherFamilyTreeCaptionHolderInComponentAsync(person.Id);
|
||||
if (otherCaptionHolder != null)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonEditSuggestion>(null,
|
||||
$"شخصیت «{otherCaptionHolder.Name}» هماکنون در همین خوشهٔ خویشاوندی (همان شجرهنامه) عنوان تبارنامهٔ «{otherCaptionHolder.FamilyTreeCaption}» را دارد. تأیید این پیشنهاد باعث میشود یک شجرهنامهٔ واحد دو عنوان/مدخل جداگانه در فهرست شجرهنامهها پیدا کند. نخست عنوان «{otherCaptionHolder.Name}» را حذف یا ویرایش کنید، یا این پیشنهاد را رد کنید.");
|
||||
}
|
||||
}
|
||||
|
||||
person.Name = suggestion.SuggestedName;
|
||||
person.Description = suggestion.SuggestedDescription;
|
||||
person.WikiUrl = suggestion.SuggestedWikiUrl;
|
||||
@ -679,6 +886,17 @@ namespace RMuseum.Services.Implementation
|
||||
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "یکی از دو طرف نسبت پیدا نشد.");
|
||||
}
|
||||
|
||||
if (suggestion.Kind == PersonRelationSuggestionKind.Family &&
|
||||
(suggestion.Action == PersonRelationSuggestionAction.Add || suggestion.Action == PersonRelationSuggestionAction.Modify))
|
||||
{
|
||||
var excludeRelationId = suggestion.Action == PersonRelationSuggestionAction.Modify ? suggestion.ExistingRelationId : null;
|
||||
var validationError = await _ValidateFamilyRelationAsync(suggestion.Person1Id, suggestion.Person2Id, suggestion.SuggestedRelationType, excludeRelationId);
|
||||
if (validationError != null)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, validationError);
|
||||
}
|
||||
}
|
||||
|
||||
suggestion.Id = 0;
|
||||
suggestion.ExistingRelation = null;
|
||||
suggestion.ExistingAffiliation = null;
|
||||
@ -852,6 +1070,15 @@ namespace RMuseum.Services.Implementation
|
||||
switch (suggestion.Action)
|
||||
{
|
||||
case PersonRelationSuggestionAction.Add:
|
||||
{
|
||||
// re-validated here (not just at submission time in
|
||||
// SuggestPersonRelationEditAsync) in case another suggestion
|
||||
// touching the same people/relations was approved in between
|
||||
var validationError = await _ValidateFamilyRelationAsync(suggestion.Person1Id, suggestion.Person2Id, suggestion.SuggestedRelationType, null);
|
||||
if (validationError != null)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, validationError);
|
||||
}
|
||||
_context.GanjoorPersonRelations.Add(new GanjoorPersonRelation()
|
||||
{
|
||||
Person1Id = suggestion.Person1Id,
|
||||
@ -861,6 +1088,7 @@ namespace RMuseum.Services.Implementation
|
||||
Note = suggestion.SuggestedNote,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PersonRelationSuggestionAction.Modify:
|
||||
{
|
||||
var existing = await _context.GanjoorPersonRelations.Where(r => r.Id == suggestion.ExistingRelationId.Value).SingleOrDefaultAsync();
|
||||
@ -868,6 +1096,11 @@ namespace RMuseum.Services.Implementation
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر دیگر وجود ندارد.");
|
||||
}
|
||||
var validationError = await _ValidateFamilyRelationAsync(suggestion.Person1Id, suggestion.Person2Id, suggestion.SuggestedRelationType, existing.Id);
|
||||
if (validationError != null)
|
||||
{
|
||||
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, validationError);
|
||||
}
|
||||
existing.RelationType = suggestion.SuggestedRelationType;
|
||||
existing.DegreeHint = suggestion.SuggestedDegreeHint;
|
||||
existing.Note = suggestion.SuggestedNote;
|
||||
|
||||
@ -944,6 +944,15 @@ namespace RMuseum.Services.Implementation
|
||||
// second pass: now that every node has a real id, create the edges between them
|
||||
if (graph.Relations != null)
|
||||
{
|
||||
// ancestor-type (Parent/Ancestor) edges added earlier in THIS SAME batch, kept
|
||||
// separately because they're not queryable from _context until the final
|
||||
// SaveChangesAsync below - a single suggested-person-graph payload can easily
|
||||
// describe more than one generation at once (e.g. a new person plus their new
|
||||
// parent plus that parent's own already-known parent), so the cycle/parent-cap
|
||||
// checks below need to see these in-flight edges too, not just what's already live
|
||||
var newlyAddedAncestorEdges = new List<(int AncestorId, int DescendantId)>();
|
||||
var newlyAddedRelations = new List<(int Person1Id, int Person2Id, PersonRelationType RelationType)>();
|
||||
|
||||
foreach (var relation in graph.Relations)
|
||||
{
|
||||
if (!localKeyToPersonId.TryGetValue(relation.Person1 ?? "", out int person1Id) ||
|
||||
@ -952,6 +961,11 @@ namespace RMuseum.Services.Implementation
|
||||
return new Tuple<int, string>(0, "یکی از روابط پیشنهادی به شخصیتی خارج از این پیشنهاد اشاره میکند.");
|
||||
}
|
||||
|
||||
if (person1Id == person2Id)
|
||||
{
|
||||
return new Tuple<int, string>(0, "دو طرف یک رابطه نمیتوانند یک شخص باشند.");
|
||||
}
|
||||
|
||||
if (string.Equals(relation.Kind, "affiliation", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!Enum.TryParse<PersonAffiliationType>(relation.AffiliationType, out var affiliationType))
|
||||
@ -972,6 +986,40 @@ namespace RMuseum.Services.Implementation
|
||||
{
|
||||
return new Tuple<int, string>(0, $"نوع رابطهٔ خویشاوندی «{relation.RelationType}» نامعتبر است.");
|
||||
}
|
||||
|
||||
if (relationType == PersonRelationType.Parent || relationType == PersonRelationType.Ancestor)
|
||||
{
|
||||
if (await _WouldCreateAncestryCycleInGraphAsync(person1Id, person2Id, newlyAddedAncestorEdges))
|
||||
{
|
||||
return new Tuple<int, string>(0, "این رابطه باعث ایجاد حلقهٔ تناقضآمیز در شجرهنامه میشود (مثلاً فردی نیای خود شناخته میشود).");
|
||||
}
|
||||
}
|
||||
|
||||
if (relationType == PersonRelationType.Parent)
|
||||
{
|
||||
var existingParentsCount = await _context.GanjoorPersonRelations
|
||||
.Where(r => r.RelationType == PersonRelationType.Parent && r.Person2Id == person2Id)
|
||||
.Select(r => r.Person1Id).Distinct().CountAsync();
|
||||
var newParentsCount = newlyAddedAncestorEdges.Count(e => e.DescendantId == person2Id && e.AncestorId != person1Id);
|
||||
if (existingParentsCount + newParentsCount >= 2)
|
||||
{
|
||||
return new Tuple<int, string>(0, "این نامبرده هماکنون دو پدر/مادر دارد؛ نمیتوان سومی را از طریق این برچسب افزود.");
|
||||
}
|
||||
}
|
||||
|
||||
var conflictingExisting = await _context.GanjoorPersonRelations
|
||||
.Where(r =>
|
||||
((r.Person1Id == person1Id && r.Person2Id == person2Id) || (r.Person1Id == person2Id && r.Person2Id == person1Id))
|
||||
&& r.RelationType != relationType)
|
||||
.AnyAsync();
|
||||
var conflictingNew = newlyAddedRelations.Any(r =>
|
||||
((r.Person1Id == person1Id && r.Person2Id == person2Id) || (r.Person1Id == person2Id && r.Person2Id == person1Id))
|
||||
&& r.RelationType != relationType);
|
||||
if (conflictingExisting || conflictingNew)
|
||||
{
|
||||
return new Tuple<int, string>(0, "نسبت دیگری با نوع متفاوت بین همین دو نفر از قبل (یا در همین پیشنهاد) ثبت شده که با این مغایرت دارد.");
|
||||
}
|
||||
|
||||
_context.GanjoorPersonRelations.Add(new GanjoorPersonRelation()
|
||||
{
|
||||
Person1Id = person1Id,
|
||||
@ -980,6 +1028,12 @@ namespace RMuseum.Services.Implementation
|
||||
DegreeHint = relation.DegreeHint,
|
||||
Note = relation.Note,
|
||||
});
|
||||
|
||||
if (relationType == PersonRelationType.Parent || relationType == PersonRelationType.Ancestor)
|
||||
{
|
||||
newlyAddedAncestorEdges.Add((person1Id, person2Id));
|
||||
}
|
||||
newlyAddedRelations.Add((person1Id, person2Id, relationType));
|
||||
}
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
@ -987,5 +1041,64 @@ namespace RMuseum.Services.Implementation
|
||||
|
||||
return new Tuple<int, string>(localKeyToPersonId[graph.Person.LocalKey], null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true if adding a directed ancestor-type edge ancestorId -> descendantId (ancestorId
|
||||
/// becomes a parent/ancestor of descendantId) would create a cycle, counting both the
|
||||
/// already-live kinship graph AND any ancestor-type edges added earlier in the same
|
||||
/// in-progress _MaterializePersonGraphAsync batch (extraEdges) - mirrors
|
||||
/// GanjoorRelatedPersonService's own _WouldCreateAncestryCycleAsync, duplicated here since
|
||||
/// this runs in a different service/DbContext and also has to account for not-yet-saved,
|
||||
/// same-batch edges that the other copy never needs to.
|
||||
/// </summary>
|
||||
private async Task<bool> _WouldCreateAncestryCycleInGraphAsync(int ancestorId, int descendantId, List<(int AncestorId, int DescendantId)> extraEdges)
|
||||
{
|
||||
if (ancestorId == descendantId)
|
||||
return true;
|
||||
|
||||
var edges = await _context.GanjoorPersonRelations
|
||||
.Where(r => r.RelationType == PersonRelationType.Parent || r.RelationType == PersonRelationType.Ancestor)
|
||||
.Select(r => new { r.Person1Id, r.Person2Id })
|
||||
.ToListAsync();
|
||||
|
||||
var childrenOf = new Dictionary<int, List<int>>();
|
||||
void AddEdge(int parent, int child)
|
||||
{
|
||||
if (!childrenOf.TryGetValue(parent, out var list))
|
||||
{
|
||||
list = new List<int>();
|
||||
childrenOf[parent] = list;
|
||||
}
|
||||
list.Add(child);
|
||||
}
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
AddEdge(edge.Person1Id, edge.Person2Id);
|
||||
}
|
||||
foreach (var edge in extraEdges)
|
||||
{
|
||||
AddEdge(edge.AncestorId, edge.DescendantId);
|
||||
}
|
||||
|
||||
var visited = new HashSet<int>() { descendantId };
|
||||
var queue = new Queue<int>();
|
||||
queue.Enqueue(descendantId);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (current == ancestorId)
|
||||
return true;
|
||||
if (!childrenOf.TryGetValue(current, out var children))
|
||||
continue;
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (visited.Add(child))
|
||||
{
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user