people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-10-03 18:05:55 +03:30
parent 00f98bb611
commit fecbf0ec3b
16 changed files with 6862 additions and 6 deletions

View File

@ -113,6 +113,11 @@ else
<td>@Model.ImportanceLabel(p.Importance)</td>
<td>@Model.ImportanceLabel(s.SuggestedImportance)</td>
</tr>
<tr>
<td>جنسیت</td>
<td>@Model.GenderLabel(p.Gender)</td>
<td>@Model.GenderLabel(s.SuggestedGender)</td>
</tr>
<tr>
<td>توضیحات</td>
<td><small>@p.Description</small></td>

View File

@ -69,6 +69,19 @@ namespace GanjooRazor.Areas.Admin.Pages
}
}
public string GenderLabel(PersonGender gender)
{
switch (gender)
{
case PersonGender.Male:
return "مرد";
case PersonGender.Female:
return "زن";
default:
return "نامشخص";
}
}
public IActionResult OnPost()
{
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);

View File

@ -561,7 +561,8 @@
birthLocationId: null,
deathLocationId: null,
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null,
importance: $('#geotag-new-person-importance').val()
importance: $('#geotag-new-person-importance').val(),
gender: $('#geotag-new-person-gender').val()
};
var relatedPeople = [];
@ -854,6 +855,7 @@
$('#geotag-new-person-wikiurl').val('');
$('#geotag-new-person-familytreecaption').val('');
$('#geotag-new-person-importance').val('Normal');
$('#geotag-new-person-gender').val('Unknown');
$('#geotag-person-relatives-list').empty();
if (document.getElementById('new-person-widget').style.display !== 'none') {
toggleNewPersonWidget();
@ -2691,6 +2693,15 @@ else
</select>
<p class="up-hint"><small>فقط اندازهٔ نمایش این نامبرده را در نمودار شخصیت‌ها تعیین می‌کند.</small></p>
</div>
<div class="up-field">
<label for="geotag-new-person-gender">جنسیت</label>
<select id="geotag-new-person-gender">
<option value="Unknown" selected="selected">نامشخص</option>
<option value="Male">مرد</option>
<option value="Female">زن</option>
</select>
<p class="up-hint"><small>فقط در ترسیم نمودار تبارنامه استفاده می‌شود (برای تشخیص اینکه کدام والد به عنوان ادامهٔ اصلی شاخهٔ خانوادگی رسم شود).</small></p>
</div>
<p class="up-hint">
اگر خویشاوندی یا وابستگی این نامبرده با نامبردۀ دیگری را می‌دانید، می‌توانید همین‌جا اضافه کنید (نامبردۀ دیگر باید از قبل در فهرست موجود باشد؛

View File

@ -78,6 +78,19 @@ else
</select>
</td>
</tr>
<tr>
<td>
جنسیت:
<p class="up-hint"><small>صِرفاً در ترسیم نمودار تبارنامه استفاده می‌شود (برای تشخیص اینکه کدام والد به عنوان ادامهٔ اصلی شاخهٔ خانوادگی رسم شود)؛ در هیچ فهرست یا جستجویی تأثیری ندارد.</small></p>
</td>
<td>
<select asp-for="Suggestion.SuggestedGender">
<option value="Unknown">نامشخص</option>
<option value="Male">مرد</option>
<option value="Female">زن</option>
</select>
</td>
</tr>
<tr>
<td>توضیحات:</td>
<td><textarea asp-for="Suggestion.SuggestedDescription" rows="10" cols="60"></textarea></td>

View File

@ -90,6 +90,7 @@ namespace GanjooRazor.Areas.User.Pages
SuggestedDeathLocationId = Person.DeathLocationId,
SuggestedFamilyTreeCaption = Person.FamilyTreeCaption,
SuggestedImportance = Person.Importance,
SuggestedGender = Person.Gender,
};
}

View File

