people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-30 20:44:35 +03:30
parent 800857aadc
commit ddbef76d94
13 changed files with 7276 additions and 258 deletions

View File

@ -19,6 +19,24 @@
} }
} }
string AffiliationTypeName(PersonAffiliationType t)
{
switch (t)
{
case PersonAffiliationType.Minister: return "وزیر/کارگزار";
case PersonAffiliationType.Advisor: return "مشاور";
case PersonAffiliationType.Courtier: return "درباری/همراه";
case PersonAffiliationType.Patron: return "حامی";
case PersonAffiliationType.Ally: return "متحد";
case PersonAffiliationType.Rival: return "رقیب";
case PersonAffiliationType.Servant: return "خدمتگزار";
case PersonAffiliationType.Companion: return "همدم";
case PersonAffiliationType.Successor: return "جانشین";
case PersonAffiliationType.Other: return "نوع دیگر";
default: return t.ToString();
}
}
string ActionName(PersonRelationSuggestionAction a) string ActionName(PersonRelationSuggestionAction a)
{ {
switch (a) switch (a)
@ -49,6 +67,7 @@ else
<p> <p>
<span class="up-badge">@ActionName(s.Action)</span> <span class="up-badge">@ActionName(s.Action)</span>
<span class="up-badge">@(s.Kind == PersonRelationSuggestionKind.Affiliation ? "وابستگی غیرخویشاوندی" : "نسبت خویشاوندی")</span>
</p> </p>
<table> <table>
@ -111,25 +130,45 @@ else
</td> </td>
} }
</tr> </tr>
<tr> @if (s.Kind == PersonRelationSuggestionKind.Affiliation)
<td>نوع نسبت</td> {
<td>@(s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? RelationTypeName(s.ExistingRelation.RelationType) : RelationTypeName(s.SuggestedRelationType))</td> <tr>
@if (s.Action == PersonRelationSuggestionAction.Modify) <td>نوع وابستگی</td>
{ <td>@(s.Action == PersonRelationSuggestionAction.Modify && s.ExistingAffiliation != null ? AffiliationTypeName(s.ExistingAffiliation.AffiliationType) : AffiliationTypeName(s.SuggestedAffiliationType ?? PersonAffiliationType.Other))</td>
<td>@RelationTypeName(s.SuggestedRelationType)</td> @if (s.Action == PersonRelationSuggestionAction.Modify)
} {
</tr> <td>@AffiliationTypeName(s.SuggestedAffiliationType ?? PersonAffiliationType.Other)</td>
<tr> }
<td>درجه (در نیا/نواده)</td> </tr>
<td>@((s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? s.ExistingRelation.DegreeHint : s.SuggestedDegreeHint)?.ToString().ToPersianNumbers())</td> }
@if (s.Action == PersonRelationSuggestionAction.Modify) else
{ {
<td>@s.SuggestedDegreeHint?.ToString().ToPersianNumbers()</td> <tr>
} <td>نوع نسبت</td>
</tr> <td>@(s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? RelationTypeName(s.ExistingRelation.RelationType) : RelationTypeName(s.SuggestedRelationType))</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>@RelationTypeName(s.SuggestedRelationType)</td>
}
</tr>
<tr>
<td>درجه (در نیا/نواده)</td>
<td>@((s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? s.ExistingRelation.DegreeHint : s.SuggestedDegreeHint)?.ToString().ToPersianNumbers())</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>@s.SuggestedDegreeHint?.ToString().ToPersianNumbers()</td>
}
</tr>
}
<tr> <tr>
<td>یادداشت</td> <td>یادداشت</td>
<td><small>@(s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? s.ExistingRelation.Note : s.SuggestedNote)</small></td> <td>
<small>
@(s.Action == PersonRelationSuggestionAction.Modify
? (s.Kind == PersonRelationSuggestionKind.Affiliation ? s.ExistingAffiliation?.Note : s.ExistingRelation?.Note)
: s.SuggestedNote)
</small>
</td>
@if (s.Action == PersonRelationSuggestionAction.Modify) @if (s.Action == PersonRelationSuggestionAction.Modify)
{ {
<td><small>@s.SuggestedNote</small></td> <td><small>@s.SuggestedNote</small></td>

View File

@ -214,29 +214,6 @@
document.getElementById('geotag-new-person-name').value = typedText; document.getElementById('geotag-new-person-name').value = typedText;
} }
); );
// birth/death location pickers inside the "new person" panel - these only ever pick an
// existing location (a brand-new-location-within-a-brand-new-person is one indirection too
// many for this form), so an unmatched search has nothing to offer
setupLocationAutocomplete(
document.getElementById('geotag-new-person-birthlocation-search'),
document.getElementById('geotag-new-person-birthlocation-id'),
document.getElementById('geotag-new-person-birthlocation-results'),
allGeoLocations,
null,
function (typedText) {
upToast('این مکان در فهرست نیست. ابتدا آن را جدا (در بخش «مکان» بالا) به‌عنوان مکان جدید پیشنهاد دهید.', 'error');
}
);
setupLocationAutocomplete(
document.getElementById('geotag-new-person-deathlocation-search'),
document.getElementById('geotag-new-person-deathlocation-id'),
document.getElementById('geotag-new-person-deathlocation-results'),
allGeoLocations,
null,
function (typedText) {
upToast('این مکان در فهرست نیست. ابتدا آن را جدا (در بخش «مکان» بالا) به‌عنوان مکان جدید پیشنهاد دهید.', 'error');
}
);
}); });
var geoDateTagMap = null; var geoDateTagMap = null;
var geoDateTagMarker = null; var geoDateTagMarker = null;
@ -488,21 +465,22 @@
return null; return null;
} }
var birthLocationId = $('#geotag-new-person-birthlocation-id').val(); // birth/death year, "date is documented" and birth/death location are left out of this
var deathLocationId = $('#geotag-new-person-deathlocation-id').val(); // form for now (no real usage of them yet elsewhere) - the shape below still carries all
// of PersonGraphNode's fields, just with those ones always null/false, same as any other
// field this form doesn't offer a control for.
var person = { var person = {
localKey: 'new', localKey: 'new',
existingPersonId: null, existingPersonId: null,
name: name, name: name,
description: $('#geotag-new-person-description').val() || null, description: $('#geotag-new-person-description').val() || null,
wikiUrl: $('#geotag-new-person-wikiurl').val() || null, wikiUrl: $('#geotag-new-person-wikiurl').val() || null,
birthYearInLHijri: $('#geotag-new-person-birthyear').val() ? parseInt($('#geotag-new-person-birthyear').val()) : null, birthYearInLHijri: null,
deathYearInLHijri: $('#geotag-new-person-deathyear').val() ? parseInt($('#geotag-new-person-deathyear').val()) : null, deathYearInLHijri: null,
validBirthDate: $('#geotag-new-person-validbirth').is(':checked'), validBirthDate: false,
validDeathDate: $('#geotag-new-person-validdeath').is(':checked'), validDeathDate: false,
birthLocationId: birthLocationId ? parseInt(birthLocationId) : null, birthLocationId: null,
deathLocationId: deathLocationId ? parseInt(deathLocationId) : null, deathLocationId: null,
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null
}; };
@ -794,16 +772,6 @@
$('#geotag-new-person-name').val(''); $('#geotag-new-person-name').val('');
$('#geotag-new-person-description').val(''); $('#geotag-new-person-description').val('');
$('#geotag-new-person-wikiurl').val(''); $('#geotag-new-person-wikiurl').val('');
$('#geotag-new-person-birthyear').val('');
$('#geotag-new-person-deathyear').val('');
$('#geotag-new-person-validbirth').prop('checked', false);
$('#geotag-new-person-validdeath').prop('checked', false);
$('#geotag-new-person-birthlocation-id').val('');
$('#geotag-new-person-birthlocation-search').val('');
$('#geotag-new-person-birthlocation-results').hide().empty();
$('#geotag-new-person-deathlocation-id').val('');
$('#geotag-new-person-deathlocation-search').val('');
$('#geotag-new-person-deathlocation-results').hide().empty();
$('#geotag-new-person-familytreecaption').val(''); $('#geotag-new-person-familytreecaption').val('');
$('#geotag-person-relatives-list').empty(); $('#geotag-person-relatives-list').empty();
if (document.getElementById('new-person-widget').style.display !== 'none') { if (document.getElementById('new-person-widget').style.display !== 'none') {
@ -2617,36 +2585,6 @@ else
<label for="geotag-new-person-wikiurl">لینک ویکی‌پدیا (اختیاری)</label> <label for="geotag-new-person-wikiurl">لینک ویکی‌پدیا (اختیاری)</label>
<input type="text" id="geotag-new-person-wikiurl" /> <input type="text" id="geotag-new-person-wikiurl" />
</div> </div>
<div class="up-field-row">
<div class="up-field">
<label for="geotag-new-person-birthyear">سال تولد (قمری)</label>
<input type="text" id="geotag-new-person-birthyear" />
<span class="up-check-row"><input type="checkbox" id="geotag-new-person-validbirth" /><label for="geotag-new-person-validbirth">تاریخ تولد قطعی است</label></span>
</div>
<div class="up-field">
<label for="geotag-new-person-deathyear">سال وفات (قمری)</label>
<input type="text" id="geotag-new-person-deathyear" />
<span class="up-check-row"><input type="checkbox" id="geotag-new-person-validdeath" /><label for="geotag-new-person-validdeath">تاریخ وفات قطعی است</label></span>
</div>
</div>
<div class="up-field-row">
<div class="up-field">
<label for="geotag-new-person-birthlocation-search">محل تولد (اختیاری، فقط از مکان‌های موجود در فهرست)</label>
<div class="up-autocomplete-wrap">
<input type="text" id="geotag-new-person-birthlocation-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
<input type="hidden" id="geotag-new-person-birthlocation-id" />
<div id="geotag-new-person-birthlocation-results" class="up-autocomplete-results" style="display:none;"></div>
</div>
</div>
<div class="up-field">
<label for="geotag-new-person-deathlocation-search">محل وفات (اختیاری، فقط از مکان‌های موجود در فهرست)</label>
<div class="up-autocomplete-wrap">
<input type="text" id="geotag-new-person-deathlocation-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
<input type="hidden" id="geotag-new-person-deathlocation-id" />
<div id="geotag-new-person-deathlocation-results" class="up-autocomplete-results" style="display:none;"></div>
</div>
</div>
</div>
<div class="up-field"> <div class="up-field">
<label for="geotag-new-person-familytreecaption">عنوان شجره‌نامه (اختیاری، فقط اگر این شخصیت ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label> <label for="geotag-new-person-familytreecaption">عنوان شجره‌نامه (اختیاری، فقط اگر این شخصیت ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label>
<input type="text" id="geotag-new-person-familytreecaption" /> <input type="text" id="geotag-new-person-familytreecaption" />

View File

@ -3,7 +3,7 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{ @{
Layout = "_UserPanelLayout"; Layout = "_UserPanelLayout";
ViewData["Title"] = "پیشنهاد خویشاوند جدید"; ViewData["Title"] = "پیشنهاد خویشاوند یا وابستگی جدید";
await GanjooRazor.Utils.GanjoorSessionChecker.ApplyPermissionsToViewData(Request, Response, ViewData); await GanjooRazor.Utils.GanjoorSessionChecker.ApplyPermissionsToViewData(Request, Response, ViewData);
} }
@if (!string.IsNullOrEmpty(Model.LastError)) @if (!string.IsNullOrEmpty(Model.LastError))
@ -13,11 +13,11 @@
else else
{ {
<div class="up-page-header"> <div class="up-page-header">
<h1>پیشنهاد خویشاوند جدید برای: @Model.Person.Name</h1> <h1>پیشنهاد خویشاوند یا وابستگی جدید برای: @Model.Person.Name</h1>
</div> </div>
<p class="up-hint"> <p class="up-hint">
این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران، به شجره‌نامهٔ این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران، به شجره‌نامه/شبکهٔ وابستگی‌های
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Person.Id)">@Model.Person.Name</a> افزوده خواهد شد. <a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Person.Id)">@Model.Person.Name</a> افزوده خواهد شد.
</p> </p>
@ -32,7 +32,7 @@ else
<table> <table>
<tr> <tr>
<td>خویشاوند:</td> <td>خویشاوند یا شخصیت دیگر:</td>
<td> <td>
<select asp-for="OtherPersonId"> <select asp-for="OtherPersonId">
<option value="0">— انتخاب کنید —</option> <option value="0">— انتخاب کنید —</option>
@ -43,6 +43,18 @@ else
</select> </select>
</td> </td>
</tr> </tr>
<tr>
<td>نوع پیوند:</td>
<td>
<select id="RelationKindGroup" asp-for="RelationKindGroup">
<option value="family">نسبت خویشاوندی (پدر/مادر، فرزند، همسر، نیا/نواده...)</option>
<option value="affiliation">وابستگی غیرخویشاوندی (وزیر، مشاور، حامی، متحد، رقیب...)</option>
</select>
</td>
</tr>
</table>
<table id="family-relation-fields">
<tr> <tr>
<td>نسبت با @Model.Person.Name:</td> <td>نسبت با @Model.Person.Name:</td>
<td> <td>
@ -63,6 +75,35 @@ else
</td> </td>
<td><input asp-for="DegreeHint" /></td> <td><input asp-for="DegreeHint" /></td>
</tr> </tr>
</table>
<table id="affiliation-fields" style="display:none">
<tr>
<td>وابستگی با @Model.Person.Name:</td>
<td>
<select asp-for="AffiliationKind">
<option value="Minister_Other">او وزیر یا کارگزار این شخصیت بود</option>
<option value="Minister_Subject">این شخصیت وزیر یا کارگزار او بود</option>
<option value="Advisor_Other">او مشاور این شخصیت بود</option>
<option value="Advisor_Subject">این شخصیت مشاور او بود</option>
<option value="Courtier_Other">او از درباریان/همراهان این شخصیت بود</option>
<option value="Courtier_Subject">این شخصیت از درباریان/همراهان او بود</option>
<option value="Servant_Other">او خدمتگزار این شخصیت بود</option>
<option value="Servant_Subject">این شخصیت خدمتگزار او بود</option>
<option value="Patron_Other">او از این شخصیت حمایت می‌کرد (حامی او بود)</option>
<option value="Patron_Subject">این شخصیت از او حمایت می‌کرد (حامی او بود)</option>
<option value="Successor_Other">او جانشین این شخصیت شد (پس از او آمد)</option>
<option value="Successor_Subject">این شخصیت جانشین او شد (پس از او آمد)</option>
<option value="Ally">با او متحد/هم‌پیمان بود</option>
<option value="Rival">با او رقیب/دشمن بود</option>
<option value="Companion">با او همراه/همدم بود</option>
<option value="Other">نوع دیگر (در یادداشت توضیح دهید)</option>
</select>
</td>
</tr>
</table>
<table>
<tr> <tr>
<td>یادداشت (مستندسازی):</td> <td>یادداشت (مستندسازی):</td>
<td><textarea asp-for="Note" rows="3" cols="60"></textarea></td> <td><textarea asp-for="Note" rows="3" cols="60"></textarea></td>
@ -77,4 +118,21 @@ else
<a role="button" href="javascript:void(0)" onclick="history.back()" class="up-btn up-btn--ghost">انصراف</a> <a role="button" href="javascript:void(0)" onclick="history.back()" class="up-btn up-btn--ghost">انصراف</a>
</form> </form>
</div> </div>
<script>
(function () {
var groupSelect = document.getElementById('RelationKindGroup');
var familyFields = document.getElementById('family-relation-fields');
var affiliationFields = document.getElementById('affiliation-fields');
function applyKindGroup() {
var isAffiliation = groupSelect.value === 'affiliation';
familyFields.style.display = isAffiliation ? 'none' : '';
affiliationFields.style.display = isAffiliation ? '' : 'none';
}
groupSelect.addEventListener('change', applyKindGroup);
applyKindGroup();
})();
</script>
} }

View File

@ -14,14 +14,17 @@ using RMuseum.Models.Ganjoor;
namespace GanjooRazor.Areas.User.Pages namespace GanjooRazor.Areas.User.Pages
{ {
/// <summary> /// <summary>
/// contributor-facing form for suggesting a brand new kinship edge between the subject person /// contributor-facing form for suggesting a brand new edge between the subject person and
/// and another already-approved person - the "Add" counterpart of SuggestPersonRelationEdit.cshtml /// another already-approved person - the "Add" counterpart of SuggestPersonRelationEdit.cshtml
/// (which only handles Modify/Remove of an already-existing edge). Presents a direction-aware /// (which only handles Modify/Remove of an already-existing edge). Covers both kinds of edge:
/// "RelationKind" choice (e.g. "فرزند" vs "پدر یا مادر") from the subject's point of view, and /// a family/kinship tie (GanjoorPersonRelation, via RelationKind) and a non-family affiliation
/// translates it into the correct Person1Id/Person2Id/RelationType triple before submitting - /// tie (GanjoorPersonAffiliation, via AffiliationKind) - RelationKindGroup picks which. Both
/// GanjoorPersonRelation itself has no notion of "from the subject's point of view", only /// present a direction-aware choice from the subject's point of view (e.g. "فرزند" vs "پدر یا
/// Person1/Person2, so that translation has to happen somewhere, and doing it here keeps the /// مادر", or "او وزیر این شخصیت بود" vs "این شخصیت وزیر او بود") and translate it into the
/// underlying suggestion/moderation code simple and symmetric. /// symmetric Person1Id/Person2Id/type triple the underlying entity actually stores - neither
/// GanjoorPersonRelation nor GanjoorPersonAffiliation has any notion of "from the subject's own
/// point of view", only Person1/Person2, so that translation has to happen somewhere, and doing
/// it here keeps the underlying suggestion/moderation code simple and symmetric.
/// </summary> /// </summary>
public class SuggestNewPersonRelationModel : LoginPartialEnabledPageModel public class SuggestNewPersonRelationModel : LoginPartialEnabledPageModel
{ {
@ -40,9 +43,23 @@ namespace GanjooRazor.Areas.User.Pages
[BindProperty] [BindProperty]
public int OtherPersonId { get; set; } public int OtherPersonId { get; set; }
/// <summary>
/// "family" (default) or "affiliation" - which of RelationKind/AffiliationKind below applies
/// </summary>
[BindProperty]
public string RelationKindGroup { get; set; }
[BindProperty] [BindProperty]
public string RelationKind { get; set; } public string RelationKind { get; set; }
/// <summary>
/// combined type+direction key for a non-family tie, e.g. "Minister_Subject" meaning
/// "this person served as minister to the other person" - see the switch in
/// OnPostSuggestAsync for the full list and their direction handling
/// </summary>
[BindProperty]
public string AffiliationKind { get; set; }
[BindProperty] [BindProperty]
public int? DegreeHint { get; set; } public int? DegreeHint { get; set; }
@ -112,12 +129,10 @@ namespace GanjooRazor.Areas.User.Pages
if (OtherPersonId == 0 || OtherPersonId == personId) if (OtherPersonId == 0 || OtherPersonId == personId)
{ {
LastResult = "لطفاً خویشاوند مورد نظر را انتخاب کنید."; LastResult = "لطفاً خویشاوند یا شخصیت مورد نظر را انتخاب کنید.";
return Page(); return Page();
} }
// translate the direction-aware choice (from Person's own point of view) into the
// symmetric Person1Id/Person2Id/RelationType triple GanjoorPersonRelation actually stores
var suggestion = new GanjoorPersonRelationEditSuggestion() var suggestion = new GanjoorPersonRelationEditSuggestion()
{ {
Action = PersonRelationSuggestionAction.Add, Action = PersonRelationSuggestionAction.Add,
@ -126,41 +141,142 @@ namespace GanjooRazor.Areas.User.Pages
SuggestionNote = string.IsNullOrWhiteSpace(SuggestionNote) ? null : SuggestionNote.Trim(), SuggestionNote = string.IsNullOrWhiteSpace(SuggestionNote) ? null : SuggestionNote.Trim(),
}; };
switch (RelationKind) if (RelationKindGroup == "affiliation")
{ {
case "Child": // OtherPerson is a child of Person suggestion.Kind = PersonRelationSuggestionKind.Affiliation;
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId; // OtherIsPerson1/SubjectIsPerson1 pairs translate the direction-aware choice into the
suggestion.SuggestedRelationType = PersonRelationType.Parent; // symmetric Person1Id/Person2Id pair, following the direction each PersonAffiliationType
break; // value's own doc comment defines (Minister/Advisor/Courtier/Servant: Person1 is the
case "Parent": // OtherPerson is a parent of Person // subordinate one; Patron/Successor: Person1 is the patron/later one; Ally/Rival/
suggestion.Person1Id = OtherPersonId; // Companion/Other: symmetric, direction doesn't matter)
suggestion.Person2Id = personId; switch (AffiliationKind)
suggestion.SuggestedRelationType = PersonRelationType.Parent; {
break; case "Minister_Other": // the other person served as minister to this one
case "Sibling": suggestion.Person1Id = OtherPersonId;
suggestion.Person1Id = personId; suggestion.Person2Id = personId;
suggestion.Person2Id = OtherPersonId; suggestion.SuggestedAffiliationType = PersonAffiliationType.Minister;
suggestion.SuggestedRelationType = PersonRelationType.Sibling; break;
break; case "Minister_Subject": // this person served as minister to the other one
case "Spouse": suggestion.Person1Id = personId;
suggestion.Person1Id = personId; suggestion.Person2Id = OtherPersonId;
suggestion.Person2Id = OtherPersonId; suggestion.SuggestedAffiliationType = PersonAffiliationType.Minister;
suggestion.SuggestedRelationType = PersonRelationType.Spouse; break;
break; case "Advisor_Other":
case "DistantAncestor": // OtherPerson is a distant ancestor of Person suggestion.Person1Id = OtherPersonId;
suggestion.Person1Id = OtherPersonId; suggestion.Person2Id = personId;
suggestion.Person2Id = personId; suggestion.SuggestedAffiliationType = PersonAffiliationType.Advisor;
suggestion.SuggestedRelationType = PersonRelationType.Ancestor; break;
break; case "Advisor_Subject":
case "DistantDescendant": // OtherPerson is a distant descendant of Person suggestion.Person1Id = personId;
suggestion.Person1Id = personId; suggestion.Person2Id = OtherPersonId;
suggestion.Person2Id = OtherPersonId; suggestion.SuggestedAffiliationType = PersonAffiliationType.Advisor;
suggestion.SuggestedRelationType = PersonRelationType.Ancestor; break;
break; case "Courtier_Other":
default: suggestion.Person1Id = OtherPersonId;
LastResult = "نوع نسبت نامعتبر است."; suggestion.Person2Id = personId;
return Page(); suggestion.SuggestedAffiliationType = PersonAffiliationType.Courtier;
break;
case "Courtier_Subject":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Courtier;
break;
case "Servant_Other":
suggestion.Person1Id = OtherPersonId;
suggestion.Person2Id = personId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Servant;
break;
case "Servant_Subject":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Servant;
break;
case "Patron_Other": // the other person was patron of this one
suggestion.Person1Id = OtherPersonId;
suggestion.Person2Id = personId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Patron;
break;
case "Patron_Subject": // this person was patron of the other one
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Patron;
break;
case "Successor_Other": // the other person succeeded this one
suggestion.Person1Id = OtherPersonId;
suggestion.Person2Id = personId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Successor;
break;
case "Successor_Subject": // this person succeeded the other one
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Successor;
break;
case "Ally":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Ally;
break;
case "Rival":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Rival;
break;
case "Companion":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Companion;
break;
case "Other":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedAffiliationType = PersonAffiliationType.Other;
break;
default:
LastResult = "نوع وابستگی نامعتبر است.";
return Page();
}
}
else
{
suggestion.Kind = PersonRelationSuggestionKind.Family;
switch (RelationKind)
{
case "Child": // OtherPerson is a child of Person
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedRelationType = PersonRelationType.Parent;
break;
case "Parent": // OtherPerson is a parent of Person
suggestion.Person1Id = OtherPersonId;
suggestion.Person2Id = personId;
suggestion.SuggestedRelationType = PersonRelationType.Parent;
break;
case "Sibling":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedRelationType = PersonRelationType.Sibling;
break;
case "Spouse":
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedRelationType = PersonRelationType.Spouse;
break;
case "DistantAncestor": // OtherPerson is a distant ancestor of Person
suggestion.Person1Id = OtherPersonId;
suggestion.Person2Id = personId;
suggestion.SuggestedRelationType = PersonRelationType.Ancestor;
break;
case "DistantDescendant": // OtherPerson is a distant descendant of Person
suggestion.Person1Id = personId;
suggestion.Person2Id = OtherPersonId;
suggestion.SuggestedRelationType = PersonRelationType.Ancestor;
break;
default:
LastResult = "نوع نسبت نامعتبر است.";
return Page();
}
} }
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response))) using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))

View File

@ -30,6 +30,17 @@ else
<form method="post" action="/User/SuggestPersonEdit?id=@Model.Person.Id&amp;Handler=SuggestEdit"> <form method="post" action="/User/SuggestPersonEdit?id=@Model.Person.Id&amp;Handler=SuggestEdit">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" asp-for="Suggestion.PersonId" /> <input type="hidden" asp-for="Suggestion.PersonId" />
@* birth/death year, "date is documented" and birth/death location have no UI here for now
(no real usage of them elsewhere yet) - carried through as hidden fields instead of
dropped outright, so submitting this form never blanks out whatever the person record
already has for these on approval (GanjoorRelatedPersonService applies every Suggested*
field unconditionally, with no "did they actually change this" check). *@
<input type="hidden" asp-for="Suggestion.SuggestedBirthYearInLHijri" />
<input type="hidden" asp-for="Suggestion.SuggestedValidBirthDate" />
<input type="hidden" asp-for="Suggestion.SuggestedBirthLocationId" />
<input type="hidden" asp-for="Suggestion.SuggestedDeathYearInLHijri" />
<input type="hidden" asp-for="Suggestion.SuggestedValidDeathDate" />
<input type="hidden" asp-for="Suggestion.SuggestedDeathLocationId" />
<table> <table>
<tr> <tr>
@ -47,44 +58,6 @@ else
<td>نشانی ویکی‌پدیا:</td> <td>نشانی ویکی‌پدیا:</td>
<td><input style="text-align:left;direction:ltr;" asp-for="Suggestion.SuggestedWikiUrl" size="60" /></td> <td><input style="text-align:left;direction:ltr;" asp-for="Suggestion.SuggestedWikiUrl" size="60" /></td>
</tr> </tr>
<tr>
<td>زاده:</td>
<td><input asp-for="Suggestion.SuggestedBirthYearInLHijri" /> (قمری)</td>
</tr>
<tr>
<td>تاریخ تولد مستند است:</td>
<td><input asp-for="Suggestion.SuggestedValidBirthDate" /></td>
</tr>
<tr>
<td>زادگاه:</td>
<td>
<select asp-for="Suggestion.SuggestedBirthLocationId">
@foreach (var location in Model.Locations)
{
<option value="@(location.Id == 0 ? "" : location.Id.ToString())">@location.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>درگذشته:</td>
<td><input asp-for="Suggestion.SuggestedDeathYearInLHijri" /> (قمری)</td>
</tr>
<tr>
<td>تاریخ وفات مستند است:</td>
<td><input asp-for="Suggestion.SuggestedValidDeathDate" /></td>
</tr>
<tr>
<td>آرامگاه:</td>
<td>
<select asp-for="Suggestion.SuggestedDeathLocationId">
@foreach (var location in Model.Locations)
{
<option value="@(location.Id == 0 ? "" : location.Id.ToString())">@location.Name</option>
}
</select>
</td>
</tr>
<tr> <tr>
<td> <td>
عنوان شجره‌نامه: عنوان شجره‌نامه:

View File

@ -177,6 +177,14 @@ namespace RMuseum.DbContext
.HasForeignKey(s => s.ExistingRelationId) .HasForeignKey(s => s.ExistingRelationId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
// same Restrict treatment as ExistingRelationId above, but for Kind == Affiliation
// suggestions targeting a GanjoorPersonAffiliation instead
builder.Entity<GanjoorPersonRelationEditSuggestion>()
.HasOne(s => s.ExistingAffiliation)
.WithMany()
.HasForeignKey(s => s.ExistingAffiliationId)
.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 });

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RMuseum.Migrations
{
/// <inheritdoc />
public partial class AddPersonRelationSuggestionAffiliationSupport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "Kind",
table: "GanjoorPersonRelationEditSuggestions",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "SuggestedAffiliationType",
table: "GanjoorPersonRelationEditSuggestions",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions",
column: "ExistingAffiliationId");
migrationBuilder.AddForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_GanjoorPersonAffiliations_ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions",
column: "ExistingAffiliationId",
principalTable: "GanjoorPersonAffiliations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_GanjoorPersonAffiliations_ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions");
migrationBuilder.DropIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions");
migrationBuilder.DropColumn(
name: "ExistingAffiliationId",
table: "GanjoorPersonRelationEditSuggestions");
migrationBuilder.DropColumn(
name: "Kind",
table: "GanjoorPersonRelationEditSuggestions");
migrationBuilder.DropColumn(
name: "SuggestedAffiliationType",
table: "GanjoorPersonRelationEditSuggestions");
}
}
}

