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(p.Importance)</td>
<td>@Model.ImportanceLabel(s.SuggestedImportance)</td> <td>@Model.ImportanceLabel(s.SuggestedImportance)</td>
</tr> </tr>
<tr>
<td>جنسیت</td>
<td>@Model.GenderLabel(p.Gender)</td>
<td>@Model.GenderLabel(s.SuggestedGender)</td>
</tr>
<tr> <tr>
<td>توضیحات</td> <td>توضیحات</td>
<td><small>@p.Description</small></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() public IActionResult OnPost()
{ {
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]); Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);

View File

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

View File

@ -78,6 +78,19 @@ else
</select> </select>
</td> </td>
</tr> </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> <tr>
<td>توضیحات:</td> <td>توضیحات:</td>
<td><textarea asp-for="Suggestion.SuggestedDescription" rows="10" cols="60"></textarea></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, SuggestedDeathLocationId = Person.DeathLocationId,
SuggestedFamilyTreeCaption = Person.FamilyTreeCaption, SuggestedFamilyTreeCaption = Person.FamilyTreeCaption,
SuggestedImportance = Person.Importance, SuggestedImportance = Person.Importance,
SuggestedGender = Person.Gender,
}; };
} }

View File

@ -79,8 +79,21 @@
allIds.forEach(function (id) { isRoot[id] = !parentsOf[id] || parentsOf[id].length === 0; }); 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" // 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 // parent per child to build a drawable tree. Preference order:
// a chart-root (so the root-level spouse ends up as the "attached" side, see below) // 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 primaryParentOf = {};
var secondaryParentOf = {}; var secondaryParentOf = {};
allIds.forEach(function (childId) { allIds.forEach(function (childId) {
@ -90,6 +103,8 @@
var sorted = ps.slice().sort(function (a, b) { var sorted = ps.slice().sort(function (a, b) {
var ar = isRoot[a] ? 1 : 0, br = isRoot[b] ? 1 : 0; var ar = isRoot[a] ? 1 : 0, br = isRoot[b] ? 1 : 0;
if (ar !== br) return ar - br; if (ar !== br) return ar - br;
var ag = genderRank(a), bg = genderRank(b);
if (ag !== bg) return ag - bg;
return a - b; return a - b;
}); });
primaryParentOf[childId] = sorted[0]; primaryParentOf[childId] = sorted[0];
@ -202,6 +217,9 @@
return { w: baseW + bump.w, h: baseH + bump.h }; 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) { function renderFamilyTree(containerId, svgId, tooltipId, data, requestedRootId) {
var container = document.getElementById(containerId); var container = document.getElementById(containerId);
var svg = document.getElementById(svgId); var svg = document.getElementById(svgId);
@ -264,8 +282,12 @@
'class': isRequested ? 'ft-box ft-box-current' : 'ft-box' 'class': isRequested ? 'ft-box ft-box-current' : 'ft-box'
}); });
g.appendChild(rect); 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', { 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); text.textContent = personLabel(p);
g.appendChild(text); 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") b.Property<bool>("SuggestedForDeletion")
.HasColumnType("bit"); .HasColumnType("bit");
b.Property<int>("SuggestedGender")
.HasColumnType("int");
b.Property<int>("SuggestedImportance") b.Property<int>("SuggestedImportance")
.HasColumnType("int"); .HasColumnType("int");
@ -2437,6 +2440,9 @@ namespace RMuseum.Migrations
b.Property<string>("FamilyTreeCaption") b.Property<string>("FamilyTreeCaption")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("Gender")
.HasColumnType("int");
b.Property<int>("Importance") b.Property<int>("Importance")
.HasColumnType("int"); .HasColumnType("int");

View File

@ -113,6 +113,11 @@ namespace RMuseum.Models.Ganjoor
/// </summary> /// </summary>
public PersonImportance SuggestedImportance { get; set; } public PersonImportance SuggestedImportance { get; set; }
/// <summary>
/// suggested gender (see GanjoorRelatedPerson.Gender)
/// </summary>
public PersonGender SuggestedGender { get; set; }
/// <summary> /// <summary>
/// true if this suggestion is actually a request to delete the person outright, not to /// 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 /// 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 /// smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
/// </summary> /// </summary>
public PersonImportance Importance { get; set; } 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. /// Null/empty is treated as Normal.
/// </summary> /// </summary>
public string Importance { get; set; } 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> /// <summary>

View File

@ -5010,6 +5010,18 @@
<member name="M:RMuseum.Migrations.AddPersonImportance.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)"> <member name="M:RMuseum.Migrations.AddPersonImportance.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc /> <inheritdoc />
</member> </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"> <member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary> <summary>
donation expenditures donation expenditures
@ -9534,6 +9546,11 @@
suggested importance (see GanjoorRelatedPerson.Importance) suggested importance (see GanjoorRelatedPerson.Importance)
</summary> </summary>
</member> </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"> <member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedForDeletion">
<summary> <summary>
true if this suggestion is actually a request to delete the person outright, not to 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 smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
</summary> </summary>
</member> </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"> <member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner">
<summary> <summary>
ganjoor.net site banner ganjoor.net site banner
@ -11907,6 +11931,33 @@
doesn't fit any of the above - rely on Note for what the tie actually is doesn't fit any of the above - rely on Note for what the tie actually is
</summary> </summary>
</member> </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"> <member name="T:RMuseum.Models.Ganjoor.PersonImportance">
<summary> <summary>
how editorially prominent a GanjoorRelatedPerson is treated as - purely a display/sizing how editorially prominent a GanjoorRelatedPerson is treated as - purely a display/sizing
@ -15122,6 +15173,12 @@
Null/empty is treated as Normal. Null/empty is treated as Normal.
</summary> </summary>
</member> </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"> <member name="T:RMuseum.Models.Ganjoor.ViewModels.PersonGraphRelationEntry">
<summary> <summary>
one edge in a PersonGraphSuggestion, between two nodes referenced by their LocalKey (Person1/ 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.DeathLocationId = suggestion.SuggestedDeathLocationId;
person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption; person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption;
person.Importance = suggestion.SuggestedImportance; person.Importance = suggestion.SuggestedImportance;
person.Gender = suggestion.SuggestedGender;
// Id and MachineGenerated on the person are intentionally left untouched // 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) if(dbCorrection.SummaryReviewResult == CorrectionReviewResult.Approved)
{ {
dbCorrection.AffectedThePoem = true; dbCorrection.AffectedThePoem = true;
dbPoem.PoemSummary = moderation.PoemSummary.Replace("ۀ", "هٔ").Replace("ك", "ک"); dbPoem.PoemSummary = moderation.PoemSummary.Replace("ۀ", "هٔ").Replace("ك", "ک").Replace("ي", "ی");
updatePoem = true; updatePoem = true;
} }
} }
@ -167,7 +167,11 @@ namespace RMuseum.Services.Implementation
if (dbVerse.VersePositionResult == CorrectionReviewResult.Approved) if (dbVerse.VersePositionResult == CorrectionReviewResult.Approved)
poemVerse.VersePosition = (VersePosition)dbVerse.VersePosition; poemVerse.VersePosition = (VersePosition)dbVerse.VersePosition;
if (dbVerse.SummaryReviewResult == CorrectionReviewResult.Approved) 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.LanguageReviewResult == CorrectionReviewResult.Approved)
{ {
if(dbVerse.LanguageId == 1)//fa-IR if(dbVerse.LanguageId == 1)//fa-IR
@ -910,6 +914,12 @@ namespace RMuseum.Services.Implementation
return new Tuple<int, string>(0, $"درجهٔ اهمیت «{node.Importance}» نامعتبر است."); 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() var newPerson = new GanjoorRelatedPerson()
{ {
Name = node.Name.Trim(), Name = node.Name.Trim(),
@ -923,6 +933,7 @@ namespace RMuseum.Services.Implementation
DeathLocationId = node.DeathLocationId, DeathLocationId = node.DeathLocationId,
FamilyTreeCaption = string.IsNullOrWhiteSpace(node.FamilyTreeCaption) ? null : node.FamilyTreeCaption.Trim(), FamilyTreeCaption = string.IsNullOrWhiteSpace(node.FamilyTreeCaption) ? null : node.FamilyTreeCaption.Trim(),
Importance = importance, Importance = importance,
Gender = gender,
MachineGenerated = false, MachineGenerated = false,
}; };
_context.GanjoorRelatedPersons.Add(newPerson); _context.GanjoorRelatedPersons.Add(newPerson);