people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-10-03 08:41:31 +03:30
parent 0c98df8526
commit 4ba7da57b8
14 changed files with 163 additions and 5 deletions

View File

@ -108,6 +108,11 @@ else
<td>@p.FamilyTreeCaption</td>
<td>@s.SuggestedFamilyTreeCaption</td>
</tr>
<tr>
<td>درجهٔ اهمیت</td>
<td>@Model.ImportanceLabel(p.Importance)</td>
<td>@Model.ImportanceLabel(s.SuggestedImportance)</td>
</tr>
<tr>
<td>توضیحات</td>
<td><small>@p.Description</small></td>

View File

@ -56,6 +56,19 @@ namespace GanjooRazor.Areas.Admin.Pages
return loc == null ? "" : loc.Name;
}
public string ImportanceLabel(PersonImportance importance)
{
switch (importance)
{
case PersonImportance.Important:
return "مهم";
case PersonImportance.VeryImportant:
return "بسیار مهم";
default:
return "معمولی";
}
}
public IActionResult OnPost()
{
Skip = string.IsNullOrEmpty(Request.Query["skip"]) ? 0 : int.Parse(Request.Query["skip"]);

View File

@ -560,7 +560,8 @@
validDeathDate: false,
birthLocationId: null,
deathLocationId: null,
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null,
importance: $('#geotag-new-person-importance').val()
};
var relatedPeople = [];
@ -852,6 +853,7 @@
$('#geotag-new-person-description').val('');
$('#geotag-new-person-wikiurl').val('');
$('#geotag-new-person-familytreecaption').val('');
$('#geotag-new-person-importance').val('Normal');
$('#geotag-person-relatives-list').empty();
if (document.getElementById('new-person-widget').style.display !== 'none') {
toggleNewPersonWidget();
@ -2668,6 +2670,15 @@ else
<label for="geotag-new-person-familytreecaption">عنوان تبارنامه (اختیاری، فقط اگر این نامبرده ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label>
<input type="text" id="geotag-new-person-familytreecaption" />
</div>
<div class="up-field">
<label for="geotag-new-person-importance">درجهٔ اهمیت</label>
<select id="geotag-new-person-importance">
<option value="Normal" selected="selected">معمولی</option>
<option value="Important">مهم</option>
<option value="VeryImportant">بسیار مهم</option>
</select>
<p class="up-hint"><small>فقط اندازهٔ نمایش این نامبرده را در نمودار شخصیت‌ها تعیین می‌کند.</small></p>
</div>
<p class="up-hint">
اگر خویشاوندی یا وابستگی این نامبرده با نامبردۀ دیگری را می‌دانید، می‌توانید همین‌جا اضافه کنید (نامبردۀ دیگر باید از قبل در فهرست موجود باشد؛

View File

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

View File

@ -89,6 +89,7 @@ namespace GanjooRazor.Areas.User.Pages
SuggestedBirthLocationId = Person.BirthLocationId,
SuggestedDeathLocationId = Person.DeathLocationId,
SuggestedFamilyTreeCaption = Person.FamilyTreeCaption,
SuggestedImportance = Person.Importance,
};
}

View File

@ -4,14 +4,17 @@
// self-hosted, dependency-light front-end code over pulling in something like d3-force.
//
// Data shape (RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel, camelCased) -
// { nodes: [{id, name, hasFamilyTree, directlyTagged}], edges: [{person1Id, person1Name,
// { nodes: [{id, name, hasFamilyTree, directlyTagged, importance}], edges: [{person1Id, person1Name,
// person2Id, person2Name, category, typeValue, degreeHint, note}] } where category is
// "Relation" (typeValue is PersonRelationType: 0=Parent,1=Sibling,2=Spouse,3=Ancestor) or
// "Affiliation" (typeValue is PersonAffiliationType: 0=Minister,1=Advisor,2=Courtier,3=Patron,
// 4=Ally,5=Rival,6=Servant,7=Companion,8=Successor,9=Panegyrized,10=Satirized,99=Other). directlyTagged is false only on
// the category-scoped graph (GET api/ganjoor/cat/{id}/persongraph, the "characters in this work"
// tab): such a node was pulled in as a one-hop relative/affiliate of someone actually named in the
// work's verses, and is never false on the whole-site graph fed by PeopleExplorer.cshtml.
// work's verses, and is never false on the whole-site graph fed by PeopleExplorer.cshtml. importance
// is PersonImportance (0=Normal,1=Important,2=VeryImportant), an editorial prominence set on the
// person themselves (see Editor.cshtml's new-person widget and SuggestPersonEdit.cshtml) - used
// below, alongside hasFamilyTree and degree, to size a node's circle (see nodeRadius()).
//
// Called with an options object (see PeopleExplorer.cshtml and _PersonGraphPartial.cshtml for two
// call sites with different element-id prefixes and data payloads), so the same renderer serves
@ -87,7 +90,7 @@
var nodesById = {};
var nodes = data.nodes.map(function (n) {
var node = {
id: n.id, name: n.name, hasFamilyTree: n.hasFamilyTree,
id: n.id, name: n.name, hasFamilyTree: n.hasFamilyTree, importance: n.importance || 0,
// absent on the whole-site graph payload (always directly tagged there) - default true
directlyTagged: n.directlyTagged !== false,
x: W / 2 + (Math.random() - 0.5) * W * 0.6,
@ -204,8 +207,14 @@
return { el: line, edge: e };
});
// importance (PersonImportance: 0=Normal,1=Important,2=VeryImportant) is the dominant term -
// a VeryImportant person should read as clearly bigger even with few edges - hasFamilyTree
// adds a smaller, independent bump on top (a tree root is worth noting regardless of its
// own importance), and degree only fine-tunes within that.
var IMPORTANCE_RADIUS_BUMP = { 0: 0, 1: 4, 2: 9 };
function nodeRadius(n) {
return (n.hasFamilyTree ? 10 : 7) + Math.min(6, n.degree * 0.6);
var base = (n.hasFamilyTree ? 10 : 7) + (IMPORTANCE_RADIUS_BUMP[n.importance] || 0);
return base + Math.min(6, n.degree * 0.6);
}
var nodeEls = nodes.map(function (n) {

View File

@ -108,6 +108,11 @@ namespace RMuseum.Models.Ganjoor
/// </summary>
public string SuggestedFamilyTreeCaption { get; set; }
/// <summary>
/// suggested importance (see GanjoorRelatedPerson.Importance)
/// </summary>
public PersonImportance SuggestedImportance { 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

@ -78,5 +78,11 @@
/// needs one set).
/// </summary>
public string FamilyTreeCaption { get; set; }
/// <summary>
/// editorial prominence, used to size this person's node in the people graph larger or
/// smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
/// </summary>
public PersonImportance Importance { get; set; }
}
}

View File

@ -0,0 +1,26 @@
namespace RMuseum.Models.Ganjoor
{
/// <summary>
/// how editorially prominent a GanjoorRelatedPerson is treated as - purely a display/sizing
/// hint (see GanjoorRelatedPerson.Importance, GanjoorPersonGraphNode.Importance and
/// peoplegraph.js's nodeRadius()), never used to filter, rank or otherwise affect query
/// results. New values can be appended safely later (stored as int).
/// </summary>
public enum PersonImportance
{
/// <summary>
/// default - no special prominence, drawn at the normal node size
/// </summary>
Normal = 0,
/// <summary>
/// somewhat prominent (e.g. a well-known minister or local ruler) - drawn larger than Normal
/// </summary>
Important = 1,
/// <summary>
/// most prominent (e.g. a major poet or king) - drawn largest
/// </summary>
VeryImportant = 2,
}
}

View File

@ -54,6 +54,12 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
/// </summary>
public bool DirectlyTagged { get; set; } = true;
/// <summary>
/// numeric value of PersonImportance (0=Normal, 1=Important, 2=VeryImportant) - used
/// client-side to size this node larger or smaller, see peoplegraph.js's nodeRadius()
/// </summary>
public int Importance { get; set; }
}
/// <summary>

View File

@ -60,6 +60,13 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// to be a tree's named root
/// </summary>
public string FamilyTreeCaption { get; set; }
/// <summary>
/// name of a PersonImportance value ("Normal", "Important", "VeryImportant") - same
/// string-enum convention as PersonGraphRelationEntry.RelationType/AffiliationType below.
/// Null/empty is treated as Normal.
/// </summary>
public string Importance { get; set; }
}
/// <summary>

View File

@ -9517,6 +9517,11 @@
suggested family tree caption
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorPersonEditSuggestion.SuggestedImportance">
<summary>
suggested importance (see GanjoorRelatedPerson.Importance)
</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
@ -11317,6 +11322,12 @@
needs one set).
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.GanjoorRelatedPerson.Importance">
<summary>
editorial prominence, used to size this person's node in the people graph larger or
smaller (see PersonImportance and peoplegraph.js's nodeRadius()) - defaults to Normal
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner">
<summary>
ganjoor.net site banner
@ -11884,6 +11895,29 @@
doesn't fit any of the above - rely on Note for what the tie actually is
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonImportance">
<summary>
how editorially prominent a GanjoorRelatedPerson is treated as - purely a display/sizing
hint (see GanjoorRelatedPerson.Importance, GanjoorPersonGraphNode.Importance and
peoplegraph.js's nodeRadius()), 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.PersonImportance.Normal">
<summary>
default - no special prominence, drawn at the normal node size
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonImportance.Important">
<summary>
somewhat prominent (e.g. a well-known minister or local ruler) - drawn larger than Normal
</summary>
</member>
<member name="F:RMuseum.Models.Ganjoor.PersonImportance.VeryImportant">
<summary>
most prominent (e.g. a major poet or king) - drawn largest
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction">
<summary>
what a GanjoorPersonRelationEditSuggestion proposes doing to the kinship graph
@ -13528,6 +13562,12 @@
as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphNode.Importance">
<summary>
numeric value of PersonImportance (0=Normal, 1=Important, 2=VeryImportant) - used
client-side to size this node larger or smaller, see peoplegraph.js's nodeRadius()
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge">
<summary>
one edge of the people graph - either a kinship edge or a non-family tie, flattened to a
@ -15063,6 +15103,13 @@
to be a tree's named root
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.PersonGraphNode.Importance">
<summary>
name of a PersonImportance value ("Normal", "Important", "VeryImportant") - same
string-enum convention as PersonGraphRelationEntry.RelationType/AffiliationType below.
Null/empty is treated as Normal.
</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

@ -421,6 +421,7 @@ namespace RMuseum.Services.Implementation
person.BirthLocationId = suggestion.SuggestedBirthLocationId;
person.DeathLocationId = suggestion.SuggestedDeathLocationId;
person.FamilyTreeCaption = suggestion.SuggestedFamilyTreeCaption;
person.Importance = suggestion.SuggestedImportance;
// Id and MachineGenerated on the person are intentionally left untouched
}
}
@ -1115,6 +1116,7 @@ namespace RMuseum.Services.Implementation
Name = p.Name,
HasFamilyTree = !string.IsNullOrEmpty(p.FamilyTreeCaption),
DirectlyTagged = directlyTaggedIds == null || directlyTaggedIds.Contains(p.Id),
Importance = (int)p.Importance,
}).ToList();
}

View File

@ -904,6 +904,12 @@ namespace RMuseum.Services.Implementation
return new Tuple<int, string>(0, "نام یکی از افراد پیشنهادی وارد نشده است.");
}
var importance = PersonImportance.Normal;
if (!string.IsNullOrWhiteSpace(node.Importance) && !Enum.TryParse<PersonImportance>(node.Importance, out importance))
{
return new Tuple<int, string>(0, $"درجهٔ اهمیت «{node.Importance}» نامعتبر است.");
}
var newPerson = new GanjoorRelatedPerson()
{
Name = node.Name.Trim(),
@ -916,6 +922,7 @@ namespace RMuseum.Services.Implementation
BirthLocationId = node.BirthLocationId,
DeathLocationId = node.DeathLocationId,
FamilyTreeCaption = string.IsNullOrWhiteSpace(node.FamilyTreeCaption) ? null : node.FamilyTreeCaption.Trim(),
Importance = importance,
MachineGenerated = false,
};
_context.GanjoorRelatedPersons.Add(newPerson);