people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-29 19:32:57 +03:30
parent 9d12c23400
commit bb68c074b0
23 changed files with 8908 additions and 82 deletions

View File

@ -45,67 +45,77 @@ else
}
</table>
<table class="up-table">
<thead>
<tr>
<th>فیلد</th>
<th>مقدار فعلی</th>
<th>پیشنهاد</th>
</tr>
</thead>
<tbody>
<tr>
<td>نام</td>
<td>@p.Name</td>
<td>@s.SuggestedName</td>
</tr>
<tr>
<td>نشانی ویکی‌پدیا</td>
<td>@p.WikiUrl</td>
<td>@s.SuggestedWikiUrl</td>
</tr>
<tr>
<td>سال تولد (قمری)</td>
<td>@(p.BirthYearInLHijri == 0 ? "" : p.BirthYearInLHijri.ToPersianNumbers())</td>
<td>@(s.SuggestedBirthYearInLHijri == 0 ? "" : s.SuggestedBirthYearInLHijri.ToPersianNumbers())</td>
</tr>
<tr>
<td>تاریخ تولد مستند است</td>
<td>@(p.ValidBirthDate ? "بله" : "خیر")</td>
<td>@(s.SuggestedValidBirthDate ? "بله" : "خیر")</td>
</tr>
<tr>
<td>زادگاه</td>
<td>@Model.LocationName(p.BirthLocationId)</td>
<td>@(s.SuggestedBirthLocation != null ? s.SuggestedBirthLocation.Name : "")</td>
</tr>
<tr>
<td>سال وفات (قمری)</td>
<td>@(p.DeathYearInLHijri == 0 ? "" : p.DeathYearInLHijri.ToPersianNumbers())</td>
<td>@(s.SuggestedDeathYearInLHijri == 0 ? "" : s.SuggestedDeathYearInLHijri.ToPersianNumbers())</td>
</tr>
<tr>
<td>تاریخ وفات مستند است</td>
<td>@(p.ValidDeathDate ? "بله" : "خیر")</td>
<td>@(s.SuggestedValidDeathDate ? "بله" : "خیر")</td>
</tr>
<tr>
<td>آرامگاه</td>
<td>@Model.LocationName(p.DeathLocationId)</td>
<td>@(s.SuggestedDeathLocation != null ? s.SuggestedDeathLocation.Name : "")</td>
</tr>
<tr>
<td>عنوان شجره‌نامه</td>
<td>@p.FamilyTreeCaption</td>
<td>@s.SuggestedFamilyTreeCaption</td>
</tr>
<tr>
<td>توضیحات</td>
<td><small>@p.Description</small></td>
<td><small>@s.SuggestedDescription</small></td>
</tr>
</tbody>
</table>
@if (s.SuggestedForDeletion)
{
<div class="up-alert up-alert--danger">
<strong>این پیشنهاد درخواست حذف کامل شخصیت «@p.Name» است.</strong>
در صورت تأیید، این شخصیت و همهٔ نسبت‌های خویشاوندی/وابستگی‌های آن حذف خواهد شد و هرگونه پیشنهاد در حال بررسی مرتبط با آن‌ها به طور خودکار رد می‌شود. فیلدهای زیر در این حالت نادیده گرفته می‌شوند.
</div>
}
else
{
<table class="up-table">
<thead>
<tr>
<th>فیلد</th>
<th>مقدار فعلی</th>
<th>پیشنهاد</th>
</tr>
</thead>
<tbody>
<tr>
<td>نام</td>
<td>@p.Name</td>
<td>@s.SuggestedName</td>
</tr>
<tr>
<td>نشانی ویکی‌پدیا</td>
<td>@p.WikiUrl</td>
<td>@s.SuggestedWikiUrl</td>
</tr>
<tr>
<td>سال تولد (قمری)</td>
<td>@(p.BirthYearInLHijri == 0 ? "" : p.BirthYearInLHijri.ToPersianNumbers())</td>
<td>@(s.SuggestedBirthYearInLHijri == 0 ? "" : s.SuggestedBirthYearInLHijri.ToPersianNumbers())</td>
</tr>
<tr>
<td>تاریخ تولد مستند است</td>
<td>@(p.ValidBirthDate ? "بله" : "خیر")</td>
<td>@(s.SuggestedValidBirthDate ? "بله" : "خیر")</td>
</tr>
<tr>
<td>زادگاه</td>
<td>@Model.LocationName(p.BirthLocationId)</td>
<td>@(s.SuggestedBirthLocation != null ? s.SuggestedBirthLocation.Name : "")</td>
</tr>
<tr>
<td>سال وفات (قمری)</td>
<td>@(p.DeathYearInLHijri == 0 ? "" : p.DeathYearInLHijri.ToPersianNumbers())</td>
<td>@(s.SuggestedDeathYearInLHijri == 0 ? "" : s.SuggestedDeathYearInLHijri.ToPersianNumbers())</td>
</tr>
<tr>
<td>تاریخ وفات مستند است</td>
<td>@(p.ValidDeathDate ? "بله" : "خیر")</td>
<td>@(s.SuggestedValidDeathDate ? "بله" : "خیر")</td>
</tr>
<tr>
<td>آرامگاه</td>
<td>@Model.LocationName(p.DeathLocationId)</td>
<td>@(s.SuggestedDeathLocation != null ? s.SuggestedDeathLocation.Name : "")</td>
</tr>
<tr>
<td>عنوان شجره‌نامه</td>
<td>@p.FamilyTreeCaption</td>
<td>@s.SuggestedFamilyTreeCaption</td>
</tr>
<tr>
<td>توضیحات</td>
<td><small>@p.Description</small></td>
<td><small>@s.SuggestedDescription</small></td>
</tr>
</tbody>
</table>
}
<div class="spacer">&nbsp;</div>

View File

@ -0,0 +1,199 @@
@page
@model GanjooRazor.Areas.Admin.Pages.ReviewPersonRelationEditsModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using DNTPersianUtils.Core
@using RMuseum.Models.Ganjoor
@{
Layout = "_AdminLayout";
ViewData["Title"] = "بازبینی پیشنهادهای نسبت‌های خویشاوندی";
string RelationTypeName(PersonRelationType t)
{
switch (t)
{
case PersonRelationType.Parent: return "پدر/مادر و فرزند";
case PersonRelationType.Sibling: return "خواهر/برادر";
case PersonRelationType.Spouse: return "همسر";
case PersonRelationType.Ancestor: return "نیا و نواده";
default: return t.ToString();
}
}
string ActionName(PersonRelationSuggestionAction a)
{
switch (a)
{
case PersonRelationSuggestionAction.Add: return "افزودن نسبت جدید";
case PersonRelationSuggestionAction.Modify: return "ویرایش نسبت موجود";
case PersonRelationSuggestionAction.Remove: return "حذف نسبت موجود";
default: return a.ToString();
}
}
}
<div class="up-page-header">
<h1>@ViewData["Title"]</h1>
</div>
@if (!string.IsNullOrEmpty(Model.FatalError))
{
<div class="up-alert up-alert--danger">@Model.FatalError</div>
}
else if (Model.Suggestion == null)
{
<partial name="_EmptyState" model="@("پیشنهاد ویرایشی وجود ندارد.")" />
}
else
{
var s = Model.Suggestion;
<p>
<span class="up-badge">@ActionName(s.Action)</span>
</p>
<table>
<tr>
<td>پیشنهاد دهنده:</td>
<td>
<a href="/user/edits/?allusers=1&userid=@s.UserId" target="_blank">@(s.User != null ? s.User.NickName : s.UserId.ToString())</a>
— @s.Date.ToFriendlyPersianDateTextify()
</td>
</tr>
@if (!string.IsNullOrEmpty(s.SuggestionNote))
{
<tr>
<td>یادداشت پیشنهاددهنده:</td>
<td>@s.SuggestionNote</td>
</tr>
}
</table>
@if (s.Action == PersonRelationSuggestionAction.Remove)
{
<div class="up-alert up-alert--danger">
<strong>این پیشنهاد درخواست حذف نسبت زیر است:</strong>
</div>
}
<table class="up-table">
<thead>
<tr>
<th></th>
<th>@(s.Action == PersonRelationSuggestionAction.Remove || s.Action == PersonRelationSuggestionAction.Modify ? "مقدار فعلی" : "پیشنهاد")</th>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<th>پیشنهاد</th>
}
</tr>
</thead>
<tbody>
<tr>
<td>طرف اول</td>
<td>
<a role="button" target="_blank" href="/person/@s.Person1Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>
<a role="button" target="_blank" href="/person/@s.Person1Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
</td>
}
</tr>
<tr>
<td>طرف دوم</td>
<td>
<a role="button" target="_blank" href="/person/@s.Person2Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>
<a role="button" target="_blank" href="/person/@s.Person2Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
</td>
}
</tr>
<tr>
<td>نوع نسبت</td>
<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>
<td>یادداشت</td>
<td><small>@(s.Action == PersonRelationSuggestionAction.Modify && s.ExistingRelation != null ? s.ExistingRelation.Note : s.SuggestedNote)</small></td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td><small>@s.SuggestedNote</small></td>
}
</tr>
</tbody>
</table>
<div class="spacer">&nbsp;</div>
<table>
<tr>
<td>
<label><input type="radio" name="review_result" id="review_result_Approved" value="Approved" checked="checked" /> تأیید</label>
&nbsp;
<label><input type="radio" name="review_result" id="review_result_Rejected" value="Rejected" /> رد</label>
</td>
</tr>
<tr>
<td>
یادداشت برای پیشنهاددهنده:<br />
<textarea id="review_note" rows="3" cols="60"></textarea>
</td>
</tr>
<tr>
<td>
<a role="button" id="moderatepersonrelationedit" class="up-btn up-btn--success up-btn--block" onclick="moderatePersonRelationEditSuggestion(@s.Id)">بررسی</a>
</td>
<td>
<form method="post">
@Html.AntiForgeryToken()
<button type="submit" name="next" id="next" class="up-btn up-btn--ghost up-btn--block">بعدی</button>
</form>
</td>
</tr>
</table>
<p><small>@Model.Skip.ToPersianNumbers() از @Model.TotalCount.ToPersianNumbers()</small></p>
<script>
function moderatePersonRelationEditSuggestion(id) {
$('#moderatepersonrelationedit').hide();
var url = '?handler=Moderate';
var pms = {};
pms.id = id;
pms.result = $('input[name=review_result]:checked').val();
pms.reviewNote = $('#review_note').val();
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(pms),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function () {
location.reload(true);
},
error: function (e) {
upToast(e.responseText || 'خطا رخ داد.', 'error');
$('#moderatepersonrelationedit').show();
}
});
}
</script>
}

