people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-28 14:39:47 +03:30
parent 57e6d77df2
commit 5fcf862862
3 changed files with 183 additions and 0 deletions

View File

@ -15,6 +15,9 @@
// full existing-location catalog ({id, name, latitude, longitude}[]), used by the
// search-as-you-type location picker on each geo tag - see setupLocationAutocomplete in bk.js
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
// full existing-people catalog, used by the search-as-you-type person picker on each geo tag -
// see setupPersonAutocomplete in bk.js
var allGeoPeople = @Html.Raw(Model.AllPeopleJson);
async function checkAllNotCorrect() {
var ok = await upConfirm('همه نادرست؟');
@ -53,6 +56,16 @@
fields.style.display = linkedToExisting ? 'none' : '';
}
// same purpose as toggleGeoTagNewLocationFields above, but for a suggested person instead of a
// suggested location
function toggleGeoTagNewPersonFields(geoIndex) {
var hidden = document.getElementById('geotag' + geoIndex + '_person_select');
var fields = document.getElementById('geotag' + geoIndex + '_new_person_fields');
if (!hidden || !fields) return;
var linkedToExisting = hidden.value && hidden.value !== '0';
fields.style.display = linkedToExisting ? 'none' : '';
}
function moderatePoemCorrections(verseCount, correctionId, geoTagCount) {
$('#moderatecorrections').hide();
@ -154,6 +167,8 @@
var geoTagLocationName = [];
var geoTagLatitude = [];
var geoTagLongitude = [];
var geoTagPersonId = [];
var geoTagPersonName = [];
for (var g = 0; g < (geoTagCount || 0); g++) {
var geoElementId = '#geotag' + (g + 1).toString() + '_review_result_Approved';
if ($(geoElementId).length > 0) {
@ -166,6 +181,10 @@
geoTagLocationName.push($('#geotag' + (g + 1).toString() + '_location_name').val() || null);
geoTagLatitude.push($('#geotag' + (g + 1).toString() + '_latitude').val() || null);
geoTagLongitude.push($('#geotag' + (g + 1).toString() + '_longitude').val() || null);
var geoPersonSelect = $('#geotag' + (g + 1).toString() + '_person_select');
geoTagPersonId.push(geoPersonSelect.length > 0 ? geoPersonSelect.val() : '0');
geoTagPersonName.push($('#geotag' + (g + 1).toString() + '_person_name').val() || null);
}
}
@ -193,6 +212,8 @@
pms.geoTagLocationName= geoTagLocationName;
pms.geoTagLatitude= geoTagLatitude;
pms.geoTagLongitude= geoTagLongitude;
pms.geoTagPersonId= geoTagPersonId;
pms.geoTagPersonName= geoTagPersonName;
$.ajax({
type: "POST",
@ -907,11 +928,65 @@ else
}
}
}
@{
RMuseum.Models.Ganjoor.ViewModels.PersonGraphSuggestion personGraph = null;
if (geoTag.PersonId == null && !string.IsNullOrEmpty(geoTag.SuggestedPersonGraphJson))
{
try
{
personGraph = Newtonsoft.Json.JsonConvert.DeserializeObject<RMuseum.Models.Ganjoor.ViewModels.PersonGraphSuggestion>(geoTag.SuggestedPersonGraphJson);
}
catch (Exception) { }
}
}
@if (geoTag.PersonId != null)
{
<text> - فرد: @(geoTag.Person != null ? geoTag.Person.Name : $"فرد #{geoTag.PersonId}")</text>
}
else if (personGraph?.Person != null)
{
<text> - فرد جدید: @personGraph.Person.Name</text>
if (personGraph.Relations != null && personGraph.Relations.Count > 0)
{
<text> (@personGraph.Relations.Count.ToPersianNumbers() رابطهٔ همراه)</text>
}
}
</p>
@if (!string.IsNullOrEmpty(geoTag.SuggestionNote))
{
<p><small>یادداشت پیشنهاد دهنده: @geoTag.SuggestionNote</small></p>
}
@if (personGraph?.Person != null)
{
<p class="up-hint">
<small>
@if (!string.IsNullOrEmpty(personGraph.Person.Description)) { <text>توضیحات: @personGraph.Person.Description<br /></text> }
@if (personGraph.Person.BirthYearInLHijri != 0) { <text>تولد: @personGraph.Person.BirthYearInLHijri @(personGraph.Person.ValidBirthDate ? "(قطعی)" : "(تخمینی)")<br /></text> }
@if (personGraph.Person.DeathYearInLHijri != 0) { <text>وفات: @personGraph.Person.DeathYearInLHijri @(personGraph.Person.ValidDeathDate ? "(قطعی)" : "(تخمینی)")<br /></text> }
@if (!string.IsNullOrEmpty(personGraph.Person.FamilyTreeCaption)) { <text>عنوان شجره‌نامه: @personGraph.Person.FamilyTreeCaption<br /></text> }
@if (personGraph.RelatedPeople != null && personGraph.RelatedPeople.Count > 0)
{
<text>
خویشاوندان/وابستگان همراه این پیشنهاد:
<ul>
@foreach (var relation in personGraph.Relations ?? new List<RMuseum.Models.Ganjoor.ViewModels.PersonGraphRelationEntry>())
{
var otherNode = personGraph.RelatedPeople.Where(p => p.LocalKey == (relation.Person1 == personGraph.Person.LocalKey ? relation.Person2 : relation.Person1)).FirstOrDefault();
var otherName = otherNode != null
? (otherNode.ExistingPersonId != null ? (Model.People ?? new List<GanjoorRelatedPerson>()).Where(p => p.Id == otherNode.ExistingPersonId).Select(p => p.Name).FirstOrDefault() : otherNode.Name)
: null;
<li>
@(relation.Kind == "affiliation" ? relation.AffiliationType : relation.RelationType)
— @otherName
@if (!string.IsNullOrEmpty(relation.Note)) { <text>(@relation.Note)</text> }
</li>
}
</ul>
</text>
}
</small>
</p>
}
</td>
</tr>
<tr>
@ -951,6 +1026,43 @@ else
}
</td>
</tr>
@if (geoTag.PersonId != null || !string.IsNullOrEmpty(geoTag.SuggestedPersonGraphJson))
{
<tr>
<td colspan="2">
<p class="up-hint">
اگر کاربر فرد را اشتباه تشخیص داده (مثلاً فردی که از قبل در فهرست است را به‌عنوان فرد جدید پیشنهاد کرده)، به‌جای رد کردن می‌توانید آن را به فرد موجود متصل کنید؛
برای فرد تازه فقط نام قابل اصلاح است - اصلاح روابط/تاریخ‌ها نیازمند رد کردن و ارسال دوبارهٔ پیشنهاد توسط کاربر است:
</p>
<label for="geotag@(geoIndex)_person_search">اتصال به فرد موجود در فهرست (در صورت وجود اشتباه در تشخیص فرد):</label>
<div class="up-autocomplete-wrap">
<input type="text" id="geotag@(geoIndex)_person_search" autocomplete="off" placeholder="نام فرد را تایپ کنید..."
value="@(geoTag.PersonId != null ? (Model.People ?? new List<GanjoorRelatedPerson>()).Where(p => p.Id == geoTag.PersonId).Select(p => p.Name).FirstOrDefault() : null)" />
<input type="hidden" id="geotag@(geoIndex)_person_select" name="geotag@(geoIndex)_person_select" value="@(geoTag.PersonId ?? 0)" />
<div id="geotag@(geoIndex)_person_results" class="up-autocomplete-results" style="display:none;"></div>
</div>
<p class="up-hint">برای اتصال به یک فرد موجود، نام او را تایپ کنید و از فهرست انتخاب کنید؛ برای رها کردن اتصال و پیشنهاد به‌عنوان فرد جدید، این فیلد را خالی کنید.</p>
<script>
setupPersonAutocomplete(
document.getElementById('geotag@(geoIndex)_person_search'),
document.getElementById('geotag@(geoIndex)_person_select'),
document.getElementById('geotag@(geoIndex)_person_results'),
allGeoPeople,
function (person) { toggleGeoTagNewPersonFields(@geoIndex); },
function (typedText) { toggleGeoTagNewPersonFields(@geoIndex); }
);
</script>
@if (geoTag.PersonId == null)
{
<div id="geotag@(geoIndex)_new_person_fields">
<br>
<label for="geotag@(geoIndex)_person_name">نام اصلاح‌شدهٔ فرد:</label>
<input type="text" id="geotag@(geoIndex)_person_name" name="geotag@(geoIndex)_person_name" size="30" />
</div>
}
</td>
</tr>
}
}
<tr>
<td colspan="2">

