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>@p.FamilyTreeCaption</td>
<td>@s.SuggestedFamilyTreeCaption</td> <td>@s.SuggestedFamilyTreeCaption</td>
</tr> </tr>
<tr>
<td>درجهٔ اهمیت</td>
<td>@Model.ImportanceLabel(p.Importance)</td>
<td>@Model.ImportanceLabel(s.SuggestedImportance)</td>
</tr>
<tr> <tr>
<td>توضیحات</td> <td>توضیحات</td>
<td><small>@p.Description</small></td> <td><small>@p.Description</small></td>

View File

@ -56,6 +56,19 @@ namespace GanjooRazor.Areas.Admin.Pages
return loc == null ? "" : loc.Name; 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() 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

@ -560,7 +560,8 @@
validDeathDate: false, validDeathDate: false,
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()
}; };
var relatedPeople = []; var relatedPeople = [];
@ -852,6 +853,7 @@
$('#geotag-new-person-description').val(''); $('#geotag-new-person-description').val('');
$('#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-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();
@ -2668,6 +2670,15 @@ else
<label for="geotag-new-person-familytreecaption">عنوان تبارنامه (اختیاری، فقط اگر این نامبرده ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label> <label for="geotag-new-person-familytreecaption">عنوان تبارنامه (اختیاری، فقط اگر این نامبرده ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label>
<input type="text" id="geotag-new-person-familytreecaption" /> <input type="text" id="geotag-new-person-familytreecaption" />
</div> </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"> <p class="up-hint">
اگر خویشاوندی یا وابستگی این نامبرده با نامبردۀ دیگری را می‌دانید، می‌توانید همین‌جا اضافه کنید (نامبردۀ دیگر باید از قبل در فهرست موجود باشد؛ اگر خویشاوندی یا وابستگی این نامبرده با نامبردۀ دیگری را می‌دانید، می‌توانید همین‌جا اضافه کنید (نامبردۀ دیگر باید از قبل در فهرست موجود باشد؛

View File

@ -65,6 +65,19 @@ else
</td> </td>
<td><input asp-for="Suggestion.SuggestedFamilyTreeCaption" size="40" /></td> <td><input asp-for="Suggestion.SuggestedFamilyTreeCaption" size="40" /></td>
</tr> </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> <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

@ -89,6 +89,7 @@ namespace GanjooRazor.Areas.User.Pages
SuggestedBirthLocationId = Person.BirthLocationId, SuggestedBirthLocationId = Person.BirthLocationId,
SuggestedDeathLocationId = Person.DeathLocationId, SuggestedDeathLocationId = Person.DeathLocationId,
SuggestedFamilyTreeCaption = Person.FamilyTreeCaption, 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. // self-hosted, dependency-light front-end code over pulling in something like d3-force.
// //
// Data shape (RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphViewModel, camelCased) - // 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 // person2Id, person2Name, category, typeValue, degreeHint, note}] } where category is
// "Relation" (typeValue is PersonRelationType: 0=Parent,1=Sibling,2=Spouse,3=Ancestor) or // "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, // "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 // 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" // 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 // 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 // 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 // call sites with different element-id prefixes and data payloads), so the same renderer serves
@ -87,7 +90,7 @@
var nodesById = {}; var nodesById = {};
var nodes = data.nodes.map(function (n) { var nodes = data.nodes.map(function (n) {
var node = { 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 // absent on the whole-site graph payload (always directly tagged there) - default true
directlyTagged: n.directlyTagged !== false, directlyTagged: n.directlyTagged !== false,
x: W / 2 + (Math.random() - 0.5) * W * 0.6, x: W / 2 + (Math.random() - 0.5) * W * 0.6,
@ -204,8 +207,14 @@
return { el: line, edge: e }; 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) { 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) { var nodeEls = nodes.map(function (n) {

View File

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

@ -78,5 +78,11 @@
/// needs one set). /// needs one set).
/// </summary> /// </summary>
public string FamilyTreeCaption { get; set; } 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. /// as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
/// </summary> /// </summary>
public bool DirectlyTagged { get; set; } = true; 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> /// <summary>

View File

@ -60,6 +60,13 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// to be a tree's named root /// to be a tree's named root
/// </summary> /// </summary>
public string FamilyTreeCaption { get; set; } 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> /// <summary>

View File

@ -9517,6 +9517,11 @@
suggested family tree caption suggested family tree caption
</summary> </summary>
</member> </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"> <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
@ -11317,6 +11322,12 @@
needs one set). needs one set).
</summary> </summary>
</member> </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"> <member name="T:RMuseum.Models.Ganjoor.GanjoorSiteBanner">
<summary> <summary>
ganjoor.net site banner ganjoor.net site banner
@ -11884,6 +11895,29 @@
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.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"> <member name="T:RMuseum.Models.Ganjoor.PersonRelationSuggestionAction">
<summary> <summary>
what a GanjoorPersonRelationEditSuggestion proposes doing to the kinship graph 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. as secondary (e.g. dashed/dimmer) since it's context the reader wasn't shown directly.
</summary> </summary>
</member> </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"> <member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorPersonGraphEdge">
<summary> <summary>
one edge of the people graph - either a kinship edge or a non-family tie, flattened to a 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 to be a tree's named root
</summary> </summary>
</member> </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"> <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

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

View File

@ -904,6 +904,12 @@ namespace RMuseum.Services.Implementation
return new Tuple<int, string>(0, "نام یکی از افراد پیشنهادی وارد نشده است."); 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() var newPerson = new GanjoorRelatedPerson()
{ {
Name = node.Name.Trim(), Name = node.Name.Trim(),
@ -916,6 +922,7 @@ namespace RMuseum.Services.Implementation
BirthLocationId = node.BirthLocationId, BirthLocationId = node.BirthLocationId,
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,
MachineGenerated = false, MachineGenerated = false,
}; };
_context.GanjoorRelatedPersons.Add(newPerson); _context.GanjoorRelatedPersons.Add(newPerson);