View File

@ -0,0 +1,155 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using GanjooRazor.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
using RSecurityBackend.Models.Generic;
namespace GanjooRazor.Areas.Admin.Pages
{
/// <summary>
/// moderator review queue for pending GanjoorPersonRelationEditSuggestion entries - counterpart
/// of ReviewPersonEdits.cshtml, but for kinship edges (Add/Modify/Remove) rather than a person's
/// own fields.
/// </summary>
public class ReviewPersonRelationEditsModel : PageModel
{
/// <summary>
/// the pending suggestion currently shown
/// </summary>
public GanjoorPersonRelationEditSuggestion Suggestion { get; set; }
/// <summary>
/// fatal error
/// </summary>
public string FatalError { get; set; }
/// <summary>
/// skip
/// </summary>
public int Skip { get; set; }
/// <summary>
/// total count
/// </summary>
public int TotalCount { get; set; }
public IActionResult OnPost()
{
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);
if (Request.Form["next"].Count == 1)
{
return Redirect($"/Admin/ReviewPersonRelationEdits/?skip={Skip + 1}");
}
return Page();
}
public async Task<IActionResult> OnGetAsync()
{
if (string.IsNullOrEmpty(Request.Cookies["Token"]))
return Redirect("/");
FatalError = "";
TotalCount = 0;
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
{
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
{
var nextResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/people/relationeditsuggestions/next?skip={Skip}");
if (!nextResponse.IsSuccessStatusCode)
{
FatalError = JsonConvert.DeserializeObject<string>(await nextResponse.Content.ReadAsStringAsync());
return Page();
}
string paginationMetadata = nextResponse.Headers.GetValues("paging-headers").FirstOrDefault();
if (!string.IsNullOrEmpty(paginationMetadata))
{
TotalCount = JsonConvert.DeserializeObject<PaginationMetadata>(paginationMetadata).totalCount;
}
Suggestion = JsonConvert.DeserializeObject<GanjoorPersonRelationEditSuggestion>(await nextResponse.Content.ReadAsStringAsync());
return Page();
}
else
{
return Redirect("/");
}
}
}
public async Task<IActionResult> OnPostModerateAsync([FromBody] PersonRelationEditSuggestionModerationRequest pms)
{
if (string.IsNullOrEmpty(Request.Cookies["Token"]))
return new BadRequestObjectResult("لطفاً از گنجور خارج و مجدداً به آن وارد شوید.");
if (pms == null || string.IsNullOrEmpty(pms.Result))
{
return new BadRequestObjectResult("لطفاً تکلیف بررسی این پیشنهاد را مشخص کنید.");
}
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
{
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
{
var moderation = new PersonEditSuggestionModerationViewModel()
{
Result = (CorrectionReviewResult)Enum.Parse(typeof(CorrectionReviewResult), pms.Result),
ReviewNote = pms.ReviewNote,
};
var moderationResponse = await secureClient.PostAsync($"{APIRoot.Url}/api/people/relationeditsuggestions/{pms.Id}/moderate",
new StringContent(JsonConvert.SerializeObject(moderation), Encoding.UTF8, "application/json"));
if (!moderationResponse.IsSuccessStatusCode)
{
string err = await moderationResponse.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(err))
{
if (!string.IsNullOrEmpty(moderationResponse.ReasonPhrase))
{
err = moderationResponse.ReasonPhrase;
}
else
{
err = $"Error Code: {moderationResponse.StatusCode}";
}
}
else
{
err = JsonConvert.DeserializeObject<string>(err);
}
return new BadRequestObjectResult(err);
}
return new OkObjectResult(true);
}
else
{
return new BadRequestObjectResult("لطفاً از گنجور خارج و مجدداً به آن وارد شوید.");
}
}
}
}
/// <summary>
/// JSON body shape posted by ReviewPersonRelationEdits.cshtml's moderatePersonRelationEditSuggestion()
/// ajax call - kept local to this page since it's only ever used to bind that one handler's
/// [FromBody] parameter
/// </summary>
public class PersonRelationEditSuggestionModerationRequest
{
public int Id { get; set; }
public string Result { get; set; }
public string ReviewNote { get; set; }
}
}

View File

