people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-29 12:35:40 +03:30
parent 20b13cd0e7
commit 1c65da5ade
13 changed files with 1341 additions and 9 deletions

View File

@ -958,6 +958,7 @@ else
@if (geoTag.PersonId != null) @if (geoTag.PersonId != null)
{ {
<text>شخصیت: @(geoTag.Person != null ? geoTag.Person.Name : $"شخصیت #{geoTag.PersonId}")</text> <text>شخصیت: @(geoTag.Person != null ? geoTag.Person.Name : $"شخصیت #{geoTag.PersonId}")</text>
<a href="/SuggestPersonEdit/@geoTag.PersonId" target="_blank" class="up-icon-btn" title="پیشنهاد ویرایش این شخصیت" aria-label="پیشنهاد ویرایش این شخصیت">✎</a>
} }
else if (personGraph?.Person != null) else if (personGraph?.Person != null)
{ {

View File

@ -0,0 +1,168 @@
@page
@model GanjooRazor.Areas.Admin.Pages.ReviewPersonEditsModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using DNTPersianUtils.Core
@{
Layout = "_AdminLayout";
ViewData["Title"] = "بازبینی پیشنهادهای ویرایش شخصیت‌ها";
}
<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;
var p = s.Person;
<p>
<a role="button" target="_blank" href="/person/@p.Id" class="up-btn up-btn--ghost up-btn--sm">@p.Name</a>
</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>
<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>
<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="moderatepersonedit" class="up-btn up-btn--success up-btn--block" onclick="moderatePersonEditSuggestion(@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 moderatePersonEditSuggestion(id) {
$('#moderatepersonedit').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');
$('#moderatepersonedit').show();
}
});
}
</script>
}

View File