View File

@ -1557,9 +1557,15 @@ namespace RMuseum.Migrations
b.Property<DateTime>("Date") b.Property<DateTime>("Date")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<int?>("ExistingAffiliationId")
.HasColumnType("int");
b.Property<int?>("ExistingRelationId") b.Property<int?>("ExistingRelationId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Kind")
.HasColumnType("int");
b.Property<int>("Person1Id") b.Property<int>("Person1Id")
.HasColumnType("int"); .HasColumnType("int");
@ -1581,6 +1587,9 @@ namespace RMuseum.Migrations
b.Property<Guid?>("ReviewerUserId") b.Property<Guid?>("ReviewerUserId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<int?>("SuggestedAffiliationType")
.HasColumnType("int");
b.Property<int?>("SuggestedDegreeHint") b.Property<int?>("SuggestedDegreeHint")
.HasColumnType("int"); .HasColumnType("int");
@ -1598,6 +1607,8 @@ namespace RMuseum.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ExistingAffiliationId");
b.HasIndex("ExistingRelationId"); b.HasIndex("ExistingRelationId");
b.HasIndex("Person1Id"); b.HasIndex("Person1Id");
@ -5514,6 +5525,11 @@ namespace RMuseum.Migrations
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion", b => modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion", b =>
{ {
b.HasOne("RMuseum.Models.Ganjoor.GanjoorPersonAffiliation", "ExistingAffiliation")
.WithMany()
.HasForeignKey("ExistingAffiliationId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("RMuseum.Models.Ganjoor.GanjoorPersonRelation", "ExistingRelation") b.HasOne("RMuseum.Models.Ganjoor.GanjoorPersonRelation", "ExistingRelation")
.WithMany() .WithMany()
.HasForeignKey("ExistingRelationId") .HasForeignKey("ExistingRelationId")
@ -5541,6 +5557,8 @@ namespace RMuseum.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ExistingAffiliation");
b.Navigation("ExistingRelation"); b.Navigation("ExistingRelation");
b.Navigation("Person1"); b.Navigation("Person1");

View File

@ -23,17 +23,39 @@ namespace RMuseum.Models.Ganjoor
public PersonRelationSuggestionAction Action { get; set; } public PersonRelationSuggestionAction Action { get; set; }
/// <summary> /// <summary>
/// for Modify/Remove: the existing GanjoorPersonRelation row this suggestion targets. Null /// whether this suggestion is about a kinship edge (Family, using SuggestedRelationType /
/// for Add, where there's nothing existing yet. /// ExistingRelationId / ExistingRelation) or a non-family affiliation edge (Affiliation, using
/// SuggestedAffiliationType / ExistingAffiliationId / ExistingAffiliation instead). Defaults
/// to Family so rows created before this field existed keep working unchanged.
/// </summary>
public PersonRelationSuggestionKind Kind { get; set; }
/// <summary>
/// for Modify/Remove when Kind is Family: the existing GanjoorPersonRelation row this
/// suggestion targets. Null for Add, where there's nothing existing yet, and for any
/// Kind == Affiliation suggestion (see ExistingAffiliationId instead).
/// </summary> /// </summary>
public int? ExistingRelationId { get; set; } public int? ExistingRelationId { get; set; }
/// <summary> /// <summary>
/// the existing relation being modified/removed (for showing a before/after diff to the /// the existing relation being modified/removed (for showing a before/after diff to the
/// moderator) - null for Add /// moderator) - null for Add, and for Kind == Affiliation
/// </summary> /// </summary>
public virtual GanjoorPersonRelation ExistingRelation { get; set; } public virtual GanjoorPersonRelation ExistingRelation { get; set; }
/// <summary>
/// for Modify/Remove when Kind is Affiliation: the existing GanjoorPersonAffiliation row this
/// suggestion targets. Null for Add, and for any Kind == Family suggestion (see
/// ExistingRelationId instead).
/// </summary>
public int? ExistingAffiliationId { get; set; }
/// <summary>
/// the existing affiliation being modified/removed (for showing a before/after diff to the
/// moderator) - null unless Kind is Affiliation and Action is Modify/Remove
/// </summary>
public virtual GanjoorPersonAffiliation ExistingAffiliation { get; set; }
/// <summary> /// <summary>
/// the pair this suggestion is about. For Add, this and Person2Id define the new relation. /// the pair this suggestion is about. For Add, this and Person2Id define the new relation.
/// For Modify/Remove, these are copied from ExistingRelation at submission time, so the /// For Modify/Remove, these are copied from ExistingRelation at submission time, so the
@ -58,11 +80,19 @@ namespace RMuseum.Models.Ganjoor
public virtual GanjoorRelatedPerson Person2 { get; set; } public virtual GanjoorRelatedPerson Person2 { get; set; }
/// <summary> /// <summary>
/// for Add/Modify: the relation type to create/change to. For Remove: a snapshot of the /// for Add/Modify when Kind is Family: the relation type to create/change to. For Remove: a
/// existing relation's type at submission time, purely for display (removing doesn't use it). /// snapshot of the existing relation's type at submission time, purely for display (removing
/// doesn't use it). Ignored when Kind is Affiliation - see SuggestedAffiliationType instead.
/// </summary> /// </summary>
public PersonRelationType SuggestedRelationType { get; set; } public PersonRelationType SuggestedRelationType { get; set; }
/// <summary>
/// for Add/Modify when Kind is Affiliation: the affiliation type to create/change to. For
/// Remove: a snapshot of the existing affiliation's type at submission time, purely for
/// display. Null/ignored when Kind is Family - see SuggestedRelationType instead.
/// </summary>
public PersonAffiliationType? SuggestedAffiliationType { get; set; }
/// <summary> /// <summary>
/// see GanjoorPersonRelation.DegreeHint - same for/Add/Modify/Remove-display purpose as /// see GanjoorPersonRelation.DegreeHint - same for/Add/Modify/Remove-display purpose as
/// SuggestedRelationType above /// SuggestedRelationType above

View File

@ -0,0 +1,16 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// which kind of edge a GanjoorPersonRelationEditSuggestion is about: a kinship tie
/// (GanjoorPersonRelation, via SuggestedRelationType/ExistingRelationId) or a non-family
/// affiliation tie (GanjoorPersonAffiliation, via SuggestedAffiliationType/ExistingAffiliationId).
/// Defaults to Family (0), so every suggestion row created before this field existed keeps being
/// read exactly as it was before - a plain kinship suggestion.
/// </summary>
public enum PersonRelationSuggestionKind
{
Family = 0,
Affiliation = 1,
}
}

View File

@ -4955,6 +4955,18 @@
<member name="M:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)"> <member name="M:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc /> <inheritdoc />
</member> </member>
<member name="T:RMuseum.Migrations.AddPersonRelationSuggestionAffiliationSupport">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonRelationSuggestionAffiliationSupport.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonRelationSuggestionAffiliationSupport.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonRelationSuggestionAffiliationSupport.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
@ -9590,16 +9602,38 @@
what this suggestion proposes doing what this suggestion proposes doing
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Kind">
<summary>
whether this suggestion is about a kinship edge (Family, using SuggestedRelationType /
ExistingRelationId / ExistingRelation) or a non-family affiliation edge (Affiliation, using
SuggestedAffiliationType / ExistingAffiliationId / ExistingAffiliation instead). Defaults
to Family so rows created before this field existed keep working unchanged.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelationId"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelationId">
<summary> <summary>
for Modify/Remove: the existing GanjoorPersonRelation row this suggestion targets. Null for Modify/Remove when Kind is Family: the existing GanjoorPersonRelation row this
for Add, where there's nothing existing yet. suggestion targets. Null for Add, where there's nothing existing yet, and for any
Kind == Affiliation suggestion (see ExistingAffiliationId instead).
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelation"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelation">
<summary> <summary>
the existing relation being modified/removed (for showing a before/after diff to the the existing relation being modified/removed (for showing a before/after diff to the
moderator) - null for Add moderator) - null for Add, and for Kind == Affiliation
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingAffiliationId">
<summary>
for Modify/Remove when Kind is Affiliation: the existing GanjoorPersonAffiliation row this
suggestion targets. Null for Add, and for any Kind == Family suggestion (see
ExistingRelationId instead).
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingAffiliation">
<summary>
the existing affiliation being modified/removed (for showing a before/after diff to the
moderator) - null unless Kind is Affiliation and Action is Modify/Remove
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person1Id"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person1Id">
@ -9627,8 +9661,16 @@
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedRelationType"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedRelationType">
<summary> <summary>
for Add/Modify: the relation type to create/change to. For Remove: a snapshot of the for Add/Modify when Kind is Family: the relation type to create/change to. For Remove: a
existing relation's type at submission time, purely for display (removing doesn't use it). snapshot of the existing relation's type at submission time, purely for display (removing
doesn't use it). Ignored when Kind is Affiliation - see SuggestedAffiliationType instead.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedAffiliationType">
<summary>
for Add/Modify when Kind is Affiliation: the affiliation type to create/change to. For
Remove: a snapshot of the existing affiliation's type at submission time, purely for
display. Null/ignored when Kind is Family - see SuggestedRelationType instead.
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedDegreeHint"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedDegreeHint">
@ -11819,6 +11861,15 @@
remove the existing relation ExistingRelationId points to outright remove the existing relation ExistingRelationId points to outright
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationSuggestionKind">
<summary>
which kind of edge a GanjoorPersonRelationEditSuggestion is about: a kinship tie
(GanjoorPersonRelation, via SuggestedRelationType/ExistingRelationId) or a non-family
affiliation tie (GanjoorPersonAffiliation, via SuggestedAffiliationType/ExistingAffiliationId).
Defaults to Family (0), so every suggestion row created before this field existed keeps being
read exactly as it was before - a plain kinship suggestion.
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationType"> <member name="T:RMuseum.Models.Ganjoor.PersonRelationType">
<summary> <summary>
the kind of kinship edge a GanjoorPersonRelation represents between two GanjoorRelatedPerson the kind of kinship edge a GanjoorPersonRelation represents between two GanjoorRelatedPerson

View File

@ -465,16 +465,18 @@ namespace RMuseum.Services.Implementation
var affiliations = await _context.GanjoorPersonAffiliations var affiliations = await _context.GanjoorPersonAffiliations
.Where(a => a.Person1Id == person.Id || a.Person2Id == person.Id) .Where(a => a.Person1Id == person.Id || a.Person2Id == person.Id)
.ToListAsync(); .ToListAsync();
var affiliationIds = affiliations.Select(a => a.Id).ToList();
// any other still-pending relation-edit suggestion that touches this person directly, or // any other still-pending relation-edit suggestion that touches this person directly, or
// targets one of the relations we're about to remove, would otherwise dangle or violate // targets one of the relations/affiliations we're about to remove, would otherwise dangle
// the Restrict FK above - auto-reject those with an explanatory note rather than letting // or violate the Restrict FKs above - auto-reject those with an explanatory note rather
// the delete fail or silently drop them // than letting the delete fail or silently drop them
var conflictingRelationSuggestions = await _context.GanjoorPersonRelationEditSuggestions var conflictingRelationSuggestions = await _context.GanjoorPersonRelationEditSuggestions
.Where(s => !s.Reviewed && ( .Where(s => !s.Reviewed && (
s.Person1Id == person.Id || s.Person1Id == person.Id ||
s.Person2Id == person.Id || s.Person2Id == person.Id ||
(s.ExistingRelationId != null && relationIds.Contains(s.ExistingRelationId.Value)) (s.ExistingRelationId != null && relationIds.Contains(s.ExistingRelationId.Value)) ||
(s.ExistingAffiliationId != null && affiliationIds.Contains(s.ExistingAffiliationId.Value))
)) ))
.ToListAsync(); .ToListAsync();
foreach (var conflicting in conflictingRelationSuggestions) foreach (var conflicting in conflictingRelationSuggestions)
@ -553,34 +555,65 @@ namespace RMuseum.Services.Implementation
} }
GanjoorPersonRelation existingRelation = null; GanjoorPersonRelation existingRelation = null;
GanjoorPersonAffiliation existingAffiliation = null;
if (suggestion.Action == PersonRelationSuggestionAction.Modify || suggestion.Action == PersonRelationSuggestionAction.Remove) if (suggestion.Action == PersonRelationSuggestionAction.Modify || suggestion.Action == PersonRelationSuggestionAction.Remove)
{ {
if (suggestion.ExistingRelationId == null) if (suggestion.Kind == PersonRelationSuggestionKind.Affiliation)
{ {
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر برای ویرایش یا حذف مشخص نشده است."); if (suggestion.ExistingAffiliationId == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "وابستگی مورد نظر برای ویرایش یا حذف مشخص نشده است.");
}
existingAffiliation = await _context.GanjoorPersonAffiliations
.Include(a => a.Person1)
.Include(a => a.Person2)
.Where(a => a.Id == suggestion.ExistingAffiliationId.Value)
.SingleOrDefaultAsync();
if (existingAffiliation == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "وابستگی مورد نظر پیدا نشد.");
}
// same trust-the-existing-row convention as the Family branch below
suggestion.Person1Id = existingAffiliation.Person1Id;
suggestion.Person2Id = existingAffiliation.Person2Id;
if (suggestion.Action == PersonRelationSuggestionAction.Remove)
{
suggestion.SuggestedAffiliationType = existingAffiliation.AffiliationType;
suggestion.SuggestedNote = existingAffiliation.Note;
}
} }
else
existingRelation = await _context.GanjoorPersonRelations
.Include(r => r.Person1)
.Include(r => r.Person2)
.Where(r => r.Id == suggestion.ExistingRelationId.Value)
.SingleOrDefaultAsync();
if (existingRelation == null)
{ {
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر پیدا نشد."); if (suggestion.ExistingRelationId == null)
} {
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر برای ویرایش یا حذف مشخص نشده است.");
}
// always trust the existing relation's own Person1Id/Person2Id/type/etc over existingRelation = await _context.GanjoorPersonRelations
// whatever the client sent, so the suggestion is guaranteed self-consistent with .Include(r => r.Person1)
// what it actually targets, even for a Remove-display .Include(r => r.Person2)
suggestion.Person1Id = existingRelation.Person1Id; .Where(r => r.Id == suggestion.ExistingRelationId.Value)
suggestion.Person2Id = existingRelation.Person2Id; .SingleOrDefaultAsync();
if (suggestion.Action == PersonRelationSuggestionAction.Remove)
{ if (existingRelation == null)
suggestion.SuggestedRelationType = existingRelation.RelationType; {
suggestion.SuggestedDegreeHint = existingRelation.DegreeHint; return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر پیدا نشد.");
suggestion.SuggestedNote = existingRelation.Note; }
// always trust the existing relation's own Person1Id/Person2Id/type/etc over
// whatever the client sent, so the suggestion is guaranteed self-consistent with
// what it actually targets, even for a Remove-display
suggestion.Person1Id = existingRelation.Person1Id;
suggestion.Person2Id = existingRelation.Person2Id;
if (suggestion.Action == PersonRelationSuggestionAction.Remove)
{
suggestion.SuggestedRelationType = existingRelation.RelationType;
suggestion.SuggestedDegreeHint = existingRelation.DegreeHint;
suggestion.SuggestedNote = existingRelation.Note;
}
} }
} }
else // Add else // Add
@ -600,6 +633,7 @@ namespace RMuseum.Services.Implementation
suggestion.Id = 0; suggestion.Id = 0;
suggestion.ExistingRelation = null; suggestion.ExistingRelation = null;
suggestion.ExistingAffiliation = null;
suggestion.Person1 = null; suggestion.Person1 = null;
suggestion.Person2 = null; suggestion.Person2 = null;
suggestion.Date = DateTime.Now; suggestion.Date = DateTime.Now;
@ -613,17 +647,19 @@ namespace RMuseum.Services.Implementation
_context.GanjoorPersonRelationEditSuggestions.Add(suggestion); _context.GanjoorPersonRelationEditSuggestions.Add(suggestion);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
bool isAffiliation = suggestion.Kind == PersonRelationSuggestionKind.Affiliation;
string actionTitle = suggestion.Action switch string actionTitle = suggestion.Action switch
{ {
PersonRelationSuggestionAction.Add => "پیشنهاد نسبت خویشاوندی جدید", PersonRelationSuggestionAction.Add => isAffiliation ? "پیشنهاد وابستگی جدید" : "پیشنهاد نسبت خویشاوندی جدید",
PersonRelationSuggestionAction.Modify => "پیشنهاد ویرایش نسبت خویشاوندی", PersonRelationSuggestionAction.Modify => isAffiliation ? "پیشنهاد ویرایش وابستگی" : "پیشنهاد ویرایش نسبت خویشاوندی",
_ => "پیشنهاد حذف نسبت خویشاوندی", _ => isAffiliation ? "پیشنهاد حذف وابستگی" : "پیشنهاد حذف نسبت خویشاوندی",
}; };
string edgeLabel = isAffiliation ? "وابستگی" : "نسبت خویشاوندی";
string actionText = suggestion.Action switch string actionText = suggestion.Action switch
{ {
PersonRelationSuggestionAction.Add => $"کاربری پیشنهاد افزودن نسبت خویشاوندی جدید بین «{person1.Name}» و «{person2.Name}» را داده است.", PersonRelationSuggestionAction.Add => $"کاربری پیشنهاد افزودن {edgeLabel} جدید بین «{person1.Name}» و «{person2.Name}» را داده است.",
PersonRelationSuggestionAction.Modify => $"کاربری پیشنهاد ویرایش نسبت خویشاوندی بین «{person1.Name}» و «{person2.Name}» را داده است.", PersonRelationSuggestionAction.Modify => $"کاربری پیشنهاد ویرایش {edgeLabel} بین «{person1.Name}» و «{person2.Name}» را داده است.",
_ => $"کاربری پیشنهاد حذف نسبت خویشاوندی بین «{person1.Name}» و «{person2.Name}» را داده است.", _ => $"کاربری پیشنهاد حذف {edgeLabel} بین «{person1.Name}» و «{person2.Name}» را داده است.",
}; };
await NotifyModeratorsOfPendingSuggestionAsync( await NotifyModeratorsOfPendingSuggestionAsync(
@ -652,6 +688,7 @@ namespace RMuseum.Services.Implementation
.Include(s => s.Person1) .Include(s => s.Person1)
.Include(s => s.Person2) .Include(s => s.Person2)
.Include(s => s.ExistingRelation) .Include(s => s.ExistingRelation)
.Include(s => s.ExistingAffiliation)
.Include(s => s.User) .Include(s => s.User)
.Where(s => s.Reviewed == false) .Where(s => s.Reviewed == false)
.OrderBy(s => s.Id) .OrderBy(s => s.Id)
@ -715,51 +752,101 @@ namespace RMuseum.Services.Implementation
if (result == CorrectionReviewResult.Approved) if (result == CorrectionReviewResult.Approved)
{ {
switch (suggestion.Action) if (suggestion.Kind == PersonRelationSuggestionKind.Affiliation)
{ {
case PersonRelationSuggestionAction.Add: switch (suggestion.Action)
_context.GanjoorPersonRelations.Add(new GanjoorPersonRelation() {
{ case PersonRelationSuggestionAction.Add:
Person1Id = suggestion.Person1Id, _context.GanjoorPersonAffiliations.Add(new GanjoorPersonAffiliation()
Person2Id = suggestion.Person2Id,
RelationType = suggestion.SuggestedRelationType,
DegreeHint = suggestion.SuggestedDegreeHint,
Note = suggestion.SuggestedNote,
});
break;
case PersonRelationSuggestionAction.Modify:
{
var existing = await _context.GanjoorPersonRelations.Where(r => r.Id == suggestion.ExistingRelationId.Value).SingleOrDefaultAsync();
if (existing == null)
{ {
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر دیگر وجود ندارد."); Person1Id = suggestion.Person1Id,
} Person2Id = suggestion.Person2Id,
existing.RelationType = suggestion.SuggestedRelationType; AffiliationType = suggestion.SuggestedAffiliationType ?? PersonAffiliationType.Other,
existing.DegreeHint = suggestion.SuggestedDegreeHint; Note = suggestion.SuggestedNote,
existing.Note = suggestion.SuggestedNote; });
break; break;
} case PersonRelationSuggestionAction.Modify:
case PersonRelationSuggestionAction.Remove:
{
var existing = await _context.GanjoorPersonRelations.Where(r => r.Id == suggestion.ExistingRelationId.Value).SingleOrDefaultAsync();
if (existing != null)
{ {
// any other still-pending suggestion targeting this same relation var existing = await _context.GanjoorPersonAffiliations.Where(a => a.Id == suggestion.ExistingAffiliationId.Value).SingleOrDefaultAsync();
// would otherwise dangle once it's gone - auto-reject those too if (existing == null)
var conflicting = await _context.GanjoorPersonRelationEditSuggestions
.Where(s => !s.Reviewed && s.Id != suggestion.Id && s.ExistingRelationId == existing.Id)
.ToListAsync();
foreach (var c in conflicting)
{ {
c.Reviewed = true; return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "وابستگی مورد نظر دیگر وجود ندارد.");
c.Result = CorrectionReviewResult.RejectedBecauseUnnecessaryChange;
c.ReviewNote = "این نسبت پیش‌تر حذف شد.";
c.ReviewDate = DateTime.Now;
} }
_context.GanjoorPersonRelations.Remove(existing); existing.AffiliationType = suggestion.SuggestedAffiliationType ?? existing.AffiliationType;
existing.Note = suggestion.SuggestedNote;
break;
} }
case PersonRelationSuggestionAction.Remove:
{
var existing = await _context.GanjoorPersonAffiliations.Where(a => a.Id == suggestion.ExistingAffiliationId.Value).SingleOrDefaultAsync();
if (existing != null)
{
// any other still-pending suggestion targeting this same affiliation
// would otherwise dangle once it's gone - auto-reject those too
var conflicting = await _context.GanjoorPersonRelationEditSuggestions
.Where(s => !s.Reviewed && s.Id != suggestion.Id && s.ExistingAffiliationId == existing.Id)
.ToListAsync();
foreach (var c in conflicting)
{
c.Reviewed = true;
c.Result = CorrectionReviewResult.RejectedBecauseUnnecessaryChange;
c.ReviewNote = "این وابستگی پیش‌تر حذف شد.";
c.ReviewDate = DateTime.Now;
}
_context.GanjoorPersonAffiliations.Remove(existing);
}
break;
}
}
}
else
{
switch (suggestion.Action)
{
case PersonRelationSuggestionAction.Add:
_context.GanjoorPersonRelations.Add(new GanjoorPersonRelation()
{
Person1Id = suggestion.Person1Id,
Person2Id = suggestion.Person2Id,
RelationType = suggestion.SuggestedRelationType,
DegreeHint = suggestion.SuggestedDegreeHint,
Note = suggestion.SuggestedNote,
});
break; break;
} case PersonRelationSuggestionAction.Modify:
{
var existing = await _context.GanjoorPersonRelations.Where(r => r.Id == suggestion.ExistingRelationId.Value).SingleOrDefaultAsync();
if (existing == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر دیگر وجود ندارد.");
}
existing.RelationType = suggestion.SuggestedRelationType;
existing.DegreeHint = suggestion.SuggestedDegreeHint;
existing.Note = suggestion.SuggestedNote;
break;
}
case PersonRelationSuggestionAction.Remove:
{
var existing = await _context.GanjoorPersonRelations.Where(r => r.Id == suggestion.ExistingRelationId.Value).SingleOrDefaultAsync();
if (existing != null)
{
// any other still-pending suggestion targeting this same relation
// would otherwise dangle once it's gone - auto-reject those too
var conflicting = await _context.GanjoorPersonRelationEditSuggestions
.Where(s => !s.Reviewed && s.Id != suggestion.Id && s.ExistingRelationId == existing.Id)
.ToListAsync();
foreach (var c in conflicting)
{
c.Reviewed = true;
c.Result = CorrectionReviewResult.RejectedBecauseUnnecessaryChange;
c.ReviewNote = "این نسبت پیش‌تر حذف شد.";
c.ReviewDate = DateTime.Now;
}
_context.GanjoorPersonRelations.Remove(existing);
}
break;
}
}
} }
} }
@ -771,11 +858,12 @@ namespace RMuseum.Services.Implementation
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
string edgeLabel = suggestion.Kind == PersonRelationSuggestionKind.Affiliation ? "وابستگی" : "نسبت خویشاوندی";
string actionLabel = suggestion.Action switch string actionLabel = suggestion.Action switch
{ {
PersonRelationSuggestionAction.Add => "افزودن نسبت خویشاوندی", PersonRelationSuggestionAction.Add => $"افزودن {edgeLabel}",
PersonRelationSuggestionAction.Modify => "ویرایش نسبت خویشاوندی", PersonRelationSuggestionAction.Modify => $"ویرایش {edgeLabel}",
_ => "حذف نسبت خویشاوندی", _ => $"حذف {edgeLabel}",
}; };
if (result == CorrectionReviewResult.Approved) if (result == CorrectionReviewResult.Approved)