@ -79,8 +79,21 @@
allIds.forEach(function (id) { isRoot[id] = !parentsOf[id] || parentsOf[id].length === 0; });
// one child can have two recorded parents (mother + father) - pick a single "primary"
// parent per child to build a drawable tree, preferring whichever parent is NOT themselves
// a chart-root (so the root-level spouse ends up as the "attached" side, see below)
// parent per child to build a drawable tree. Preference order:
// 1) whichever parent is NOT themselves a chart-root (so the tree keeps following a
// documented lineage instead of dead-ending at a root with no known ancestors) -
// the root-level parent then ends up as the "attached" side, see below;
// 2) if that's a tie (both parents are roots, or both have their own recorded
// ancestors - common in legendary/mythological genealogies where both sides are
// fully documented), prefer the father (PersonGender.Male = 1), matching
// conventional patrilineal genealogy-chart practice;
// 3) if gender doesn't resolve it either (same gender, or neither recorded), fall back
// to ascending person id, just to be deterministic - this last step has no meaning
// of its own and is only a last resort.
function genderRank(id) {
var g = (persons[id] && persons[id].gender) || 0;
return g === 1 ? 0 : (g === 2 ? 1 : 2); // Male first, then Female, then Unknown
}
var primaryParentOf = {};
var secondaryParentOf = {};
allIds.forEach(function (childId) {
@ -90,6 +103,8 @@
var sorted = ps.slice().sort(function (a, b) {
var ar = isRoot[a] ? 1 : 0, br = isRoot[b] ? 1 : 0;
if (ar !== br) return ar - br;
var ag = genderRank(a), bg = genderRank(b);
if (ag !== bg) return ag - bg;
return a - b;
});
primaryParentOf[childId] = sorted[0];
@ -202,6 +217,9 @@
return { w: baseW + bump.w, h: baseH + bump.h };
}
// 0=Normal (regular weight), 1=Important (semi-bold), 2=VeryImportant (bold)
var IMPORTANCE_FONT_WEIGHT = { 0: 400, 1: 600, 2: 700 };
function renderFamilyTree(containerId, svgId, tooltipId, data, requestedRootId) {
var container = document.getElementById(containerId);
var svg = document.getElementById(svgId);
@ -264,8 +282,12 @@
'class': isRequested ? 'ft-box ft-box-current' : 'ft-box'
});
g.appendChild(rect);
// same importance->weight idea as the width/height bump above - Important/VeryImportant
// names are drawn bolder, not just in a bigger box, so they stand out at a glance
var fontWeight = IMPORTANCE_FONT_WEIGHT[(p && p.importance) || 0] || IMPORTANCE_FONT_WEIGHT[0];
var text = el('text', {
x: x, y: y + 5, 'text-anchor': 'middle', 'class': 'ft-box-text', direction: 'rtl'
x: x, y: y + 5, 'text-anchor': 'middle', 'class': 'ft-box-text', direction: 'rtl',
style: 'font-weight:' + fontWeight
});
text.textContent = personLabel(p);
g.appendChild(text);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RMuseum.Migrations
{
/// <inheritdoc />
public partial class AddPersonGender : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Gender",
table: "GanjoorRelatedPersons",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "SuggestedGender",
table: "GanjoorPersonEditSuggestions",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Gender",
table: "GanjoorRelatedPersons");
migrationBuilder.DropColumn(
name: "SuggestedGender",
table: "GanjoorPersonEditSuggestions");
}
}
}

View File

@ -1478,6 +1478,9 @@ namespace RMuseum.Migrations
b.Property<bool>("SuggestedForDeletion")
.HasColumnType("bit");
b.Property<int>("SuggestedGender")
.HasColumnType("int");
b.Property<int>("SuggestedImportance")
.HasColumnType("int");
@ -2437,6 +2440,9 @@ namespace RMuseum.Migrations
b.Property<string>("FamilyTreeCaption")
.HasColumnType("nvarchar(max)");
b.Property<int>("Gender")
.HasColumnType("int");
b.Property<int>("Importance")
.HasColumnType("int");

View File

@ -113,6 +113,11 @@ namespace RMuseum.Models.Ganjoor
/// </summary>
public PersonImportance SuggestedImportance { get; set; }
/// <summary>
/// suggested gender (see GanjoorRelatedPerson.Gender)
/// </summary>
public PersonGender SuggestedGender { 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

View File

@ -84,5 +84,12 @@
/// smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
/// </summary>
public PersonImportance Importance { get; set; }
/// <summary>
/// gender - only used by the family-tree chart to pick which parent is drawn as the tree's
/// spine when a child has two recorded parents who both have their own recorded ancestors
/// (see PersonGender and familytree.js's buildLayout()) - defaults to Unknown
/// </summary>
public PersonGender Gender { get; set; }
}
}

View File

@ -0,0 +1,30 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// a GanjoorRelatedPerson's gender - used only by the family-tree chart (familytree.js's
/// buildLayout()) to break ties when a child has two recorded parents and both happen to have
/// their own recorded ancestors (so neither side can be preferred on "continues a documented
/// lineage" grounds alone): the father is preferred as the tree's spine parent, matching
/// conventional patrilineal genealogy-chart practice, with the mother then drawn attached
/// beside him. Never used to filter, rank or otherwise affect query results. New values can be
/// appended safely later (stored as int).
/// </summary>
public enum PersonGender
{
/// <summary>
/// default - not recorded. Falls back to the old ambiguous-tie-break behavior (ascending
/// person id) when it matters.
/// </summary>
Unknown = 0,
/// <summary>
/// male
/// </summary>
Male = 1,
/// <summary>
/// female
/// </summary>
Female = 2,
}
}

View File

@ -67,6 +67,12 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// Null/empty is treated as Normal.
/// </summary>
public string Importance { get; set; }
/// <summary>
/// name of a PersonGender value ("Unknown", "Male", "Female") - same string-enum convention
/// as Importance above. Null/empty is treated as Unknown.
/// </summary>
public string Gender { get; set; }
}
/// <summary>

View File