View File

@ -79,6 +79,24 @@ namespace GanjooRazor.Areas.Admin.Pages
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
);
/// <summary>
/// full people catalog, offered as an alternative to a user's new-person suggestion so the
/// moderator can correct a duplicate/misidentified suggestion by linking it to the existing
/// entry instead - same purpose as Locations above
/// </summary>
public List<GanjoorRelatedPerson> People { get; set; }
/// <summary>
/// camelCase JSON of People (id/name/birthYearInLHijri/deathYearInLHijri only), used
/// client-side for the search-as-you-type person picker on each geo tag (see
/// setupPersonAutocomplete in bk.js) - same shape/purpose as Editor.cshtml.cs's AllPeopleJson
/// </summary>
public string AllPeopleJson =>
JsonConvert.SerializeObject(
(People ?? new List<GanjoorRelatedPerson>()).Select(p => new { p.Id, p.Name, p.BirthYearInLHijri, p.DeathYearInLHijri }),
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
);
/// <summary>
/// groups verses into couplets - same logic as SuggestQuoted.cshtml.cs's/Editor.cshtml.cs's GetCouplets,
/// duplicated here rather than shared, so this page doesn't take on a cross-file dependency on those
@ -204,6 +222,16 @@ namespace GanjooRazor.Areas.Admin.Pages
Locations = new List<GanjoorGeoLocation>();
Locations.Add(new GanjoorGeoLocation() { Id = 0, Latitude = 0, Longitude = 0, Name = "" });
Locations.AddRange(JsonConvert.DeserializeObject<GanjoorGeoLocation[]>(await responseLocations.Content.ReadAsStringAsync()));
var responsePeople = await secureClient.GetAsync($"{APIRoot.Url}/api/people");
if (!responsePeople.IsSuccessStatusCode)
{
FatalError = JsonConvert.DeserializeObject<string>(await responsePeople.Content.ReadAsStringAsync());
return Page();
}
People = new List<GanjoorRelatedPerson>();
People.Add(new GanjoorRelatedPerson() { Id = 0, Name = "" });
People.AddRange(JsonConvert.DeserializeObject<GanjoorRelatedPerson[]>(await responsePeople.Content.ReadAsStringAsync()));
}
if (PageInformation.Poem.Sections.Where(s => s.SectionType == PoemSectionType.WholePoem && !string.IsNullOrEmpty(s.RhymeLetters)).Any())
@ -456,6 +484,37 @@ namespace GanjooRazor.Areas.Admin.Pages
Correction.GeoDateTags[i].SuggestedLongitude = correctedLng;
}
}
// same pattern as the location correction above, but for a suggested person
// instead of a suggested location - let the moderator link a suggestion to an
// existing catalog person (e.g. the contributor didn't find them in the search),
// or just fix a typo in a brand new person's name, before approving
string correctedPersonIdText = (pms.geoTagPersonId != null && i < pms.geoTagPersonId.Length) ? pms.geoTagPersonId[i] : null;
if (!string.IsNullOrWhiteSpace(correctedPersonIdText) && int.TryParse(correctedPersonIdText, out int correctedPersonId) && correctedPersonId > 0)
{
Correction.GeoDateTags[i].PersonId = correctedPersonId;
Correction.GeoDateTags[i].SuggestedPersonGraphJson = null;
}
else if (Correction.GeoDateTags[i].PersonId == null && !string.IsNullOrWhiteSpace(Correction.GeoDateTags[i].SuggestedPersonGraphJson))
{
string correctedPersonName = (pms.geoTagPersonName != null && i < pms.geoTagPersonName.Length) ? pms.geoTagPersonName[i] : null;
if (!string.IsNullOrWhiteSpace(correctedPersonName))
{
try
{
var personGraph = JObject.Parse(Correction.GeoDateTags[i].SuggestedPersonGraphJson);
if (personGraph["person"] != null)
{
personGraph["person"]["name"] = correctedPersonName.Trim();
Correction.GeoDateTags[i].SuggestedPersonGraphJson = personGraph.ToString(Newtonsoft.Json.Formatting.None);
}
}
catch (Exception)
{
return new BadRequestObjectResult("برچسب فرد پیشنهادی قابل تفسیر نیست.");
}
}
}
}
Correction.GeoDateTags[i].Result = (CorrectionReviewResult)Enum.Parse(typeof(CorrectionReviewResult), pms.geoTagReviewResult[i]);

View File

@ -32,5 +32,17 @@
/// </summary>
public string[] geoTagLatitude { get; set; }
public string[] geoTagLongitude { get; set; }
/// <summary>
/// moderator's corrected person for each geo tag, if they picked an existing catalog person
/// instead of what the user suggested (empty/"0" means no change to PersonId) - same
/// purpose as geoTagLocationId above
/// </summary>
public string[] geoTagPersonId { get; set; }
/// <summary>
/// moderator's corrected name for a brand new person suggestion (fixes a typo in the user's
/// suggestion), if any - only the name can be corrected this way; deeper fixes (relations,
/// dates, etc.) require rejecting and asking the contributor to resubmit
/// </summary>
public string[] geoTagPersonName { get; set; }
}
}