@ -69,6 +69,10 @@ else
<h2>
خویشاوندان
<a href="/FamilyTree/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">نمایش نمودار شجره‌نامه</a>
@if (Model.LoggedIn)
{
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
}
</h2>
<ul>
@foreach (var row in Model.RelationRows)
@ -79,11 +83,25 @@ else
{
<text> (@row.Note)</text>
}
@if (Model.LoggedIn)
{
<a href="/SuggestPersonRelationEdit/@row.RelationId" class="up-icon-btn" title="پیشنهاد ویرایش/حذف این نسبت" aria-label="پیشنهاد ویرایش/حذف این نسبت">✎</a>
}
</li>
}
</ul>
</div>
}
else if (Model.LoggedIn)
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<h2>
خویشاوندان
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
</h2>
</div>
}
@if (Model.AffiliationRows.Count > 0)
{

View File

@ -41,6 +41,12 @@ namespace GanjooRazor.Pages
public class PersonRelationDisplayRow
{
/// <summary>
/// the underlying GanjoorPersonRelation row's own id - used to link to
/// /SuggestPersonRelationEdit/{RelationId} for suggesting a change/removal of this edge
/// </summary>
public int RelationId { get; set; }
/// <summary>
/// the other person's role relative to the subject (e.g. "فرزند", "پدر/مادر", "نیا")
/// </summary>
@ -131,6 +137,7 @@ namespace GanjooRazor.Pages
{
RelationRows.Add(new PersonRelationDisplayRow()
{
RelationId = r.Id,
Label = RelationLabel(r),
OtherPersonId = r.OtherPersonId,
OtherPersonName = r.OtherPersonName,

View File

@ -482,6 +482,8 @@
<a class="dropdown-item" asp-area="Admin" asp-page="ReviewPartEdits">ویرایش‌های قطعات</a>
<a class="dropdown-item" asp-area="Admin" asp-page="ReviewCatEdits">ویرایش‌های زندگینامه‌ها</a>
<a class="dropdown-item" asp-area="Admin" asp-page="ReviewQuoteds">مشق‌ها</a>
<a class="dropdown-item" asp-area="Admin" asp-page="ReviewPersonEdits">ویرایش‌های شخصیت‌ها</a>
<a class="dropdown-item" asp-area="Admin" asp-page="ReviewPersonRelationEdits">ویرایش‌های نسبت‌های خویشاوندی</a>
</div>
</li>

View File

@ -0,0 +1,103 @@
@page "{personId:int}"
@model GanjooRazor.Pages.SuggestNewPersonRelationModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
ViewData["Title"] = "گنجور » پیشنهاد خویشاوند جدید";
}
@section Head {
<meta name="robots" content="noindex, nofollow" />
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
@Model.LastError
}
else
{
<!-- #hdr2 -->
<div id="fa">
<div id="maincnt">
<partial name="_InlineLoginPartial" model="Model" />
<div class="spacer">&nbsp;</div>
<div class="poem">
<h1>پیشنهاد خویشاوند جدید برای: @Model.Person.Name</h1>
<p class="up-hint">
این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران، به شجره‌نامهٔ
<a href="/person/@Model.Person.Id">@Model.Person.Name</a> افزوده خواهد شد.
</p>
@if (!string.IsNullOrEmpty(Model.LastResult))
{
<p>@Html.Raw(Model.LastResult)</p>
}
<form method="post" action="/SuggestNewPersonRelation/@Model.Person.Id?Handler=Suggest">
@Html.AntiForgeryToken()
<table>
<tr>
<td>خویشاوند:</td>
<td>
<select asp-for="OtherPersonId">
<option value="0">— انتخاب کنید —</option>
@foreach (var p in Model.OtherPeople)
{
<option value="@p.Id">@p.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>نسبت با @Model.Person.Name:</td>
<td>
<select asp-for="RelationKind">
<option value="Child">فرزند این شخصیت است</option>
<option value="Parent">پدر یا مادر این شخصیت است</option>
<option value="Sibling">خواهر یا برادر این شخصیت است</option>
<option value="Spouse">همسر این شخصیت است</option>
<option value="DistantAncestor">نیای دور این شخصیت است (بدون نسل مشخص)</option>
<option value="DistantDescendant">نوادهٔ دور این شخصیت است (بدون نسل مشخص)</option>
</select>
</td>
</tr>
<tr>
<td>
درجه (فقط برای نیا/نواده):
<p class="up-hint"><small>مثلاً برای پدربزرگ/مادربزرگ عدد ۲ را وارد کنید. اگر نمی‌دانید خالی بگذارید.</small></p>
</td>
<td><input asp-for="DegreeHint" /></td>
</tr>
<tr>
<td>یادداشت (مستندسازی):</td>
<td><textarea asp-for="Note" rows="3" cols="60"></textarea></td>
</tr>
<tr>
<td>یادداشت برای مدیر بررسی‌کننده:</td>
<td><textarea asp-for="SuggestionNote" rows="3" cols="60"></textarea></td>
</tr>
</table>
<input type="submit" value="ثبت پیشنهاد" class="up-btn up-btn--success" />
<a role="button" href="/person/@Model.Person.Id" class="up-btn up-btn--ghost">انصراف</a>
</form>
</div>
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using GanjooRazor.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RMuseum.Models.Ganjoor;
namespace GanjooRazor.Pages
{
/// <summary>
/// contributor-facing form for suggesting a brand new kinship edge between the subject person
/// and another already-approved person - the "Add" counterpart of SuggestPersonRelationEdit.cshtml
/// (which only handles Modify/Remove of an already-existing edge). Presents a direction-aware
/// "RelationKind" choice (e.g. "فرزند" vs "پدر یا مادر") from the subject's point of view, and
/// translates it into the correct Person1Id/Person2Id/RelationType triple before submitting -
/// GanjoorPersonRelation itself has no notion of "from the subject's 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>
public class SuggestNewPersonRelationModel : LoginPartialEnabledPageModel
{
public SuggestNewPersonRelationModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public string LastResult { get; set; }
public GanjoorRelatedPerson Person { get; set; }
public List<GanjoorRelatedPerson> OtherPeople { get; set; }
[BindProperty]
public int OtherPersonId { get; set; }
[BindProperty]
public string RelationKind { get; set; }
[BindProperty]
public int? DegreeHint { get; set; }
[BindProperty]
public string Note { get; set; }
[BindProperty]
public string SuggestionNote { get; set; }
private async Task<bool> PreparePersonAsync(int id)
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/{id}");
if (!response.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(response);
return false;
}
Person = JsonConvert.DeserializeObject<GanjoorRelatedPerson>(await response.Content.ReadAsStringAsync());
if (Person == null)
{
LastError = "شخصیتی با این کد پیدا نشد.";
return false;
}
var peopleResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/people");
if (!peopleResponse.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(peopleResponse);
return false;
}
var allPeople = JsonConvert.DeserializeObject<List<GanjoorRelatedPerson>>(await peopleResponse.Content.ReadAsStringAsync());
OtherPeople = allPeople.Where(p => p.Id != id).ToList();
return true;
}
public async Task<IActionResult> OnGetAsync(int personId)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
await PreparePersonAsync(personId);
return Page();
}
public async Task<IActionResult> OnPostSuggestAsync(int personId)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
LastResult = "";
if (!await PreparePersonAsync(personId))
{
return Page();
}
if (OtherPersonId == 0 || OtherPersonId == personId)
{
LastResult = "لطفاً خویشاوند مورد نظر را انتخاب کنید.";
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()
{
Action = PersonRelationSuggestionAction.Add,
SuggestedDegreeHint = DegreeHint,
SuggestedNote = string.IsNullOrWhiteSpace(Note) ? null : Note.Trim(),
SuggestionNote = string.IsNullOrWhiteSpace(SuggestionNote) ? null : SuggestionNote.Trim(),
};
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)))
{
await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response);
HttpResponseMessage response = await secureClient.PostAsync(
$"{APIRoot.Url}/api/people/relationeditsuggestion",
new StringContent(JsonConvert.SerializeObject(suggestion), Encoding.UTF8, "application/json"));
if (!response.IsSuccessStatusCode)
{
LastResult = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return Page();
}
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{personId}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
return Page();
}
}
}
}

View File

@ -42,6 +42,13 @@ else
<input type="hidden" asp-for="Suggestion.PersonId" />
<table>
<tr>
<td>پیشنهاد حذف این شخصیت:</td>
<td>
<input asp-for="Suggestion.SuggestedForDeletion" id="suggestedForDeletion" />
<p class="up-hint"><small>در صورت انتخاب این گزینه سایر فیلدها نادیده گرفته خواهد شد و در صورت تأیید مدیران، این شخصیت و همهٔ نسبت‌ها/وابستگی‌های آن حذف می‌شود.</small></p>
</td>
</tr>
<tr>
<td>نام:</td>
<td><input asp-for="Suggestion.SuggestedName" size="40" /></td>

View File

@ -0,0 +1,105 @@
@page "{relationId:int}"
@model GanjooRazor.Pages.SuggestPersonRelationEditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using RMuseum.Models.Ganjoor
@{
Layout = "_Layout";
ViewData["Title"] = "گنجور » پیشنهاد ویرایش نسبت خویشاوندی";
}
@section Head {
<meta name="robots" content="noindex, nofollow" />
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
@Model.LastError
}
else
{
<!-- #hdr2 -->
<div id="fa">
<div id="maincnt">
<partial name="_InlineLoginPartial" model="Model" />
<div class="spacer">&nbsp;</div>
<div class="poem">
<h1>
پیشنهاد ویرایش نسبت:
<a href="/person/@Model.Relation.Person1Id">@(Model.Relation.Person1 != null ? Model.Relation.Person1.Name : Model.Relation.Person1Id.ToString())</a>
و
<a href="/person/@Model.Relation.Person2Id">@(Model.Relation.Person2 != null ? Model.Relation.Person2.Name : Model.Relation.Person2Id.ToString())</a>
</h1>
<p class="up-hint">
این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران اعمال خواهد شد.
</p>
@if (!string.IsNullOrEmpty(Model.LastResult))
{
<p>@Html.Raw(Model.LastResult)</p>
}
<form method="post" action="/SuggestPersonRelationEdit/@Model.Relation.Id?Handler=SuggestEdit">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Suggestion.ExistingRelationId" />
<input type="hidden" asp-for="Suggestion.Person1Id" />
<input type="hidden" asp-for="Suggestion.Person2Id" />
<table>
<tr>
<td>نوع درخواست:</td>
<td>
<label><input type="radio" name="Action" value="Modify" checked="checked" /> ویرایش این نسبت</label>
&nbsp;
<label><input type="radio" name="Action" value="Remove" /> حذف این نسبت (اشتباه ثبت شده است)</label>
</td>
</tr>
<tr>
<td>نوع نسبت:</td>
<td>
<select asp-for="Suggestion.SuggestedRelationType">
<option value="@((int)PersonRelationType.Parent)">پدر/مادر و فرزند (طرف اول، پدر/مادر است)</option>
<option value="@((int)PersonRelationType.Sibling)">خواهر/برادر</option>
<option value="@((int)PersonRelationType.Spouse)">همسر</option>
<option value="@((int)PersonRelationType.Ancestor)">نیا و نواده (طرف اول، نیاست)</option>
</select>
</td>
</tr>
<tr>
<td>
درجه (فقط برای نیا/نواده):
<p class="up-hint"><small>مثلاً برای پدربزرگ/مادربزرگ عدد ۲ را وارد کنید. اگر نمی‌دانید خالی بگذارید.</small></p>
</td>
<td><input asp-for="Suggestion.SuggestedDegreeHint" /></td>
</tr>
<tr>
<td>یادداشت (مستندسازی):</td>
<td><textarea asp-for="Suggestion.SuggestedNote" rows="3" cols="60"></textarea></td>
</tr>
<tr>
<td>یادداشت برای مدیر بررسی‌کننده:</td>
<td><textarea asp-for="Suggestion.SuggestionNote" rows="3" cols="60"></textarea></td>
</tr>
</table>
<input type="submit" value="ثبت پیشنهاد" class="up-btn up-btn--success" />
<a role="button" href="/person/@Model.Relation.Person1Id" class="up-btn up-btn--ghost">انصراف</a>
</form>
</div>
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using GanjooRazor.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RMuseum.Models.Ganjoor;
namespace GanjooRazor.Pages
{
/// <summary>
/// contributor-facing form for suggesting a change to, or removal of, an existing
/// GanjoorPersonRelation - the kinship-edge counterpart of SuggestPersonEdit.cshtml. Any
/// logged-in user may submit here; nothing is changed until a moderator approves it via
/// Admin/ReviewPersonRelationEdits. Reachable from a person's own public page (Person.cshtml).
/// </summary>
public class SuggestPersonRelationEditModel : LoginPartialEnabledPageModel
{
public SuggestPersonRelationEditModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public string LastResult { get; set; }
public GanjoorPersonRelation Relation { get; set; }
[BindProperty]
public GanjoorPersonRelationEditSuggestion Suggestion { get; set; }
[BindProperty]
public string Action { get; set; } // "Modify" or "Remove", from the posted radio buttons
private async Task<bool> PrepareRelationAsync(int relationId)
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/relations/{relationId}");
if (!response.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(response);
return false;
}
Relation = JsonConvert.DeserializeObject<GanjoorPersonRelation>(await response.Content.ReadAsStringAsync());
if (Relation == null)
{
LastError = "نسبتی با این کد پیدا نشد.";
return false;
}
return true;
}
private void FillSuggestionFromCurrentRelation()
{
// pre-filled with the relation's CURRENT values, same convention as SuggestPersonEdit -
// submitting a plain "Modify" without changing anything is a harmless no-op
Suggestion = new GanjoorPersonRelationEditSuggestion()
{
Action = PersonRelationSuggestionAction.Modify,
ExistingRelationId = Relation.Id,
Person1Id = Relation.Person1Id,
Person2Id = Relation.Person2Id,
SuggestedRelationType = Relation.RelationType,
SuggestedDegreeHint = Relation.DegreeHint,
SuggestedNote = Relation.Note,
};
}
public async Task<IActionResult> OnGetAsync(int relationId)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
if (!await PrepareRelationAsync(relationId))
{
return Page();
}
FillSuggestionFromCurrentRelation();
return Page();
}
public async Task<IActionResult> OnPostSuggestEditAsync(int relationId)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
LastResult = "";
if (!await PrepareRelationAsync(relationId))
{
return Page();
}
Suggestion.ExistingRelationId = Relation.Id;
Suggestion.Action = Action == "Remove" ? PersonRelationSuggestionAction.Remove : PersonRelationSuggestionAction.Modify;
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
{
await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response);
HttpResponseMessage response = await secureClient.PostAsync(
$"{APIRoot.Url}/api/people/relationeditsuggestion",
new StringContent(JsonConvert.SerializeObject(Suggestion), Encoding.UTF8, "application/json"));
if (!response.IsSuccessStatusCode)
{
LastResult = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
FillSuggestionFromCurrentRelation();
return Page();
}
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{Relation.Person1Id}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
FillSuggestionFromCurrentRelation();
return Page();
}
}
}
}