@ -0,0 +1,178 @@
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.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 GanjoorPersonEditSuggestion entries - counterpart of
/// ReviewEdits.cshtml for poem corrections, but much simpler: one suggestion, one overall
/// Result, no per-field branching.
/// </summary>
public class ReviewPersonEditsModel : PageModel
{
/// <summary>
/// the pending suggestion currently shown
/// </summary>
public GanjoorPersonEditSuggestion 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; }
/// <summary>
/// full location catalog, used only to resolve the CURRENT person's BirthLocationId/
/// DeathLocationId into names for the before/after diff (the suggestion's own Suggested*
/// locations already arrive populated via the backend's Include)
/// </summary>
public List<GanjoorGeoLocation> Locations { get; set; }
public string LocationName(int? id)
{
if (id == null || Locations == null)
return "";
var loc = Locations.Where(l => l.Id == id).FirstOrDefault();
return loc == null ? "" : loc.Name;
}
public IActionResult OnPost()
{
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);
if (Request.Form["next"].Count == 1)
{
return Redirect($"/Admin/ReviewPersonEdits/?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 locationsResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/locations");
if (!locationsResponse.IsSuccessStatusCode)
{
FatalError = JsonConvert.DeserializeObject<string>(await locationsResponse.Content.ReadAsStringAsync());
return Page();
}
Locations = JsonConvert.DeserializeObject<List<GanjoorGeoLocation>>(await locationsResponse.Content.ReadAsStringAsync());
var nextResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/people/editsuggestions/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<GanjoorPersonEditSuggestion>(await nextResponse.Content.ReadAsStringAsync());
return Page();
}
else
{
return Redirect("/");
}
}
}
public async Task<IActionResult> OnPostModerateAsync([FromBody] PersonEditSuggestionModerationRequest 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/editsuggestions/{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 ReviewPersonEdits.cshtml's moderatePersonEditSuggestion() ajax call -
/// kept local to this page since it's only ever used to bind that one handler's [FromBody] parameter
/// </summary>
public class PersonEditSuggestionModerationRequest
{
public int Id { get; set; }
public string Result { get; set; }
public string ReviewNote { get; set; }
}
}

View File

@ -24,7 +24,13 @@ else
<div class="spacer">&nbsp;</div> <div class="spacer">&nbsp;</div>
<div class="poem"> <div class="poem">
<h1>@Model.Person.Name</h1> <h1>
@Model.Person.Name
@if (Model.LoggedIn)
{
<a href="/SuggestPersonEdit/@Model.Person.Id" class="up-icon-btn" title="پیشنهاد ویرایش این شخصیت" aria-label="پیشنهاد ویرایش این شخصیت">✎</a>
}
</h1>
@if (!string.IsNullOrEmpty(Model.Person.FamilyTreeCaption)) @if (!string.IsNullOrEmpty(Model.Person.FamilyTreeCaption))
{ {

View File

@ -0,0 +1,125 @@
@page "{id:int}"
@model GanjooRazor.Pages.SuggestPersonEditModel
@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="/SuggestPersonEdit/@Model.Person.Id?Handler=SuggestEdit">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Suggestion.PersonId" />
<table>
<tr>
<td>نام:</td>
<td><input asp-for="Suggestion.SuggestedName" size="40" /></td>
</tr>
<tr>
<td>نشانی ویکی‌پدیا:</td>
<td><input style="text-align:left;direction:ltr;" asp-for="Suggestion.SuggestedWikiUrl" size="60" /></td>
</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>
<td>
عنوان شجره‌نامه:
<p class="up-hint"><small>پُر کنید تا این شخصیت روی <a href="/people" target="_blank">صفحهٔ شجره‌نامه‌ها</a> با این عنوان فهرست شود.</small></p>
</td>
<td><input asp-for="Suggestion.SuggestedFamilyTreeCaption" size="40" /></td>
</tr>
<tr>
<td>توضیحات:</td>
<td><textarea asp-for="Suggestion.SuggestedDescription" rows="10" 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.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,156 @@
using System;
using System.Collections.Generic;
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 an edit to an already-approved GanjoorRelatedPerson.
/// Any logged-in user may submit here (same bar as suggesting a poem correction); nothing is
/// changed until a moderator approves it via Admin/ReviewPersonEdits. Reachable both from a
/// person's own public page (Person.cshtml) and from the poem-correction moderation page
/// (ReviewEdits.cshtml, when a correction links to an existing person).
/// </summary>
public class SuggestPersonEditModel : LoginPartialEnabledPageModel
{
public SuggestPersonEditModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public string LastResult { get; set; }
public GanjoorRelatedPerson Person { get; set; }
[BindProperty]
public GanjoorPersonEditSuggestion Suggestion { get; set; }
public List<GanjoorGeoLocation> Locations { get; set; }
private async Task ReadLocationsAsync()
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/locations");
if (!response.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(response);
return;
}
Locations = new List<GanjoorGeoLocation>
{
new GanjoorGeoLocation() { Id = 0, Latitude = 0, Longitude = 0, Name = "" }
};
Locations.AddRange(JsonConvert.DeserializeObject<GanjoorGeoLocation[]>(await response.Content.ReadAsStringAsync()));
}
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;
}
return true;
}
private void FillSuggestionFromCurrentPerson()
{
// the form is pre-filled with the person's CURRENT values, so submitting without
// changing anything is a no-op suggestion (harmless, just not useful) rather than
// accidentally blanking fields the contributor didn't mean to touch
Suggestion = new GanjoorPersonEditSuggestion()
{
PersonId = Person.Id,
SuggestedName = Person.Name,
SuggestedDescription = Person.Description,
SuggestedWikiUrl = Person.WikiUrl,
SuggestedBirthYearInLHijri = Person.BirthYearInLHijri,
SuggestedDeathYearInLHijri = Person.DeathYearInLHijri,
SuggestedValidBirthDate = Person.ValidBirthDate,
SuggestedValidDeathDate = Person.ValidDeathDate,
SuggestedBirthLocationId = Person.BirthLocationId,
SuggestedDeathLocationId = Person.DeathLocationId,
SuggestedFamilyTreeCaption = Person.FamilyTreeCaption,
};
}
public async Task<IActionResult> OnGetAsync(int id)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
await ReadLocationsAsync();
if (!await PreparePersonAsync(id))
{
return Page();
}
FillSuggestionFromCurrentPerson();
return Page();
}
public async Task<IActionResult> OnPostSuggestEditAsync(int id)
{
InitializeCommonPageState();
if (!LoggedIn)
{
return Redirect($"/login?redirect={Uri.EscapeDataString(Request.Path)}");
}
LastResult = "";
await ReadLocationsAsync();
if (!await PreparePersonAsync(id))
{
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/{id}/editsuggestion",
new StringContent(JsonConvert.SerializeObject(Suggestion), Encoding.UTF8, "application/json"));
if (!response.IsSuccessStatusCode)
{
LastResult = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
FillSuggestionFromCurrentPerson();
return Page();
}
LastResult = $"پیشنهاد ویرایش شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{id}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
FillSuggestionFromCurrentPerson();
return Page();
}
}
}
}