@ -5010,6 +5010,18 @@
<member name="M:RMuseum.Migrations.AddPersonImportance.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Migrations.AddPersonGender">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonGender.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonGender.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.AddPersonGender.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary>
donation expenditures
@ -9534,6 +9546,11 @@
suggested importance (see GanjoorRelatedPerson.Importance)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedGender">
<summary>
suggested gender (see GanjoorRelatedPerson.Gender)
</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
@ -11340,6 +11357,13 @@
smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorRelatedPerson.Gender">
<summary>
gender - only used by the family-tree chart to pick which parent is drawn as the tree's
spine when a child has two recorded parents who both have their own recorded ancestors
(see PersonGender and familytree.js's buildLayout()) - defaults to Unknown
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner">
<summary>
ganjoor.net site banner
@ -11907,6 +11931,33 @@
doesn't fit any of the above - rely on Note for what the tie actually is
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonGender">
<summary>
a GanjoorRelatedPerson's gender - used only by the family-tree chart (familytree.js's
buildLayout()) to break ties when a child has two recorded parents and both happen to have
their own recorded ancestors (so neither side can be preferred on "continues a documented
lineage" grounds alone): the father is preferred as the tree's spine parent, matching
conventional patrilineal genealogy-chart practice, with the mother then drawn attached
beside him. Never used to filter, rank or otherwise affect query results. New values can be
appended safely later (stored as int).
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonGender.Unknown">
<summary>
default - not recorded. Falls back to the old ambiguous-tie-break behavior (ascending
person id) when it matters.
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonGender.Male">
<summary>
male
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonGender.Female">
<summary>
female
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonImportance">
<summary>
how editorially prominent a GanjoorRelatedPerson is treated as - purely a display/sizing
@ -15122,6 +15173,12 @@
Null/empty is treated as Normal.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.PersonGraphNode.Gender">
<summary>
name of a PersonGender value ("Unknown", "Male", "Female") - same string-enum convention
as Importance above. Null/empty is treated as Unknown.
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.PersonGraphRelationEntry">
<summary>
one edge in a PersonGraphSuggestion, between two nodes referenced by their LocalKey (Person1/

View File

@ -422,6 +422,7 @@ namespace RMuseum.Services.Implementation
person.DeathLocationId = suggestion.SuggestedDeathLocationId;
person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption;
person.Importance = suggestion.SuggestedImportance;
person.Gender = suggestion.SuggestedGender;
// Id and MachineGenerated on the person are intentionally left untouched
}
}

View File

@ -108,7 +108,7 @@ namespace RMuseum.Services.Implementation
if(dbCorrection.SummaryReviewResult == CorrectionReviewResult.Approved)
{
dbCorrection.AffectedThePoem = true;
dbPoem.PoemSummary = moderation.PoemSummary.Replace("ۀ", "هٔ").Replace("ك", "ک");
dbPoem.PoemSummary = moderation.PoemSummary.Replace("ۀ", "هٔ").Replace("ك", "ک").Replace("ي", "ی");
updatePoem = true;
}
}
@ -167,7 +167,11 @@ namespace RMuseum.Services.Implementation
if (dbVerse.VersePositionResult == CorrectionReviewResult.Approved)
poemVerse.VersePosition = (VersePosition)dbVerse.VersePosition;
if (dbVerse.SummaryReviewResult == CorrectionReviewResult.Approved)
poemVerse.CoupletSummary = dbVerse.CoupletSummary;
// dbVerse.CoupletSummary was already run through this same substitution when the
// suggestion was first submitted (see Editor.cshtml.cs's OnPostSaveCorrectionsAsync),
// but re-applying it here too - same as Title/PoemSummary/verse Text above - keeps
// this field consistent even if a suggestion ever arrives through another path.
poemVerse.CoupletSummary = dbVerse.CoupletSummary?.Replace("ۀ", "هٔ").Replace("ك", "ک").Replace("ي", "ی");
if (dbVerse.LanguageReviewResult == CorrectionReviewResult.Approved)
{
if(dbVerse.LanguageId == 1)//fa-IR
@ -910,6 +914,12 @@ namespace RMuseum.Services.Implementation
return new Tuple<int, string>(0, $"درجهٔ اهمیت «{node.Importance}» نامعتبر است.");
}
var gender = PersonGender.Unknown;
if (!string.IsNullOrWhiteSpace(node.Gender) && !Enum.TryParse<PersonGender>(node.Gender, out gender))
{
return new Tuple<int, string>(0, $"جنسیت «{node.Gender}» نامعتبر است.");
}
var newPerson = new GanjoorRelatedPerson()
{
Name = node.Name.Trim(),
@ -923,6 +933,7 @@ namespace RMuseum.Services.Implementation
DeathLocationId = node.DeathLocationId,
FamilyTreeCaption = string.IsNullOrWhiteSpace(node.FamilyTreeCaption) ? null : node.FamilyTreeCaption.Trim(),
Importance = importance,
Gender = gender,
MachineGenerated = false,
};
_context.GanjoorRelatedPersons.Add(newPerson);