diff --git a/RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs b/RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs index 9cf85865..ac07bc62 100644 --- a/RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs +++ b/RMuseum/Models/Ganjoor/ViewModels/PersonGraphSuggestion.cs @@ -73,6 +73,17 @@ namespace RMuseum.Models.Ganjoor.ViewModels /// as Importance above. Null/empty is treated as Unknown. /// public string Gender { get; set; } + + /// + /// only meaningful when ExistingPersonId is null (a brand new person is being created). + /// Normally, if Name exactly matches an already-approved GanjoorRelatedPerson, + /// _MaterializePersonGraphAsync rejects the submission rather than silently creating a + /// near-duplicate node - the far more common case is a contributor who free-typed a name + /// instead of picking the existing person via the search-as-you-type selector. Set this to + /// true only when the name collision is known/intentional (e.g. two distinct Shahnameh + /// characters sharing a name) to let the new person be created anyway. + /// + public bool ConfirmedNewDespiteNameMatch { get; set; } } /// diff --git a/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs b/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs index 5dc1461b..adb4fb1f 100644 --- a/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs +++ b/RMuseum/Services/Implementation/GanjoorRelatedPersonService.cs @@ -477,6 +477,32 @@ namespace RMuseum.Services.Implementation return null; } + /// + /// affiliation types whose two sides are interchangeable (Person1/Person2 order carries no + /// meaning) - mirrors the convention already baked into SuggestNewPersonRelation.cshtml's + /// dropdown, where these are the only affiliation options with no "_Other"/"_Subject" pair + /// + private static readonly HashSet _symmetricAffiliationTypes = new HashSet() + { + PersonAffiliationType.Ally, + PersonAffiliationType.Rival, + PersonAffiliationType.Companion, + PersonAffiliationType.Contemporary, + }; + + /// + /// true if modifying an affiliation edge from oldType to newType would cross the symmetric/ + /// directional boundary - PersonAffiliationType.Other is excluded on either side, since its + /// direction (if any) is whatever the free-text Note says rather than something the type + /// itself implies, so moving into/out of Other is never treated as crossing the boundary + /// + private static bool _CrossesSymmetricDirectionalBoundary(PersonAffiliationType oldType, PersonAffiliationType newType) + { + if (oldType == PersonAffiliationType.Other || newType == PersonAffiliationType.Other) + return false; + return _symmetricAffiliationTypes.Contains(oldType) != _symmetricAffiliationTypes.Contains(newType); + } + /// /// submit a suggested edit to an already-approved person's own fields /// @@ -839,6 +865,12 @@ namespace RMuseum.Services.Implementation suggestion.SuggestedAffiliationType = existingAffiliation.AffiliationType; suggestion.SuggestedNote = existingAffiliation.Note; } + else if (suggestion.Action == PersonRelationSuggestionAction.Modify && suggestion.SuggestedAffiliationType != null + && _CrossesSymmetricDirectionalBoundary(existingAffiliation.AffiliationType, suggestion.SuggestedAffiliationType.Value)) + { + return new RServiceResult(null, + "تغییر نوع وابستگی بین یک نوع متقارن (مثل هم‌عصر/متحد/رقیب/همراه) و یک نوع جهت‌دار (که در آن یک طرف زیردست/حامی/جانشین/... طرف دیگر است) ممکن نیست، چون جهت صحیح طرف اول و دوم برای نوع تازه معلوم نیست. لطفاً این وابستگی را حذف کرده و یک وابستگی تازه با نوع و جهت درست پیشنهاد دهید."); + } } else { @@ -1038,7 +1070,20 @@ namespace RMuseum.Services.Implementation { return new RServiceResult(null, "وابستگی مورد نظر دیگر وجود ندارد."); } - existing.AffiliationType = suggestion.SuggestedAffiliationType ?? existing.AffiliationType; + var newAffiliationType = suggestion.SuggestedAffiliationType ?? existing.AffiliationType; + if (_CrossesSymmetricDirectionalBoundary(existing.AffiliationType, newAffiliationType)) + { + // Person1Id/Person2Id were fixed when the ORIGINAL (symmetric or + // directional) type was created/approved, and a Modify suggestion + // never lets the submitter re-pick which side is which (see the + // "جهت ... قابل تغییر نیست" hint in SuggestPersonRelationEdit.cshtml) + // - so crossing this boundary would silently keep the old Person1/ + // Person2 assignment under a type whose direction convention no + // longer matches it + return new RServiceResult(null, + "تغییر نوع وابستگی بین یک نوع متقارن (مثل هم‌عصر/متحد/رقیب/همراه) و یک نوع جهت‌دار (که در آن یک طرف زیردست/حامی/جانشین/... طرف دیگر است) ممکن نیست، چون جهت صحیح طرف اول و دوم برای نوع تازه معلوم نیست. لطفاً این وابستگی را حذف کرده و یک وابستگی تازه با نوع و جهت درست پیشنهاد دهید."); + } + existing.AffiliationType = newAffiliationType; existing.Note = suggestion.SuggestedNote; break; } diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-ModeratePoemCorrection.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-ModeratePoemCorrection.cs index 4f8bdf21..d9578f4e 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-ModeratePoemCorrection.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-ModeratePoemCorrection.cs @@ -908,6 +908,24 @@ namespace RMuseum.Services.Implementation return new Tuple(0, "نام یکی از افراد پیشنهادی وارد نشده است."); } + if (!node.ConfirmedNewDespiteNameMatch) + { + // the contributor free-typed a name instead of picking an existing person via + // the search-as-you-type selector (which would have set ExistingPersonId + // instead) - almost always this means they just didn't notice a matching person + // already exists, which would otherwise silently create a near-duplicate node + // with no relation back to the real one + var trimmedName = node.Name.Trim(); + var existingNamesake = await _context.GanjoorRelatedPersons + .Where(p => p.Name == trimmedName) + .FirstOrDefaultAsync(); + if (existingNamesake != null) + { + return new Tuple(0, + $"شخصیتی با نام «{trimmedName}» (کد {existingNamesake.Id}) از قبل ثبت شده است. اگر منظور همان شخصیت است، او را از فهرست جستجو انتخاب کنید نه اینکه نام را تازه تایپ کنید. اگر مطمئنید که این شخصیت دیگری با همین نام است (مثلاً دو شخصیت متفاوت در شاهنامه)، لازم است این موضوع به‌صراحت تأیید شود."); + } + } + var importance = PersonImportance.Normal; if (!string.IsNullOrWhiteSpace(node.Importance) && !Enum.TryParse(node.Importance, out importance)) {