View File

@ -207,6 +207,100 @@ namespace RMuseum.Controllers
return Ok(res.Result);
}
/// <summary>
/// get a single kinship edge by its own id, with both sides' names resolved - used by
/// /SuggestPersonRelationEdit/{relationId} to show what it's about
/// </summary>
/// <param name="relationId"></param>
/// <returns></returns>
[HttpGet("relations/{relationId:int}")]
[AllowAnonymous]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelation))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetRelationByIdAsync(int relationId)
{
var res = await _personService.GetRelationByIdAsync(relationId);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// suggest an addition, change or removal of a kinship edge - any logged-in user, same as
/// suggesting a person edit. Goes into the pending queue; does not change anything.
/// </summary>
/// <param name="suggestion"></param>
/// <returns></returns>
[HttpPost("relationeditsuggestion")]
[Authorize]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> SuggestPersonRelationEditAsync([FromBody] GanjoorPersonRelationEditSuggestion suggestion)
{
suggestion.UserId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
var res = await _personService.SuggestPersonRelationEditAsync(suggestion);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// get the next unreviewed relation-edit suggestion, for the moderator queue - same
/// permission as reviewing a person edit
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
[HttpGet("relationeditsuggestions/next")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetNextUnreviewedPersonRelationEditSuggestionAsync(int skip = 0)
{
var res = await _personService.GetNextUnreviewedPersonRelationEditSuggestionAsync(skip);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
var resCount = await _personService.GetUnreviewedPersonRelationEditSuggestionCountAsync();
if (!string.IsNullOrEmpty(resCount.ExceptionString))
return BadRequest(resCount.ExceptionString);
// Paging Header - same shape ReviewEdits.cshtml.cs already knows how to read for poem corrections
HttpContext.Response.Headers.Append("paging-headers",
JsonConvert.SerializeObject(
new PaginationMetadata()
{
totalCount = resCount.Result,
pageSize = -1,
currentPage = -1,
hasNextPage = false,
hasPreviousPage = false,
totalPages = -1
})
);
return Ok(res.Result);//might be null
}
/// <summary>
/// apply a moderator's decision to a pending relation-edit suggestion - same permission as
/// moderating a person edit
/// </summary>
/// <param name="id"></param>
/// <param name="moderation"></param>
/// <returns></returns>
[HttpPost("relationeditsuggestions/{id:int}/moderate")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> ModeratePersonRelationEditSuggestionAsync(int id, [FromBody] PersonEditSuggestionModerationViewModel moderation)
{
Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
var res = await _personService.ModeratePersonRelationEditSuggestionAsync(userId, id, moderation.Result, moderation.ReviewNote);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// person service
/// </summary>

View File

@ -154,6 +154,29 @@ namespace RMuseum.DbContext
.HasForeignKey(a => a.Person2Id)
.OnDelete(DeleteBehavior.Restrict);
// same two-required-FKs-to-the-same-table situation as GanjoorPersonRelation above, plus
// a third optional FK to GanjoorPersonRelations itself (ExistingRelationId) - also
// restricted, since a relation that still has a pending suggestion against it should be
// resolved (or the suggestion rejected) before it can be deleted directly, not silently
// orphan the suggestion
builder.Entity<GanjoorPersonRelationEditSuggestion>()
.HasOne(s => s.Person1)
.WithMany()
.HasForeignKey(s => s.Person1Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorPersonRelationEditSuggestion>()
.HasOne(s => s.Person2)
.WithMany()
.HasForeignKey(s => s.Person2Id)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorPersonRelationEditSuggestion>()
.HasOne(s => s.ExistingRelation)
.WithMany()
.HasForeignKey(s => s.ExistingRelationId)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<GanjoorUserBookmark>()
.HasIndex(b => new { b.UserId, b.PoemId, b.CoupletIndex });
@ -625,6 +648,12 @@ namespace RMuseum.DbContext
/// </summary>
public DbSet<GanjoorPersonEditSuggestion> GanjoorPersonEditSuggestions { get; set; }
/// <summary>
/// pending/reviewed suggested additions, changes or removals of a kinship edge between two
/// already-approved people - see GanjoorPersonRelationEditSuggestion
/// </summary>
public DbSet<GanjoorPersonRelationEditSuggestion> GanjoorPersonRelationEditSuggestions { get; set; }
/// <summary>
/// Books (PDF Library)
/// </summary>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RMuseum.Migrations
{
/// <inheritdoc />
public partial class AddGanjoorPersonRelationEditSuggestion : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "SuggestedForDeletion",
table: "GanjoorPersonEditSuggestions",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "GanjoorPersonRelationEditSuggestions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Action = table.Column<int>(type: "int", nullable: false),
ExistingRelationId = table.Column<int>(type: "int", nullable: true),
Person1Id = table.Column<int>(type: "int", nullable: false),
Person2Id = table.Column<int>(type: "int", nullable: false),
SuggestedRelationType = table.Column<int>(type: "int", nullable: false),
SuggestedDegreeHint = table.Column<int>(type: "int", nullable: true),
SuggestedNote = table.Column<string>(type: "nvarchar(max)", nullable: true),
SuggestionNote = table.Column<string>(type: "nvarchar(max)", nullable: true),
Date = table.Column<DateTime>(type: "datetime2", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Reviewed = table.Column<bool>(type: "bit", nullable: false),
Result = table.Column<int>(type: "int", nullable: false),
ReviewDate = table.Column<DateTime>(type: "datetime2", nullable: false),
ReviewNote = table.Column<string>(type: "nvarchar(max)", nullable: true),
ReviewerUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GanjoorPersonRelationEditSuggestions", x => x.Id);
table.ForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_AspNetUsers_ReviewerUserId",
column: x => x.ReviewerUserId,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_GanjoorPersonRelations_ExistingRelationId",
column: x => x.ExistingRelationId,
principalTable: "GanjoorPersonRelations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_GanjoorRelatedPersons_Person1Id",
column: x => x.Person1Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_GanjoorPersonRelationEditSuggestions_GanjoorRelatedPersons_Person2Id",
column: x => x.Person2Id,
principalTable: "GanjoorRelatedPersons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_ExistingRelationId",
table: "GanjoorPersonRelationEditSuggestions",
column: "ExistingRelationId");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_Person1Id",
table: "GanjoorPersonRelationEditSuggestions",
column: "Person1Id");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_Person2Id",
table: "GanjoorPersonRelationEditSuggestions",
column: "Person2Id");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_ReviewerUserId",
table: "GanjoorPersonRelationEditSuggestions",
column: "ReviewerUserId");
migrationBuilder.CreateIndex(
name: "IX_GanjoorPersonRelationEditSuggestions_UserId",
table: "GanjoorPersonRelationEditSuggestions",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GanjoorPersonRelationEditSuggestions");
migrationBuilder.DropColumn(
name: "SuggestedForDeletion",
table: "GanjoorPersonEditSuggestions");
}
}
}

View File

@ -1475,6 +1475,9 @@ namespace RMuseum.Migrations
b.Property<string>("SuggestedFamilyTreeCaption")
.HasColumnType("nvarchar(max)");
b.Property<bool>("SuggestedForDeletion")
.HasColumnType("bit");
b.Property<string>("SuggestedName")
.HasColumnType("nvarchar(max)");
@ -1540,6 +1543,74 @@ namespace RMuseum.Migrations
b.ToTable("GanjoorPersonRelations");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("Action")
.HasColumnType("int");
b.Property<DateTime>("Date")
.HasColumnType("datetime2");
b.Property<int?>("ExistingRelationId")
.HasColumnType("int");
b.Property<int>("Person1Id")
.HasColumnType("int");
b.Property<int>("Person2Id")
.HasColumnType("int");
b.Property<int>("Result")
.HasColumnType("int");
b.Property<DateTime>("ReviewDate")
.HasColumnType("datetime2");
b.Property<string>("ReviewNote")
.HasColumnType("nvarchar(max)");
b.Property<bool>("Reviewed")
.HasColumnType("bit");
b.Property<Guid?>("ReviewerUserId")
.HasColumnType("uniqueidentifier");
b.Property<int?>("SuggestedDegreeHint")
.HasColumnType("int");
b.Property<string>("SuggestedNote")
.HasColumnType("nvarchar(max)");
b.Property<int>("SuggestedRelationType")
.HasColumnType("int");
b.Property<string>("SuggestionNote")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ExistingRelationId");
b.HasIndex("Person1Id");
b.HasIndex("Person2Id");
b.HasIndex("ReviewerUserId");
b.HasIndex("UserId");
b.ToTable("GanjoorPersonRelationEditSuggestions");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b =>
{
b.Property<int>("Id")
@ -5441,6 +5512,46 @@ namespace RMuseum.Migrations
b.Navigation("Person2");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion", b =>
{
b.HasOne("RMuseum.Models.Ganjoor.GanjoorPersonRelation", "ExistingRelation")
.WithMany()
.HasForeignKey("ExistingRelationId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person1")
.WithMany()
.HasForeignKey("Person1Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("RMuseum.Models.Ganjoor.GanjoorRelatedPerson", "Person2")
.WithMany()
.HasForeignKey("Person2Id")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "ReviewerUser")
.WithMany()
.HasForeignKey("ReviewerUserId");
b.HasOne("RSecurityBackend.Models.Auth.Db.RAppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExistingRelation");
b.Navigation("Person1");
b.Navigation("Person2");
b.Navigation("ReviewerUser");
b.Navigation("User");
});
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorPoem", b =>
{
b.HasOne("RMuseum.Models.Ganjoor.GanjoorCat", "Cat")

View File

@ -108,6 +108,16 @@ namespace RMuseum.Models.Ganjoor
/// </summary>
public string SuggestedFamilyTreeCaption { get; set; }
/// <summary>
/// true if this suggestion is actually a request to delete the person outright, not to
/// change their fields - when true, every Suggested* field above is ignored on approval
/// (they still carry whatever the contributor's pre-filled form happened to hold) and
/// ModeratePersonEditSuggestionAsync removes the person and every relation/affiliation/tag
/// reference to them instead. Kept on this same entity rather than a separate one so the
/// existing suggest/review UI and permissions are reused as-is.
/// </summary>
public bool SuggestedForDeletion { get; set; }
/// <summary>
/// suggester's note to the moderator
/// </summary>

View File

@ -0,0 +1,128 @@
using RSecurityBackend.Models.Auth.Db;
using System;
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// a contributor's suggestion to add a brand new kinship edge between two already-approved
/// people, change an existing edge's type/degree/note, or remove an existing edge outright -
/// the moderated counterpart of GanjoorPersonEditSuggestion, but for the edges between people
/// rather than one person's own fields. Reviewed the same way (see
/// GanjoorRelatedPersonService.ModeratePersonRelationEditSuggestionAsync).
/// </summary>
public class GanjoorPersonRelationEditSuggestion
{
/// <summary>
/// suggestion id
/// </summary>
public int Id { get; set; }
/// <summary>
/// what this suggestion proposes doing
/// </summary>
public PersonRelationSuggestionAction Action { get; set; }
/// <summary>
/// for Modify/Remove: the existing GanjoorPersonRelation row this suggestion targets. Null
/// for Add, where there's nothing existing yet.
/// </summary>
public int? ExistingRelationId { get; set; }
/// <summary>
/// the existing relation being modified/removed (for showing a before/after diff to the
/// moderator) - null for Add
/// </summary>
public virtual GanjoorPersonRelation ExistingRelation { get; set; }
/// <summary>
/// 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
/// suggestion always accurately reflects what it targets even if the underlying relation
/// changes again before this is reviewed.
/// </summary>
public int Person1Id { get; set; }
/// <summary>
/// first person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person1 { get; set; }
/// <summary>
/// second person id, see Person1Id
/// </summary>
public int Person2Id { get; set; }
/// <summary>
/// second person (navigation)
/// </summary>
public virtual GanjoorRelatedPerson Person2 { get; set; }
/// <summary>
/// for Add/Modify: the relation type to create/change to. For Remove: a snapshot of the
/// existing relation's type at submission time, purely for display (removing doesn't use it).
/// </summary>
public PersonRelationType SuggestedRelationType { get; set; }
/// <summary>
/// see GanjoorPersonRelation.DegreeHint - same for/Add/Modify/Remove-display purpose as
/// SuggestedRelationType above
/// </summary>
public int? SuggestedDegreeHint { get; set; }
/// <summary>
/// see GanjoorPersonRelation.Note - same Add/Modify/Remove-display purpose as
/// SuggestedRelationType above
/// </summary>
public string SuggestedNote { get; set; }
/// <summary>
/// suggester's note to the moderator (e.g. why an existing relation is wrong)
/// </summary>
public string SuggestionNote { get; set; }
/// <summary>
/// date
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// suggester's user id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// suggester
/// </summary>
public virtual RAppUser User { get; set; }
/// <summary>
/// reviewed
/// </summary>
public bool Reviewed { get; set; }
/// <summary>
/// review result
/// </summary>
public CorrectionReviewResult Result { get; set; }
/// <summary>
/// review date
/// </summary>
public DateTime ReviewDate { get; set; }
/// <summary>
/// review note
/// </summary>
public string ReviewNote { get; set; }
/// <summary>
/// reviewer's user id
/// </summary>
public Guid? ReviewerUserId { get; set; }
/// <summary>
/// reviewer
/// </summary>
public virtual RAppUser ReviewerUser { get; set; }
}
}

View File

@ -0,0 +1,23 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// what a GanjoorPersonRelationEditSuggestion proposes doing to the kinship graph
/// </summary>
public enum PersonRelationSuggestionAction
{
/// <summary>
/// create a brand new GanjoorPersonRelation between Person1Id and Person2Id
/// </summary>
Add = 0,
/// <summary>
/// change the RelationType/DegreeHint/Note of the existing relation ExistingRelationId points to
/// </summary>
Modify = 1,
/// <summary>
/// remove the existing relation ExistingRelationId points to outright
/// </summary>
Remove = 2,
}
}

View File

@ -30,6 +30,12 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// </summary>
public class GanjoorPersonRelationInfo
{
/// <summary>
/// the underlying GanjoorPersonRelation row's own id - needed by the client to link to
/// /SuggestPersonRelationEdit/{id} for suggesting a change/removal of this specific edge
/// </summary>
public int Id { get; set; }
/// <summary>
/// the other person in this relation
/// </summary>

View File

@ -2894,6 +2894,39 @@
<param name="moderation"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetRelationByIdAsync(System.Int32)">
<summary>
get a single kinship edge by its own id, with both sides' names resolved - used by
/SuggestPersonRelationEdit/{relationId} to show what it's about
</summary>
<param name="relationId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.SuggestPersonRelationEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion)">
<summary>
suggest an addition, change or removal of a kinship edge - any logged-in user, same as
suggesting a person edit. Goes into the pending queue; does not change anything.
</summary>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetNextUnreviewedPersonRelationEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed relation-edit suggestion, for the moderator queue - same
permission as reviewing a person edit
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.ModeratePersonRelationEditSuggestionAsync(System.Int32,RMuseum.Models.Ganjoor.ViewModels.PersonEditSuggestionModerationViewModel)">
<summary>
apply a moderator's decision to a pending relation-edit suggestion - same permission as
moderating a person edit
</summary>
<param name="id"></param>
<param name="moderation"></param>
<returns></returns>
</member>
<member name="F:RMuseum.Controllers.GanjoorRelatedPersonController._personService">
<summary>
person service
@ -4030,6 +4063,12 @@
see GanjoorPersonEditSuggestion
</summary>
</member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.GanjoorPersonRelationEditSuggestions">
<summary>
pending/reviewed suggested additions, changes or removals of a kinship edge between two
already-approved people - see GanjoorPersonRelationEditSuggestion
</summary>
</member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.Books">
<summary>
Books (PDF Library)
@ -4877,6 +4916,18 @@
<member name="M:RMuseum.Migrations.AddGanjoorPersonEditSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddGanjoorPersonRelationEditSuggestion.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary>
donation expenditures
@ -9396,6 +9447,16 @@
suggested family tree caption
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedForDeletion">
<summary>
true if this suggestion is actually a request to delete the person outright, not to
change their fields - when true, every Suggested* field above is ignored on approval
(they still carry whatever the contributor's pre-filled form happened to hold) and
ModeratePersonEditSuggestionAsync removes the person and every relation/affiliation/tag
reference to them instead. Kept on this same entity rather than a separate one so the
existing suggest/review UI and permissions are reused as-is.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestionNote">
<summary>
suggester's note to the moderator
@ -9483,6 +9544,128 @@
free-text note (e.g. sourcing/reasoning for this relation)
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion">
<summary>
a contributor's suggestion to add a brand new kinship edge between two already-approved
people, change an existing edge's type/degree/note, or remove an existing edge outright -
the moderated counterpart of GanjoorPersonEditSuggestion, but for the edges between people
rather than one person's own fields. Reviewed the same way (see
GanjoorRelatedPersonService.ModeratePersonRelationEditSuggestionAsync).
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Id">
<summary>
suggestion id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Action">
<summary>
what this suggestion proposes doing
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelationId">
<summary>
for Modify/Remove: the existing GanjoorPersonRelation row this suggestion targets. Null
for Add, where there's nothing existing yet.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ExistingRelation">
<summary>
the existing relation being modified/removed (for showing a before/after diff to the
moderator) - null for Add
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person1Id">
<summary>
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
suggestion always accurately reflects what it targets even if the underlying relation
changes again before this is reviewed.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person1">
<summary>
first person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person2Id">
<summary>
second person id, see Person1Id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Person2">
<summary>
second person (navigation)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedRelationType">
<summary>
for Add/Modify: the relation type to create/change to. For Remove: a snapshot of the
existing relation's type at submission time, purely for display (removing doesn't use it).
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedDegreeHint">
<summary>
see GanjoorPersonRelation.DegreeHint - same for/Add/Modify/Remove-display purpose as
SuggestedRelationType above
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestedNote">
<summary>
see GanjoorPersonRelation.Note - same Add/Modify/Remove-display purpose as
SuggestedRelationType above
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.SuggestionNote">
<summary>
suggester's note to the moderator (e.g. why an existing relation is wrong)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Date">
<summary>
date
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.UserId">
<summary>
suggester's user id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.User">
<summary>
suggester
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Reviewed">
<summary>
reviewed
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.Result">
<summary>
review result
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ReviewDate">
<summary>
review date
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ReviewNote">
<summary>
review note
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ReviewerUserId">
<summary>
reviewer's user id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion.ReviewerUser">
<summary>
reviewer
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPoem">
<summary>
Ganjoor Poem
@ -11571,6 +11754,26 @@
doesn't fit any of the above - rely on Note for what the tie actually is
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction">
<summary>
what a GanjoorPersonRelationEditSuggestion proposes doing to the kinship graph
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction.Add">
<summary>
create a brand new GanjoorPersonRelation between Person1Id and Person2Id
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction.Modify">
<summary>
change the RelationType/DegreeHint/Note of the existing relation ExistingRelationId points to
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction.Remove">
<summary>
remove the existing relation ExistingRelationId points to outright
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationType">
<summary>
the kind of kinship edge a GanjoorPersonRelation represents between two GanjoorRelatedPerson
@ -13116,6 +13319,12 @@
one GanjoorPersonRelation row, resolved from Person's point of view
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonRelationInfo.Id">
<summary>
the underlying GanjoorPersonRelation row's own id - needed by the client to link to
/SuggestPersonRelationEdit/{id} for suggesting a change/removal of this specific edge
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonRelationInfo.OtherPersonId">
<summary>
the other person in this relation
@ -18008,6 +18217,48 @@
<param name="reviewNote"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetRelationByIdAsync(System.Int32)">
<summary>
get a single kinship edge by its own id, with both sides' names resolved - used by
/SuggestPersonRelationEdit/{relationId} to show what it's about
</summary>
<param name="relationId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.SuggestPersonRelationEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion)">
<summary>
submit a suggested addition, change or removal of a kinship edge - goes into the
pending queue, does not change anything until a moderator approves it
</summary>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetNextUnreviewedPersonRelationEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed relation-edit suggestion (for the moderator queue)
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetUnreviewedPersonRelationEditSuggestionCountAsync">
<summary>
unreviewed relation-edit suggestion count
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.ModeratePersonRelationEditSuggestionAsync(System.Guid,System.Int32,RMuseum.Models.Ganjoor.CorrectionReviewResult,System.String)">
<summary>
apply a moderator's decision to a pending relation-edit suggestion. On Approved: Add
creates a new GanjoorPersonRelation, Modify updates the existing one ExistingRelationId
points to, Remove deletes it (and auto-rejects any other still-pending suggestion that
also targeted that same now-gone relation, so it doesn't dangle).
</summary>
<param name="moderatorUserId"></param>
<param name="suggestionId"></param>
<param name="result"></param>
<param name="reviewNote"></param>
<returns></returns>
</member>
<member name="T:RMuseum.Services.IGanjoorService">
<summary>
Ganjoor Poems Content Privider Service
@ -21514,16 +21765,82 @@
<param name="reviewNote"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.DeletePersonAndReferencesAsync(RMuseum.Models.Ganjoor.GanjoorRelatedPerson)">
<summary>
removes a person and every row that would otherwise block that deletion at the database
level (kinship edges, affiliations, and other pending relation-edit suggestions touching
them - all Restrict-on-delete FKs, see RMuseumDbContext.OnModelCreating), plus detaches
(but does not delete) any approved poem geo/date tag that named them, since the tag
itself may still carry a real location/date worth keeping.
</summary>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetRelationByIdAsync(System.Int32)">
<summary>
get a single kinship edge by its own id, with both sides' names resolved
</summary>
<param name="relationId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.SuggestPersonRelationEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonRelationEditSuggestion)">
<summary>
submit a suggested addition, change or removal of a kinship edge
</summary>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetNextUnreviewedPersonRelationEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed relation-edit suggestion for the moderator queue
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetUnreviewedPersonRelationEditSuggestionCountAsync">
<summary>
unreviewed relation-edit suggestion count
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.ModeratePersonRelationEditSuggestionAsync(System.Guid,System.Int32,RMuseum.Models.Ganjoor.CorrectionReviewResult,System.String)">
<summary>
apply a moderator's decision to a pending relation-edit suggestion
</summary>
<param name="moderatorUserId"></param>
<param name="suggestionId"></param>
<param name="result"></param>
<param name="reviewNote"></param>
<returns></returns>
</member>
<member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._context">
<summary>
Database Context
</summary>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.#ctor(RMuseum.DbContext.RMuseumDbContext)">
<member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._appUserService">
<summary>
used to find every user holding the Ganjoor:Modify permission, to notify them when a new
suggestion needs review - same permission the moderation endpoints themselves require
</summary>
</member>
<member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._notificationService">
<summary>
used to notify moderators of a new pending suggestion, and submitters of its outcome -
same service/pattern GanjoorService uses for poem corrections and song suggestions
</summary>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.#ctor(RMuseum.DbContext.RMuseumDbContext,RSecurityBackend.Services.IAppUserService,RSecurityBackend.Services.IRNotificationService)">
<summary>
constructor
</summary>
<param name="context"></param>
<param name="appUserService"></param>
<param name="notificationService"></param>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.NotifyModeratorsOfPendingSuggestionAsync(System.String,System.String)">
<summary>
notify every user holding the Ganjoor:Modify permission that a new suggestion of the
given kind is pending review at reviewPageUrl - shared by both suggestion types below
</summary>
</member>
<member name="T:RMuseum.Services.Implementation.GanjoorService">
<summary>

View File

@ -96,5 +96,47 @@ namespace RMuseum.Services
/// <param name="reviewNote"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonEditSuggestion>> ModeratePersonEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote);
/// <summary>
/// get a single kinship edge by its own id, with both sides' names resolved - used by
/// /SuggestPersonRelationEdit/{relationId} to show what it's about
/// </summary>
/// <param name="relationId"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonRelation>> GetRelationByIdAsync(int relationId);
/// <summary>
/// submit a suggested addition, change or removal of a kinship edge - goes into the
/// pending queue, does not change anything until a moderator approves it
/// </summary>
/// <param name="suggestion"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> SuggestPersonRelationEditAsync(GanjoorPersonRelationEditSuggestion suggestion);
/// <summary>
/// get the next unreviewed relation-edit suggestion (for the moderator queue)
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> GetNextUnreviewedPersonRelationEditSuggestionAsync(int skip);
/// <summary>
/// unreviewed relation-edit suggestion count
/// </summary>
/// <returns></returns>
Task<RServiceResult<int>> GetUnreviewedPersonRelationEditSuggestionCountAsync();
/// <summary>
/// apply a moderator's decision to a pending relation-edit suggestion. On Approved: Add
/// creates a new GanjoorPersonRelation, Modify updates the existing one ExistingRelationId
/// points to, Remove deletes it (and auto-rejects any other still-pending suggestion that
/// also targeted that same now-gone relation, so it doesn't dangle).
/// </summary>
/// <param name="moderatorUserId"></param>
/// <param name="suggestionId"></param>
/// <param name="result"></param>
/// <param name="reviewNote"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> ModeratePersonRelationEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote);
}
}