View File

@ -1,15 +1,25 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using RMuseum.Models.Auth.Memory;
using RMuseum.Models.Ganjoor; using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels; using RMuseum.Models.Ganjoor.ViewModels;
using RMuseum.Services; using RMuseum.Services;
using RSecurityBackend.Models.Auth.Memory;
using RSecurityBackend.Models.Generic;
using System;
using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace RMuseum.Controllers namespace RMuseum.Controllers
{ {
/// <summary> /// <summary>
/// related people (family tree / person tagging) - read-only for now, see IGanjoorRelatedPersonService /// related people (family tree / person tagging) - reads are anonymous; the only write paths are
/// the suggestion queue below (any logged-in user may suggest an edit to an existing person,
/// same as suggesting a poem correction; a moderator reviews it the same way a poem correction is
/// reviewed). There is no direct-edit endpoint for GanjoorRelatedPerson.
/// </summary> /// </summary>
[Produces("application/json")] [Produces("application/json")]
[Route("api/people")] [Route("api/people")]
@ -101,6 +111,84 @@ namespace RMuseum.Controllers
return Ok(res.Result); return Ok(res.Result);
} }
/// <summary>
/// suggest an edit to an already-approved person's own fields - any logged-in user, same as
/// suggesting a poem correction. Goes into the pending queue; does not change the person.
/// </summary>
/// <param name="id"></param>
/// <param name="suggestion"></param>
/// <returns></returns>
[HttpPost("{id:int}/editsuggestion")]
[Authorize]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> SuggestPersonEditAsync(int id, [FromBody] GanjoorPersonEditSuggestion suggestion)
{
suggestion.PersonId = id;
suggestion.UserId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
var res = await _personService.SuggestPersonEditAsync(suggestion);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary>
/// get the next unreviewed person-edit suggestion, for the moderator queue - same permission
/// as reviewing a poem correction
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
[HttpGet("editsuggestions/next")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetNextUnreviewedPersonEditSuggestionAsync(int skip = 0)
{
var res = await _personService.GetNextUnreviewedPersonEditSuggestionAsync(skip);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
var resCount = await _personService.GetUnreviewedPersonEditSuggestionCountAsync();
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 person-edit suggestion - same permission as
/// moderating a poem correction
/// </summary>
/// <param name="id"></param>
/// <param name="moderation"></param>
/// <returns></returns>
[HttpPost("editsuggestions/{id:int}/moderate")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> ModeratePersonEditSuggestionAsync(int id, [FromBody] PersonEditSuggestionModerationViewModel moderation)
{
Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
var res = await _personService.ModeratePersonEditSuggestionAsync(userId, id, moderation.Result, moderation.ReviewNote);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary> /// <summary>
/// person service /// person service
/// </summary> /// </summary>

View File

@ -619,6 +619,12 @@ namespace RMuseum.DbContext
/// </summary> /// </summary>
public DbSet<GanjoorPersonAffiliation> GanjoorPersonAffiliations { get; set; } public DbSet<GanjoorPersonAffiliation> GanjoorPersonAffiliations { get; set; }
/// <summary>
/// pending/reviewed suggested edits to an already-approved GanjoorRelatedPerson's own fields -
/// see GanjoorPersonEditSuggestion
/// </summary>
public DbSet<GanjoorPersonEditSuggestion> GanjoorPersonEditSuggestions { get; set; }
/// <summary> /// <summary>
/// Books (PDF Library) /// Books (PDF Library)
/// </summary> /// </summary>

View File

@ -0,0 +1,146 @@
using RSecurityBackend.Models.Auth.Db;
using System;
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// a contributor's suggested edit to an existing, already-approved GanjoorRelatedPerson's own
/// fields (name, bio, birth/death info, family tree caption, ...). Reviewed/approved the same
/// way a GanjoorPoemGeoDateTagCorrection is - see GanjoorService-ModeratePersonEditSuggestion.cs -
/// but standalone: it targets a person directly, not a poem/couplet. There is no admin-only
/// direct-edit path for GanjoorRelatedPerson; this suggestion queue is the only way any of its
/// fields change after it is first created.
/// </summary>
public class GanjoorPersonEditSuggestion
{
/// <summary>
/// suggestion id
/// </summary>
public int Id { get; set; }
/// <summary>
/// the person this suggestion proposes changes for
/// </summary>
public int PersonId { get; set; }
/// <summary>
/// the person this suggestion proposes changes for (their CURRENT, still-unchanged fields -
/// for showing a before/after diff to the moderator)
/// </summary>
public virtual GanjoorRelatedPerson Person { 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; }
// suggested full replacement state of every editable field - the contributor's form is
// pre-filled with the person's CURRENT values (see Person above), so these represent what
// the record ends up with after their edits, not a partial diff of only changed fields
/// <summary>
/// suggested name
/// </summary>
public string SuggestedName { get; set; }
/// <summary>
/// suggested description
/// </summary>
public string SuggestedDescription { get; set; }
/// <summary>
/// suggested wikipedia url
/// </summary>
public string SuggestedWikiUrl { get; set; }
/// <summary>
/// suggested birth year in lunar hijri
/// </summary>
public int SuggestedBirthYearInLHijri { get; set; }
/// <summary>
/// suggested death year in lunar hijri
/// </summary>
public int SuggestedDeathYearInLHijri { get; set; }
/// <summary>
/// suggested ValidBirthDate
/// </summary>
public bool SuggestedValidBirthDate { get; set; }
/// <summary>
/// suggested ValidDeathDate
/// </summary>
public bool SuggestedValidDeathDate { get; set; }
/// <summary>
/// suggested birth location id
/// </summary>
public int? SuggestedBirthLocationId { get; set; }
/// <summary>
/// suggested birth location
/// </summary>
public virtual GanjoorGeoLocation SuggestedBirthLocation { get; set; }
/// <summary>
/// suggested death location id
/// </summary>
public int? SuggestedDeathLocationId { get; set; }
/// <summary>
/// suggested death location
/// </summary>
public virtual GanjoorGeoLocation SuggestedDeathLocation { get; set; }
/// <summary>
/// suggested family tree caption
/// </summary>
public string SuggestedFamilyTreeCaption { get; set; }
/// <summary>
/// suggester's note to the moderator
/// </summary>
public string SuggestionNote { 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,18 @@
namespace RMuseum.Models.Ganjoor.ViewModels
{
/// <summary>
/// a moderator's decision on a pending GanjoorPersonEditSuggestion
/// </summary>
public class PersonEditSuggestionModerationViewModel
{
/// <summary>
/// review result
/// </summary>
public CorrectionReviewResult Result { get; set; }
/// <summary>
/// review note
/// </summary>
public string ReviewNote { get; set; }
}
}

View File

@ -2818,7 +2818,10 @@
</member> </member>
<member name="T:RMuseum.Controllers.GanjoorRelatedPersonController"> <member name="T:RMuseum.Controllers.GanjoorRelatedPersonController">
<summary> <summary>
related people (family tree / person tagging) - read-only for now, see IGanjoorRelatedPersonService related people (family tree / person tagging) - reads are anonymous; the only write paths are
the suggestion queue below (any logged-in user may suggest an edit to an existing person,
same as suggesting a poem correction; a moderator reviews it the same way a poem correction is
reviewed). There is no direct-edit endpoint for GanjoorRelatedPerson.
</summary> </summary>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetPeopleAsync"> <member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetPeopleAsync">
@ -2857,6 +2860,32 @@
<param name="id"></param> <param name="id"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.SuggestPersonEditAsync(System.Int32,RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion)">
<summary>
suggest an edit to an already-approved person's own fields - any logged-in user, same as
suggesting a poem correction. Goes into the pending queue; does not change the person.
</summary>
<param name="id"></param>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.GetNextUnreviewedPersonEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed person-edit suggestion, for the moderator queue - same permission
as reviewing a poem correction
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorRelatedPersonController.ModeratePersonEditSuggestionAsync(System.Int32,RMuseum.Models.Ganjoor.ViewModels.PersonEditSuggestionModerationViewModel)">
<summary>
apply a moderator's decision to a pending person-edit suggestion - same permission as
moderating a poem correction
</summary>
<param name="id"></param>
<param name="moderation"></param>
<returns></returns>
</member>
<member name="F:RMuseum.Controllers.GanjoorRelatedPersonController._personService"> <member name="F:RMuseum.Controllers.GanjoorRelatedPersonController._personService">
<summary> <summary>
person service person service
@ -3987,6 +4016,12 @@
approved non-family ties between people (e.g. minister-to-king) - see GanjoorPersonAffiliation approved non-family ties between people (e.g. minister-to-king) - see GanjoorPersonAffiliation
</summary> </summary>
</member> </member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.GanjoorPersonEditSuggestions">
<summary>
pending/reviewed suggested edits to an already-approved GanjoorRelatedPerson's own fields -
see GanjoorPersonEditSuggestion
</summary>
</member>
<member name="P:RMuseum.DbContext.RMuseumDbContext.Books"> <member name="P:RMuseum.DbContext.RMuseumDbContext.Books">
<summary> <summary>
Books (PDF Library) Books (PDF Library)
@ -9240,6 +9275,142 @@
free-text note (e.g. sourcing/reasoning, or what the tie actually is when AffiliationType is Other) free-text note (e.g. sourcing/reasoning, or what the tie actually is when AffiliationType is Other)
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion">
<summary>
a contributor's suggested edit to an existing, already-approved GanjoorRelatedPerson's own
fields (name, bio, birth/death info, family tree caption, ...). Reviewed/approved the same
way a GanjoorPoemGeoDateTagCorrection is - see GanjoorService-ModeratePersonEditSuggestion.cs -
but standalone: it targets a person directly, not a poem/couplet. There is no admin-only
direct-edit path for GanjoorRelatedPerson; this suggestion queue is the only way any of its
fields change after it is first created.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.Id">
<summary>
suggestion id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.PersonId">
<summary>
the person this suggestion proposes changes for
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.Person">
<summary>
the person this suggestion proposes changes for (their CURRENT, still-unchanged fields -
for showing a before/after diff to the moderator)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.Date">
<summary>
date
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.UserId">
<summary>
suggester's user id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.User">
<summary>
suggester
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedName">
<summary>
suggested name
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedDescription">
<summary>
suggested description
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedWikiUrl">
<summary>
suggested wikipedia url
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedBirthYearInLHijri">
<summary>
suggested birth year in lunar hijri
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedDeathYearInLHijri">
<summary>
suggested death year in lunar hijri
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedValidBirthDate">
<summary>
suggested ValidBirthDate
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedValidDeathDate">
<summary>
suggested ValidDeathDate
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedBirthLocationId">
<summary>
suggested birth location id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedBirthLocation">
<summary>
suggested birth location
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedDeathLocationId">
<summary>
suggested death location id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedDeathLocation">
<summary>
suggested death location
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedFamilyTreeCaption">
<summary>
suggested family tree caption
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestionNote">
<summary>
suggester's note to the moderator
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.Reviewed">
<summary>
reviewed
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.Result">
<summary>
review result
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.ReviewDate">
<summary>
review date
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.ReviewNote">
<summary>
review note
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.ReviewerUserId">
<summary>
reviewer's user id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.ReviewerUser">
<summary>
reviewer
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorPersonRelation"> <member name="T:RMuseum.Models.Ganjoor.GanjoorPersonRelation">
<summary> <summary>
an approved kinship edge between two people (GanjoorRelatedPerson rows) - the live/materialized an approved kinship edge between two people (GanjoorRelatedPerson rows) - the live/materialized
@ -14276,6 +14447,21 @@
model (not an EF entity), so no [NotMapped] or migration concerns here. model (not an EF entity), so no [NotMapped] or migration concerns here.
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.PersonEditSuggestionModerationViewModel">
<summary>
a moderator's decision on a pending GanjoorPersonEditSuggestion
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.PersonEditSuggestionModerationViewModel.Result">
<summary>
review result
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.PersonEditSuggestionModerationViewModel.ReviewNote">
<summary>
review note
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.PersonGraphSuggestion"> <member name="T:RMuseum.Models.Ganjoor.ViewModels.PersonGraphSuggestion">
<summary> <summary>
deserialized shape of GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson - a brand new deserialized shape of GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson - a brand new
@ -17681,9 +17867,13 @@
</member> </member>
<member name="T:RMuseum.Services.IGanjoorRelatedPersonService"> <member name="T:RMuseum.Services.IGanjoorRelatedPersonService">
<summary> <summary>
related people (family tree / person tagging) service - read-only for now: the only way a related people (family tree / person tagging) service. A new person is normally created via
new person is created is via the geo/date/person tag correction's SuggestedPersonGraphJson, the geo/date/person tag correction's SuggestedPersonGraphJson, materialized on moderator
materialized on moderator approval (see GanjoorService-ModeratePoemCorrection.cs) approval (see GanjoorService-ModeratePoemCorrection.cs). Editing an already-approved person's
own fields (e.g. adding a FamilyTreeCaption after the fact) goes through the suggest/review
queue below (SuggestPersonEditAsync / ModeratePersonEditSuggestionAsync) - there is no
direct-edit path; nothing ever writes to a GanjoorRelatedPerson's fields except that approval
step and the original creation-on-approval in GanjoorService-ModeratePoemCorrection.cs.
</summary> </summary>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetPeopleAsync"> <member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetPeopleAsync">
@ -17722,6 +17912,41 @@
<param name="id"></param> <param name="id"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.SuggestPersonEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion)">
<summary>
submit a suggested edit to an already-approved person's own fields - goes into the
pending queue, does not change the person itself until a moderator approves it
</summary>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetNextUnreviewedPersonEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed person-edit suggestion (for the moderator queue), including the
target person's current fields (for a before/after diff) and the suggester's nickname
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.GetUnreviewedPersonEditSuggestionCountAsync">
<summary>
unreviewed person-edit suggestion count
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorRelatedPersonService.ModeratePersonEditSuggestionAsync(System.Guid,System.Int32,RMuseum.Models.Ganjoor.CorrectionReviewResult,System.String)">
<summary>
apply a moderator's decision to a pending person-edit suggestion. On Approved, copies the
suggestion's Suggested* fields onto the target GanjoorRelatedPerson (Id and
MachineGenerated on the person are left untouched); any other result just marks the
suggestion reviewed/rejected without touching the person.
</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"> <member name="T:RMuseum.Services.IGanjoorService">
<summary> <summary>
Ganjoor Poems Content Privider Service Ganjoor Poems Content Privider Service
@ -21191,6 +21416,36 @@
<param name="id"></param> <param name="id"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.SuggestPersonEditAsync(RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion)">
<summary>
submit a suggested edit to an already-approved person's own fields
</summary>
<param name="suggestion"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetNextUnreviewedPersonEditSuggestionAsync(System.Int32)">
<summary>
get the next unreviewed person-edit suggestion for the moderator queue
</summary>
<param name="skip"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.GetUnreviewedPersonEditSuggestionCountAsync">
<summary>
unreviewed person-edit suggestion count
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorRelatedPersonService.ModeratePersonEditSuggestionAsync(System.Guid,System.Int32,RMuseum.Models.Ganjoor.CorrectionReviewResult,System.String)">
<summary>
apply a moderator's decision to a pending person-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"> <member name="F:RMuseum.Services.Implementation.GanjoorRelatedPersonService._context">
<summary> <summary>
Database Context Database Context

View File

@ -1,14 +1,19 @@
using RMuseum.Models.Ganjoor; using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels; using RMuseum.Models.Ganjoor.ViewModels;
using RSecurityBackend.Models.Generic; using RSecurityBackend.Models.Generic;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace RMuseum.Services namespace RMuseum.Services
{ {
/// <summary> /// <summary>
/// related people (family tree / person tagging) service - read-only for now: the only way a /// related people (family tree / person tagging) service. A new person is normally created via
/// new person is created is via the geo/date/person tag correction's SuggestedPersonGraphJson, /// the geo/date/person tag correction's SuggestedPersonGraphJson, materialized on moderator
/// materialized on moderator approval (see GanjoorService-ModeratePoemCorrection.cs) /// approval (see GanjoorService-ModeratePoemCorrection.cs). Editing an already-approved person's
/// own fields (e.g. adding a FamilyTreeCaption after the fact) goes through the suggest/review
/// queue below (SuggestPersonEditAsync / ModeratePersonEditSuggestionAsync) - there is no
/// direct-edit path; nothing ever writes to a GanjoorRelatedPerson's fields except that approval
/// step and the original creation-on-approval in GanjoorService-ModeratePoemCorrection.cs.
/// </summary> /// </summary>
public interface IGanjoorRelatedPersonService public interface IGanjoorRelatedPersonService
{ {
@ -47,5 +52,40 @@ namespace RMuseum.Services
/// <param name="id"></param> /// <param name="id"></param>
/// <returns></returns> /// <returns></returns>
Task<RServiceResult<PoemGeoDateTag[]>> GetPoemsByPersonAsync(int id); Task<RServiceResult<PoemGeoDateTag[]>> GetPoemsByPersonAsync(int id);
/// <summary>
/// submit a suggested edit to an already-approved person's own fields - goes into the
/// pending queue, does not change the person itself until a moderator approves it
/// </summary>
/// <param name="suggestion"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonEditSuggestion>> SuggestPersonEditAsync(GanjoorPersonEditSuggestion suggestion);
/// <summary>
/// get the next unreviewed person-edit suggestion (for the moderator queue), including the
/// target person's current fields (for a before/after diff) and the suggester's nickname
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonEditSuggestion>> GetNextUnreviewedPersonEditSuggestionAsync(int skip);
/// <summary>
/// unreviewed person-edit suggestion count
/// </summary>
/// <returns></returns>
Task<RServiceResult<int>> GetUnreviewedPersonEditSuggestionCountAsync();
/// <summary>
/// apply a moderator's decision to a pending person-edit suggestion. On Approved, copies the
/// suggestion's Suggested* fields onto the target GanjoorRelatedPerson (Id and
/// MachineGenerated on the person are left untouched); any other result just marks the
/// suggestion reviewed/rejected without touching the person.
/// </summary>
/// <param name="moderatorUserId"></param>
/// <param name="suggestionId"></param>
/// <param name="result"></param>
/// <param name="reviewNote"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonEditSuggestion>> ModeratePersonEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote);
} }
} }

View File

@ -172,6 +172,151 @@ namespace RMuseum.Services.Implementation
} }
} }
/// <summary>
/// submit a suggested edit to an already-approved person's own fields
/// </summary>
/// <param name="suggestion"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonEditSuggestion>> SuggestPersonEditAsync(GanjoorPersonEditSuggestion suggestion)
{
try
{
if (suggestion == null || string.IsNullOrWhiteSpace(suggestion.SuggestedName))
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "نام شخصیت نمی‌تواند خالی باشد.");
}
var personExists = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).AnyAsync();
if (!personExists)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "شخصیت پیدا نشد.");
}
suggestion.Id = 0;
suggestion.Date = DateTime.Now;
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();
suggestion.Reviewed = false;
suggestion.Result = CorrectionReviewResult.NotReviewed;
suggestion.ReviewNote = null;
suggestion.ReviewerUserId = null;
_context.GanjoorPersonEditSuggestions.Add(suggestion);
await _context.SaveChangesAsync();
return new RServiceResult<GanjoorPersonEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, exp.ToString());
}
}
/// <summary>
/// get the next unreviewed person-edit suggestion for the moderator queue
/// </summary>
/// <param name="skip"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorPersonEditSuggestion>> GetNextUnreviewedPersonEditSuggestionAsync(int skip)
{
try
{
var suggestion = await _context.GanjoorPersonEditSuggestions
.Include(s => s.Person)
.Include(s => s.User)
.Include(s => s.SuggestedBirthLocation)
.Include(s => s.SuggestedDeathLocation)
.Where(s => s.Reviewed == false)
.OrderBy(s => s.Id)
.Skip(skip)
.FirstOrDefaultAsync();
return new RServiceResult<GanjoorPersonEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, exp.ToString());
}
}
/// <summary>
/// unreviewed person-edit suggestion count
/// </summary>
/// <returns></returns>
public async Task<RServiceResult<int>> GetUnreviewedPersonEditSuggestionCountAsync()
{
try
{
return new RServiceResult<int>(await _context.GanjoorPersonEditSuggestions.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 person-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<GanjoorPersonEditSuggestion>> ModeratePersonEditSuggestionAsync(Guid moderatorUserId, int suggestionId, CorrectionReviewResult result, string reviewNote)
{
try
{
var suggestion = await _context.GanjoorPersonEditSuggestions.Where(s => s.Id == suggestionId).SingleOrDefaultAsync();
if (suggestion == null)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "پیشنهاد ویرایش پیدا نشد.");
}
if (suggestion.Reviewed)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "این پیشنهاد پیش‌تر بررسی شده است.");
}
if (result == CorrectionReviewResult.Approved)
{
var person = await _context.GanjoorRelatedPersons.Where(p => p.Id == suggestion.PersonId).SingleOrDefaultAsync();
if (person == null)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, "شخصیت مقصد این پیشنهاد پیدا نشد.");
}
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;
suggestion.Result = result;
suggestion.ReviewNote = reviewNote;
suggestion.ReviewDate = DateTime.Now;
suggestion.ReviewerUserId = moderatorUserId;
await _context.SaveChangesAsync();
return new RServiceResult<GanjoorPersonEditSuggestion>(suggestion);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorPersonEditSuggestion>(null, exp.ToString());
}
}
/// <summary> /// <summary>
/// Database Context /// Database Context
/// </summary> /// </summary>