View File

@ -1,8 +1,12 @@
using Microsoft.EntityFrameworkCore;
using RMuseum.DbContext;
using RMuseum.Models.Auth.Memory;
using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
using RSecurityBackend.Models.Auth.Memory;
using RSecurityBackend.Models.Generic;
using RSecurityBackend.Models.Notification;
using RSecurityBackend.Services;
using System;
using System.Collections.Generic;
using System.Linq;
@ -102,6 +106,7 @@ namespace RMuseum.Services.Implementation
var relations = relationRows.Select(r => new GanjoorPersonRelationInfo()
{
Id = r.Id,
OtherPersonId = r.Person1Id == id ? r.Person2Id : r.Person1Id,
OtherPersonName = r.Person1Id == id ? r.Person2.Name : r.Person1.Name,
RelationType = r.RelationType,
@ -268,20 +273,20 @@ namespace RMuseum.Services.Implementation
{
try
{
if (suggestion == null || string.IsNullOrWhiteSpace(suggestion.SuggestedName))
if (suggestion == null || (!suggestion.SuggestedForDeletion && string.IsNullOrWhiteSpace(suggestion.SuggestedName)))
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "نام شخصیت نمی‌تواند خالی باشد.");
}
var personExists = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).AnyAsync();
if (!personExists)
var person = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).SingleOrDefaultAsync();
if (person == null)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "شخصیت پیدا نشد.");
}
suggestion.Id = 0;
suggestion.Date = DateTime.Now;
suggestion.SuggestedName = suggestion.SuggestedName.Trim();
suggestion.SuggestedName = suggestion.SuggestedName?.Trim();
suggestion.SuggestedDescription = string.IsNullOrWhiteSpace(suggestion.SuggestedDescription) ? null : suggestion.SuggestedDescription.Trim();
suggestion.SuggestedWikiUrl = string.IsNullOrWhiteSpace(suggestion.SuggestedWikiUrl) ? null : suggestion.SuggestedWikiUrl.Trim();
suggestion.SuggestedFamilyTreeCaption = string.IsNullOrWhiteSpace(suggestion.SuggestedFamilyTreeCaption) ? null : suggestion.SuggestedFamilyTreeCaption.Trim();
@ -293,6 +298,14 @@ namespace RMuseum.Services.Implementation
_context.GanjoorPersonEditSuggestions.Add(suggestion);
await _context.SaveChangesAsync();
await NotifyModeratorsOfPendingSuggestionAsync(
suggestion.SuggestedForDeletion ? "پیشنهاد حذف شخصیت" : "پیشنهاد ویرایش شخصیت",
(suggestion.SuggestedForDeletion
? $"کاربری پیشنهاد حذف شخصیت «{person.Name}» را داده است."
: $"کاربری ویرایش جدیدی برای شخصیت «{person.Name}» پیشنهاد داده است.") +
$" لطفاً بخش <a href=\"https://ganjoor.net/Admin/ReviewPersonEdits\">ویرایش‌های پیشنهادی شخصیت‌ها</a> را بررسی فرمایید."
);
return new RServiceResult<GanjoorPersonEditSuggestion>(suggestion);
}
catch (Exception exp)
@ -367,25 +380,33 @@ namespace RMuseum.Services.Implementation
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "این پیشنهاد پیش‌تر بررسی شده است.");
}
var person = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).SingleOrDefaultAsync();
if (person == null)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "شخصیت مقصد این پیشنهاد پیدا نشد.");
}
var personName = person.Name; // snapshot before a possible deletion below, for the notification text
if (result == CorrectionReviewResult.Approved)
{
var person = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).SingleOrDefaultAsync();
if (person == null)
if (suggestion.SuggestedForDeletion)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "شخصیت مقصد این پیشنهاد پیدا نشد.");
await DeletePersonAndReferencesAsync(person);
}
else
{
person.Name = suggestion.SuggestedName;
person.Description = suggestion.SuggestedDescription;
person.WikiUrl = suggestion.SuggestedWikiUrl;
person.BirthYearInLHijri = suggestion.SuggestedBirthYearInLHijri;
person.DeathYearInLHijri = suggestion.SuggestedDeathYearInLHijri;
person.ValidBirthDate = suggestion.SuggestedValidBirthDate;
person.ValidDeathDate = suggestion.SuggestedValidDeathDate;
person.BirthLocationId = suggestion.SuggestedBirthLocationId;
person.DeathLocationId = suggestion.SuggestedDeathLocationId;
person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption;
// Id and MachineGenerated on the person are intentionally left untouched
}
person.Name = suggestion.SuggestedName;
person.Description = suggestion.SuggestedDescription;
person.WikiUrl = suggestion.SuggestedWikiUrl;
person.BirthYearInLHijri = suggestion.SuggestedBirthYearInLHijri;
person.DeathYearInLHijri = suggestion.SuggestedDeathYearInLHijri;
person.ValidBirthDate = suggestion.SuggestedValidBirthDate;
person.ValidDeathDate = suggestion.SuggestedValidDeathDate;
person.BirthLocationId = suggestion.SuggestedBirthLocationId;
person.DeathLocationId = suggestion.SuggestedDeathLocationId;
person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption;
// Id and MachineGenerated on the person are intentionally left untouched
}
suggestion.Reviewed = true;
@ -396,6 +417,29 @@ namespace RMuseum.Services.Implementation
await _context.SaveChangesAsync();
if (result == CorrectionReviewResult.Approved)
{
await _notificationService.PushNotification(
suggestion.UserId,
suggestion.SuggestedForDeletion ? "تأیید حذف شخصیت پیشنهادی" : "تأیید ویرایش پیشنهادی شخصیت",
suggestion.SuggestedForDeletion
? $"پیشنهاد شما برای حذف شخصیت «{personName}» تأیید و اعمال شد. از این که به تکمیل اطلاعات گنجور کمک کردید سپاسگزاریم."
: $"ویرایش پیشنهادی شما برای شخصیت «{personName}» تأیید و اعمال شد. از این که به تکمیل اطلاعات گنجور کمک کردید سپاسگزاریم."
);
}
else
{
await _notificationService.PushNotification(
suggestion.UserId,
suggestion.SuggestedForDeletion ? "رد پیشنهاد حذف شخصیت" : "رد ویرایش پیشنهادی شخصیت",
(suggestion.SuggestedForDeletion
? $"پیشنهاد شما برای حذف شخصیت «{personName}» تأیید نشد."
: $"ویرایش پیشنهادی شما برای شخصیت «{personName}» تأیید نشد.") +
(string.IsNullOrWhiteSpace(reviewNote) ? "" : $"{Environment.NewLine}یادداشت بازبین: «{reviewNote}»"),
NotificationType.Warning
);
}
return new RServiceResult<GanjoorPersonEditSuggestion>(suggestion);
}
catch (Exception exp)
@ -404,18 +448,407 @@ namespace RMuseum.Services.Implementation
}
}
/// <summary>
/// removes a person and every row that would otherwise block that deletion at the database
/// level (kinship edges, affiliations, and other pending relation-edit suggestions touching
/// them - all Restrict-on-delete FKs, see RMuseumDbContext.OnModelCreating), plus detaches
/// (but does not delete) any approved poem geo/date tag that named them, since the tag
/// itself may still carry a real location/date worth keeping.
/// </summary>
private async Task DeletePersonAndReferencesAsync(GanjoorRelatedPerson person)
{
var relations = await _context.GanjoorPersonRelations
.Where(r => r.Person1Id == person.Id || r.Person2Id == person.Id)
.ToListAsync();
var relationIds = relations.Select(r => r.Id).ToList();
var affiliations = await _context.GanjoorPersonAffiliations
.Where(a => a.Person1Id == person.Id || a.Person2Id == person.Id)
.ToListAsync();
// 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
// the Restrict FK above - auto-reject those with an explanatory note rather than letting
// the delete fail or silently drop them
var conflictingRelationSuggestions = await _context.GanjoorPersonRelationEditSuggestions
.Where(s => !s.Reviewed && (
s.Person1Id == person.Id ||
s.Person2Id == person.Id ||
(s.ExistingRelationId != null && relationIds.Contains(s.ExistingRelationId.Value))
))
.ToListAsync();
foreach (var conflicting in conflictingRelationSuggestions)
{
conflicting.Reviewed = true;
conflicting.Result = CorrectionReviewResult.RejectedBecauseUnnecessaryChange;
conflicting.ReviewNote = "شخصیت یا نسبت مرتبط با این پیشنهاد حذف شد.";
conflicting.ReviewDate = DateTime.Now;
}
var geoDateTags = await _context.PoemGeoDateTags.Where(t => t.PersonId == person.Id).ToListAsync();
foreach (var tag in geoDateTags)
{
tag.PersonId = null;
}
// any other still-pending edit suggestion FOR this same person (not the one being
// approved right now, which the caller updates separately) is moot once the person is
// gone - same auto-reject treatment
var conflictingEditSuggestions = await _context.GanjoorPersonEditSuggestions
.Where(s => !s.Reviewed && s.PersonId == person.Id)
.ToListAsync();
foreach (var conflicting in conflictingEditSuggestions)
{
conflicting.Reviewed = true;
conflicting.Result = CorrectionReviewResult.RejectedBecauseUnnecessaryChange;
conflicting.ReviewNote = "این شخصیت حذف شد.";
conflicting.ReviewDate = DateTime.Now;
}
_context.GanjoorPersonRelations.RemoveRange(relations);
_context.GanjoorPersonAffiliations.RemoveRange(affiliations);
_context.GanjoorRelatedPersons.Remove(person);
}
/// <summary>
/// get a single kinship edge by its own id, with both sides' names resolved
/// </summary>
/// <param name="relationId"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonRelation>> GetRelationByIdAsync(int relationId)
{
try
{
var relation = await _context.GanjoorPersonRelations
.Include(r => r.Person1)
.Include(r => r.Person2)
.Where(r => r.Id == relationId)
.SingleOrDefaultAsync();
if (relation == null)
{
return new RServiceResult<GanjoorPersonRelation>(null, "نسبت پیدا نشد.");
}
return new RServiceResult<GanjoorPersonRelation>(relation);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonRelation>(null, exp.ToString());
}
}
/// <summary>
/// submit a suggested addition, change or removal of a kinship edge
/// </summary>
/// <param name="suggestion"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> SuggestPersonRelationEditAsync(GanjoorPersonRelationEditSuggestion suggestion)
{
try
{
if (suggestion == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "اطلاعات پیشنهاد ناقص است.");
}
GanjoorPersonRelation existingRelation = null;
if (suggestion.Action == PersonRelationSuggestionAction.Modify || suggestion.Action == PersonRelationSuggestionAction.Remove)
{
if (suggestion.ExistingRelationId == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "نسبت مورد نظر برای ویرایش یا حذف مشخص نشده است.");
}
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, "نسبت مورد نظر پیدا نشد.");
}
// 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
{
if (suggestion.Person1Id == suggestion.Person2Id)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "دو طرف یک نسبت نمی‌توانند یک نفر باشند.");
}
}
var person1 = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.Person1Id).SingleOrDefaultAsync();
var person2 = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.Person2Id).SingleOrDefaultAsync();
if (person1 == null || person2 == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "یکی از دو طرف نسبت پیدا نشد.");
}
suggestion.Id = 0;
suggestion.ExistingRelation = null;
suggestion.Person1 = null;
suggestion.Person2 = null;
suggestion.Date = DateTime.Now;
suggestion.SuggestedNote = string.IsNullOrWhiteSpace(suggestion.SuggestedNote) ? null : suggestion.SuggestedNote.Trim();
suggestion.SuggestionNote = string.IsNullOrWhiteSpace(suggestion.SuggestionNote) ? null : suggestion.SuggestionNote.Trim();
suggestion.Reviewed = false;
suggestion.Result = CorrectionReviewResult.NotReviewed;
suggestion.ReviewNote = null;
suggestion.ReviewerUserId = null;
_context.GanjoorPersonRelationEditSuggestions.Add(suggestion);
await _context.SaveChangesAsync();
string actionTitle = suggestion.Action switch
{
PersonRelationSuggestionAction.Add => "پیشنهاد نسبت خویشاوندی جدید",
PersonRelationSuggestionAction.Modify => "پیشنهاد ویرایش نسبت خویشاوندی",
_ => "پیشنهاد حذف نسبت خویشاوندی",
};
string actionText = suggestion.Action switch
{
PersonRelationSuggestionAction.Add => $"کاربری پیشنهاد افزودن نسبت خویشاوندی جدید بین «{person1.Name}» و «{person2.Name}» را داده است.",
PersonRelationSuggestionAction.Modify => $"کاربری پیشنهاد ویرایش نسبت خویشاوندی بین «{person1.Name}» و «{person2.Name}» را داده است.",
_ => $"کاربری پیشنهاد حذف نسبت خویشاوندی بین «{person1.Name}» و «{person2.Name}» را داده است.",
};
await NotifyModeratorsOfPendingSuggestionAsync(
actionTitle,
actionText + " لطفاً بخش <a href=\"https://ganjoor.net/Admin/ReviewPersonRelationEdits\">ویرایش‌های پیشنهادی نسبت‌های خویشاوندی</a> را بررسی فرمایید."
);
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, exp.ToString());
}
}
/// <summary>
/// get the next unreviewed relation-edit suggestion for the moderator queue
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> GetNextUnreviewedPersonRelationEditSuggestionAsync(int skip)
{
try
{
var suggestion = await _context.GanjoorPersonRelationEditSuggestions
.Include(s => s.Person1)
.Include(s => s.Person2)
.Include(s => s.ExistingRelation)
.Include(s => s.User)
.Where(s => s.Reviewed == false)
.OrderBy(s => s.Id)
.Skip(skip)
.FirstOrDefaultAsync();
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, exp.ToString());
}
}
/// <summary>
/// unreviewed relation-edit suggestion count
/// </summary>
/// <returns></returns>
public async Task<RServiceResult<int>> GetUnreviewedPersonRelationEditSuggestionCountAsync()
{
try
{
return new RServiceResult<int>(await _context.GanjoorPersonRelationEditSuggestions.Where(s => s.Reviewed == false).CountAsync());
}
catch (Exception exp)
{
return new RServiceResult<int>(0, exp.ToString());
}
}
/// <summary>
/// apply a moderator's decision to a pending relation-edit suggestion
/// </summary>
/// <param name="moderatorUserId"></param>
/// <param name="suggestionId"></param>
/// <param name="result"></param>
/// <param name="reviewNote"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonRelationEditSuggestion>> ModeratePersonRelationEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote)
{
try
{
var suggestion = await _context.GanjoorPersonRelationEditSuggestions
.Include(s => s.Person1)
.Include(s => s.Person2)
.Where(s => s.Id == suggestionId)
.SingleOrDefaultAsync();
if (suggestion == null)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "پیشنهاد پیدا نشد.");
}
if (suggestion.Reviewed)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, "این پیشنهاد پیش‌تر بررسی شده است.");
}
var person1Name = suggestion.Person1?.Name;
var person2Name = suggestion.Person2?.Name;
if (result == CorrectionReviewResult.Approved)
{
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;
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;
}
}
}
suggestion.Reviewed = true;
suggestion.Result = result;
suggestion.ReviewNote = reviewNote;
suggestion.ReviewDate = DateTime.Now;
suggestion.ReviewerUserId = moderatorUserId;
await _context.SaveChangesAsync();
string actionLabel = suggestion.Action switch
{
PersonRelationSuggestionAction.Add => "افزودن نسبت خویشاوندی",
PersonRelationSuggestionAction.Modify => "ویرایش نسبت خویشاوندی",
_ => "حذف نسبت خویشاوندی",
};
if (result == CorrectionReviewResult.Approved)
{
await _notificationService.PushNotification(
suggestion.UserId,
$"تأیید {actionLabel}",
$"پیشنهاد شما برای {actionLabel} بین «{person1Name}» و «{person2Name}» تأیید و اعمال شد. از این که به تکمیل اطلاعات گنجور کمک کردید سپاسگزاریم."
);
}
else
{
await _notificationService.PushNotification(
suggestion.UserId,
$"رد {actionLabel}",
$"پیشنهاد شما برای {actionLabel} بین «{person1Name}» و «{person2Name}» تأیید نشد." +
(string.IsNullOrWhiteSpace(reviewNote) ? "" : $"{Environment.NewLine}یادداشت بازبین: «{reviewNote}»"),
NotificationType.Warning
);
}
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonRelationEditSuggestion>(null, exp.ToString());
}
}
/// <summary>
/// Database Context
/// </summary>
protected readonly RMuseumDbContext _context;
/// <summary>
/// used to find every user holding the Ganjoor:Modify permission, to notify them when a new
/// suggestion needs review - same permission the moderation endpoints themselves require
/// </summary>
protected readonly IAppUserService _appUserService;
/// <summary>
/// used to notify moderators of a new pending suggestion, and submitters of its outcome -
/// same service/pattern GanjoorService uses for poem corrections and song suggestions
/// </summary>
protected readonly IRNotificationService _notificationService;
/// <summary>
/// constructor
/// </summary>
/// <param name="context"></param>
public GanjoorRelatedPersonService(RMuseumDbContext context)
/// <param name="appUserService"></param>
/// <param name="notificationService"></param>
public GanjoorRelatedPersonService(RMuseumDbContext context, IAppUserService appUserService, IRNotificationService notificationService)
{
_context = context;
_appUserService = appUserService;
_notificationService = notificationService;
}
/// <summary>
/// notify every user holding the Ganjoor:Modify permission that a new suggestion of the
/// given kind is pending review at reviewPageUrl - shared by both suggestion types below
/// </summary>
private async Task NotifyModeratorsOfPendingSuggestionAsync(string title, string htmlText)
{
var moderators = await _appUserService.GetUsersHavingPermission(RMuseumSecurableItem.GanjoorEntityShortName, SecurableItem.ModifyOperationShortName);
if (string.IsNullOrEmpty(moderators.ExceptionString))
{
foreach (var moderator in moderators.Result)
{
await _notificationService.PushNotification((Guid)moderator.Id, title, htmlText, NotificationType.ActionRequired);
}
}
}
}
}