From f281c647dc6031369c22dc83ba4adaea67ff22a7 Mon Sep 17 00:00:00 2001 From: leotsarev Date: Thu, 14 May 2026 18:00:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D1=87=D0=B8=D1=81=D1=82=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CharacterGroupDictionaryBuilder.cs | 16 ++++++ .../Repositories/CharacterRepositoryImpl.cs | 8 +-- .../Repositories/ClaimsRepositoryImpl.cs | 2 +- .../Repositories/PlotRepositoryImpl.cs | 12 +---- .../Repositories/ProjectMetadataRepository.cs | 2 - .../Claims/IClaimsRepository.cs | 3 +- .../ICharacterRepository.cs | 1 - .../IPlotRepository.cs | 2 +- src/JoinRpg.DataModel/Character.cs | 3 +- .../Interfaces/IWorldObject.cs | 1 - .../FieldSaveHelperTest.cs | 9 ++-- src/JoinRpg.Domain/CharacterBulkLoader.cs | 23 +-------- .../CharacterParentGroupExtensions.cs | 16 ++++++ src/JoinRpg.Domain/OrderingExtensions.cs | 14 ------ .../ParentGroupCalculateExtensions.cs | 50 +++++-------------- .../BrokenCharactersFilter.cs | 8 +-- .../Problems/ProblemValidator.cs | 8 +-- src/JoinRpg.Domain/SubscribeExtensions.cs | 12 ----- .../ProjectMetadata/CharacterGroupInfo.cs | 1 + .../ProjectMetadata/ProjectInfo.cs | 36 +++++++++++++ .../Controllers/CharacterController.cs | 4 +- .../Controllers/CharacterListController.cs | 12 ++--- .../Controllers/ClaimListController.cs | 25 ++++++---- .../Controllers/PlotListController.cs | 29 ++++++----- .../Controllers/XGameApi/Builders.cs | 13 +++++ .../XGameApi/CharacterApiController.cs | 5 +- .../Claims/ClaimServiceImpl.cs | 6 +-- .../Claims/SubscribeCalculator.cs | 9 ++-- src/JoinRpg.Services.Impl/PlotServiceImpl.cs | 14 +++--- .../Plots/CharacterPlotViewService.cs | 8 +-- .../Subscribe/SubscribeViewService.cs | 19 ++++--- .../UnifiedGrid/UnifiedGridViewService.cs | 9 ++-- .../Characters/CharacterDetailsViewModel.cs | 6 +-- .../Characters/CharacterGroupLinkViewModel.cs | 10 ++++ .../Characters/CharacterListItemViewModel.cs | 2 +- .../Claims/ClaimViewModel.cs | 2 +- .../Helpers/CharacterAndGroupPrefixer.cs | 2 +- .../Helpers/JoinrpgMarkdownLinkRenderer.cs | 42 ++++++++-------- .../Print/ModelBuilders.cs | 4 +- 39 files changed, 232 insertions(+), 216 deletions(-) create mode 100644 src/JoinRpg.Domain/CharacterParentGroupExtensions.cs diff --git a/src/JoinRpg.Dal.Impl/Repositories/CharacterGroupDictionaryBuilder.cs b/src/JoinRpg.Dal.Impl/Repositories/CharacterGroupDictionaryBuilder.cs index d2bd870f4..b2471420a 100644 --- a/src/JoinRpg.Dal.Impl/Repositories/CharacterGroupDictionaryBuilder.cs +++ b/src/JoinRpg.Dal.Impl/Repositories/CharacterGroupDictionaryBuilder.cs @@ -1,3 +1,5 @@ +using JoinRpg.Helpers; + namespace JoinRpg.Dal.Impl.Repositories; public static class CharacterGroupDictionaryBuilder @@ -29,6 +31,17 @@ public static IReadOnlyDictionary k.Id, group.ChildGroupsOrdering)]; + } + // Кэши для рекурсивных обходов var allChildrenCache = new Dictionary>(); var allParentsCache = new Dictionary>(); @@ -85,6 +98,8 @@ List GetAllParentGroups(int groupId) return list; } + var rootGroupId = project.RootGroup.GetId(); + var dict = new Dictionary(); foreach (var group in project.CharacterGroups) { @@ -100,6 +115,7 @@ List GetAllParentGroups(int groupId) IsActive: group.IsActive, IsPublic: group.IsPublic, IsSpecial: group.IsSpecial, + IsIntresting: !group.IsRoot && group.IsActive && (!group.IsSpecial || group.ParentCharacterGroupIds.Any(parentId => parentId != rootGroupId.Id)), DirectChildGroupIds: childGroupsMap.GetValueOrDefault(group.CharacterGroupId, []), DirectParentGroupIds: parentGroupsMap.GetValueOrDefault(group.CharacterGroupId, []), AllChildGroups: allChildGroups, diff --git a/src/JoinRpg.Dal.Impl/Repositories/CharacterRepositoryImpl.cs b/src/JoinRpg.Dal.Impl/Repositories/CharacterRepositoryImpl.cs index 3fdf4e8d8..fce41efda 100644 --- a/src/JoinRpg.Dal.Impl/Repositories/CharacterRepositoryImpl.cs +++ b/src/JoinRpg.Dal.Impl/Repositories/CharacterRepositoryImpl.cs @@ -1,7 +1,6 @@ using JoinRpg.DataModel.Extensions; using JoinRpg.DomainTypes.Characters; using JoinRpg.DomainTypes.Claims; -using JoinRpg.Helpers; using LinqKit; namespace JoinRpg.Dal.Impl.Repositories; @@ -105,12 +104,7 @@ public async Task GetCharacterViewAsync(int projectId, int charac { IsActive = activeClaimPredicate.Invoke(claim), }).ToListAsync(), - DirectGroups = directGroups, - AllGroups = directGroups - .SelectMany(g => g.FlatTree(group => group.ParentGroupIds._parentCharacterGroupIds.Select(id => allGroups[id]))) - .Where(g => g.IsActive) - .Distinct() - .ToList() + DirectGroups = directGroups }; return view; } diff --git a/src/JoinRpg.Dal.Impl/Repositories/ClaimsRepositoryImpl.cs b/src/JoinRpg.Dal.Impl/Repositories/ClaimsRepositoryImpl.cs index 0d368e554..5bef24e84 100644 --- a/src/JoinRpg.Dal.Impl/Repositories/ClaimsRepositoryImpl.cs +++ b/src/JoinRpg.Dal.Impl/Repositories/ClaimsRepositoryImpl.cs @@ -128,7 +128,7 @@ public async Task GetClaimWithDetails(int projectId, int claimId) .SingleOrDefaultAsync(e => e.ClaimId == claimId && e.ProjectId == projectId); } - public Task> GetClaimsForGroups(int projectId, ClaimStatusSpec active, int[] characterGroupsIds) + public Task> GetClaimsForGroups(ProjectIdentification projectId, ClaimStatusSpec active, CharacterGroupIdentification[] characterGroupsIds) { return GetClaimsImpl(projectId, active, ClaimPredicates.GetInGroupPredicate(characterGroupsIds)); } diff --git a/src/JoinRpg.Dal.Impl/Repositories/PlotRepositoryImpl.cs b/src/JoinRpg.Dal.Impl/Repositories/PlotRepositoryImpl.cs index 663dcc7bb..30e219c8a 100644 --- a/src/JoinRpg.Dal.Impl/Repositories/PlotRepositoryImpl.cs +++ b/src/JoinRpg.Dal.Impl/Repositories/PlotRepositoryImpl.cs @@ -19,22 +19,14 @@ await Ctx.Set() .SingleOrDefaultAsync(pf => pf.PlotFolderId == plotFolderId.PlotFolderId && pf.ProjectId == plotFolderId.ProjectId); } - public async Task> GetPlotsForCharacter(Character character) + public async Task> GetDirectPlotsForCharacter(CharacterIdentification character) { - var ids = - character.Groups.SelectMany(group => group.FlatTree(g => g.ParentGroups)) - .Select(g => g.CharacterGroupId) - .Distinct() - .ToList(); //ToList required here so all lazy loads are finished before we are starting making condition below. return await Ctx.Set() .Include(e => e.Texts) .Include(e => e.TargetCharacters) .Include(e => e.TargetGroups) - .Where( - e => - e.TargetCharacters.Any(ch => ch.CharacterId == character.CharacterId) || - e.TargetGroups.Any(g => ids.Contains(g.CharacterGroupId))) + .Where(e => e.TargetCharacters.Any(ch => ch.CharacterId == character.CharacterId)) .ToListAsync(); } diff --git a/src/JoinRpg.Dal.Impl/Repositories/ProjectMetadataRepository.cs b/src/JoinRpg.Dal.Impl/Repositories/ProjectMetadataRepository.cs index 0a45377a0..c22ce7699 100644 --- a/src/JoinRpg.Dal.Impl/Repositories/ProjectMetadataRepository.cs +++ b/src/JoinRpg.Dal.Impl/Repositories/ProjectMetadataRepository.cs @@ -35,8 +35,6 @@ internal static ProjectInfo CreateInfoFromProject(Project project, ProjectIdenti ProjectLifecycleStatus status = ProjectLoaderCommon.CreateStatus(project.Active, project.IsAcceptingClaims); - - var groups = CharacterGroupDictionaryBuilder.Build(project, projectId); return new ProjectInfo( diff --git a/src/JoinRpg.Data.Interfaces/Claims/IClaimsRepository.cs b/src/JoinRpg.Data.Interfaces/Claims/IClaimsRepository.cs index 6f004187d..f40f61aea 100644 --- a/src/JoinRpg.Data.Interfaces/Claims/IClaimsRepository.cs +++ b/src/JoinRpg.Data.Interfaces/Claims/IClaimsRepository.cs @@ -19,7 +19,8 @@ public interface IClaimsRepository : IDisposable Task GetClaim(ClaimIdentification claimId); [Obsolete] Task GetClaimWithDetails(int projectId, int claimId); - Task> GetClaimsForGroups(int projectId, ClaimStatusSpec active, int[] characterGroupsIds); + + Task> GetClaimsForGroups(ProjectIdentification projectId, ClaimStatusSpec active, CharacterGroupIdentification[] characterGroupsIds); Task> GetClaimHeadersWithPlayer(IReadOnlyCollection characterGroupsIds, ClaimStatusSpec spec); Task> GetClaimsForPlayer(int projectId, ClaimStatusSpec claimStatusSpec, int userId); diff --git a/src/JoinRpg.Data.Interfaces/ICharacterRepository.cs b/src/JoinRpg.Data.Interfaces/ICharacterRepository.cs index 139e9d25e..c83293dd6 100644 --- a/src/JoinRpg.Data.Interfaces/ICharacterRepository.cs +++ b/src/JoinRpg.Data.Interfaces/ICharacterRepository.cs @@ -41,7 +41,6 @@ public class CharacterView : IFieldContainter public Claim? ApprovedClaim { get; set; } public required IReadOnlyCollection Claims { get; set; } public required IReadOnlyCollection DirectGroups { get; set; } - public required IReadOnlyCollection AllGroups { get; set; } public required string JsonData { get; set; } public required string Name { get; set; } public required string Description { get; set; } diff --git a/src/JoinRpg.Data.Interfaces/IPlotRepository.cs b/src/JoinRpg.Data.Interfaces/IPlotRepository.cs index 58db15d37..266352e66 100644 --- a/src/JoinRpg.Data.Interfaces/IPlotRepository.cs +++ b/src/JoinRpg.Data.Interfaces/IPlotRepository.cs @@ -9,7 +9,7 @@ public interface IPlotRepository : IDisposable Task> GetPlots(ProjectIdentification projectId); Task GetPlotFolderAsync(PlotFolderIdentification plotFolderId); - Task> GetPlotsForCharacter(Character character); + Task> GetDirectPlotsForCharacter(CharacterIdentification characterId); Task> GetPlotsWithTargetAndText(int projectid); [Obsolete] diff --git a/src/JoinRpg.DataModel/Character.cs b/src/JoinRpg.DataModel/Character.cs index 22ebf49aa..2b4812095 100644 --- a/src/JoinRpg.DataModel/Character.cs +++ b/src/JoinRpg.DataModel/Character.cs @@ -22,8 +22,7 @@ public int[] ParentCharacterGroupIds public virtual Project Project { get; set; } - IEnumerable IWorldObject.ParentGroups => Groups; - + [Obsolete("Use groups from ProjectInfo")] public IEnumerable Groups => Project.CharacterGroups.Where(c => ParentCharacterGroupIds.Contains(c.CharacterGroupId)); public string CharacterName { get; set; } diff --git a/src/JoinRpg.DataModel/Interfaces/IWorldObject.cs b/src/JoinRpg.DataModel/Interfaces/IWorldObject.cs index 3aabea0f0..c20cd68e1 100644 --- a/src/JoinRpg.DataModel/Interfaces/IWorldObject.cs +++ b/src/JoinRpg.DataModel/Interfaces/IWorldObject.cs @@ -2,7 +2,6 @@ namespace JoinRpg.DataModel; public interface IWorldObject : IProjectEntity, ILinkable { - IEnumerable ParentGroups { get; } string Name { get; } bool IsPublic { get; } diff --git a/src/JoinRpg.Domain.Test/FieldSaveHelperTest.cs b/src/JoinRpg.Domain.Test/FieldSaveHelperTest.cs index dd4e14520..eaf556ffa 100644 --- a/src/JoinRpg.Domain.Test/FieldSaveHelperTest.cs +++ b/src/JoinRpg.Domain.Test/FieldSaveHelperTest.cs @@ -43,8 +43,8 @@ public void SaveOnAddTest() .ShouldBe(_original.Character.JsonData, "Adding claim should not modify any character fields"); - mock.Character.Groups.Select(g => g.CharacterGroupId).ShouldBe( - mock.Character.Groups.Select(g => g.CharacterGroupId), + mock.Character.ParentCharacterGroupIds.ShouldBe( + mock.Character.ParentCharacterGroupIds, "Adding claim should not modify any character groups"); claim.JsonData.ShouldBe($"{{\"{mock.CharacterFieldInfo.Id.ProjectFieldId}\":\"test\"}}"); @@ -212,10 +212,7 @@ public void DisableUnapprovedClaimToChangeCharacterTest() ShouldBeTestExtensions.ShouldBe(mock.Character.JsonData, _original.Character.JsonData, "Adding claim should not modify any character fields"); - mock.Character.Groups.Select(g => g.CharacterGroupId).ToList().ShouldBe( - (IEnumerable)_original.Character.Groups.Select(g => g.CharacterGroupId) - .ToList(), - "Adding claim should not modify any character groups"); + mock.Character.ParentCharacterGroupIds.ToList().ShouldBe(_original.Character.ParentCharacterGroupIds.ToList(), "Adding claim should not modify any character groups"); ShouldBeTestExtensions.ShouldBe(claim.JsonData, $"{{\"{mock.CharacterFieldInfo.Id.ProjectFieldId}\":\"test\"}}"); } diff --git a/src/JoinRpg.Domain/CharacterBulkLoader.cs b/src/JoinRpg.Domain/CharacterBulkLoader.cs index ac39de520..fcb2ebd35 100644 --- a/src/JoinRpg.Domain/CharacterBulkLoader.cs +++ b/src/JoinRpg.Domain/CharacterBulkLoader.cs @@ -1,38 +1,19 @@ - -using JoinRpg.Helpers; - namespace JoinRpg.Domain; public class CharacterBulkLoader { - private readonly Dictionary parentGroupsCache = []; private readonly Dictionary characterCache = []; - public CharacterItem LoadCharacter(Character character) + public CharacterItem LoadCharacter(Character character, ProjectInfo projectInfo) { if (characterCache.TryGetValue(character.CharacterId, out var item)) { return item; } - var directParents = character.ParentCharacterGroupIds.Select(c => new CharacterGroupIdentification(character.ProjectId, c)).ToList(); - var allParents = directParents.SelectMany(g => ResolveGroupsToTop(character.Project, g)).ToList(); - var result = new CharacterItem(character, allParents); + var result = new CharacterItem(character, [.. character.GetParentGroupIdsToTop(projectInfo)]); characterCache.Add(character.CharacterId, result); return result; } - - private CharacterGroupIdentification[] ResolveGroupsToTop(Project project, CharacterGroupIdentification groupId) - { - if (parentGroupsCache.TryGetValue(groupId, out var groups)) - { - return groups; - } - var entity = project.CharacterGroups.Single(g => g.CharacterGroupId == groupId.CharacterGroupId); - var all = entity.FlatTree(e => e.ParentGroups); - var value = all.Select(e => new CharacterGroupIdentification(e.ProjectId, e.CharacterGroupId)).ToArray(); - parentGroupsCache.Add(groupId, value); - return value; - } } public record class CharacterItem(Character Character, IReadOnlyCollection ParentGroups); diff --git a/src/JoinRpg.Domain/CharacterParentGroupExtensions.cs b/src/JoinRpg.Domain/CharacterParentGroupExtensions.cs new file mode 100644 index 000000000..47f43df07 --- /dev/null +++ b/src/JoinRpg.Domain/CharacterParentGroupExtensions.cs @@ -0,0 +1,16 @@ +namespace JoinRpg.Domain; + +public static class CharacterParentGroupExtensions +{ + public static IEnumerable GetParentGroupIdsToTop(this Character target, ProjectInfo projectInfo) + => projectInfo.GetParentGroupIdsIncludingThis(target.GetDirectGroupIds()); + + public static IEnumerable GetDirectGroupIds(this Character target) + => CharacterGroupIdentification.FromList(target.ParentCharacterGroupIds, new(target.ProjectId)); + + public static IEnumerable GetParentGroupsToTop(this Character target, ProjectInfo projectInfo) + => projectInfo.GetParentGroupsIncludingThis(target.GetDirectGroupIds()); + + public static IEnumerable GetIntrestingGroupsForDisplayToTop(this Character character, ProjectInfo projectInfo) + => character.GetParentGroupsToTop(projectInfo).Where(g => g.IsIntresting); +} diff --git a/src/JoinRpg.Domain/OrderingExtensions.cs b/src/JoinRpg.Domain/OrderingExtensions.cs index 35e5753ec..09cc9bee6 100644 --- a/src/JoinRpg.Domain/OrderingExtensions.cs +++ b/src/JoinRpg.Domain/OrderingExtensions.cs @@ -15,18 +15,9 @@ public static IReadOnlyList GetOrderedChildGroups(this Character public static VirtualOrderContainer GetCharacterGroupsContainer(this CharacterGroup characterGroup) => VirtualOrderContainerFacade.Create(characterGroup.ChildGroups, characterGroup.ChildGroupsOrdering); - public static IReadOnlyList GetOrderedPlots(this Character character, IReadOnlyCollection elements) - => character.GetCharacterPlotContainer(elements).OrderedItems; - - public static IReadOnlyList GetOrderedPlots(this Character character, IReadOnlyCollection elements) - => character.GetCharacterPlotContainer(elements).OrderedItems; - public static VirtualOrderContainer GetCharacterPlotContainer(this Character character, IReadOnlyCollection plots) => VirtualOrderContainerFacade.Create(plots.OrderBy(pe => pe.PlotFolderId), character.PlotElementOrderData, preserveOrder: true); - public static VirtualOrderContainer GetCharacterPlotContainer(this Character character, - IReadOnlyCollection plots) => VirtualOrderContainerFacade.Create(plots, character.PlotElementOrderData, preserveOrder: true); - public static IReadOnlyList GetOrderedValues(this ProjectField field) => field.GetFieldValuesContainer().OrderedItems; @@ -43,9 +34,4 @@ public static VirtualOrderContainer GetFieldsContainer( public static VirtualOrderContainer GetPlotFoldersContainer(this Project field) => VirtualOrderContainerFacade.Create(field.PlotFolders, field.Details.PlotFoldersOrdering); - - public static IReadOnlyList GetOrderedPlotFolders(this Project field) => field.GetPlotFoldersContainer().OrderedItems; - - public static VirtualOrderContainer GetPlotElementsContainer(this PlotFolder field) - => VirtualOrderContainerFacade.Create(field.Elements, field.ElementsOrdering); } diff --git a/src/JoinRpg.Domain/ParentGroupCalculateExtensions.cs b/src/JoinRpg.Domain/ParentGroupCalculateExtensions.cs index 34d366934..c0756ae98 100644 --- a/src/JoinRpg.Domain/ParentGroupCalculateExtensions.cs +++ b/src/JoinRpg.Domain/ParentGroupCalculateExtensions.cs @@ -4,53 +4,29 @@ namespace JoinRpg.Domain; public static class ParentGroupCalculateExtensions { - public static IEnumerable GetParentGroupsToTop(this IWorldObject? target) + [Obsolete("Pass ProjectInfo")] + public static IEnumerable GetParentGroupsToTop(this CharacterGroup? target) { return target?.ParentGroups.SelectMany(g => g.FlatTree(gr => gr.ParentGroups)) .OrderBy(g => g.CharacterGroupId) .Distinct() ?? []; } - public static IReadOnlyCollection GetParentGroupIdsToTop(this IWorldObject? target) + [Obsolete("Pass ProjectInfo")] + public static IEnumerable GetParentGroupsToTop(this Character? target) + { + return target?.Groups.SelectMany(g => g.FlatTree(gr => gr.ParentGroups)) + .OrderBy(g => g.CharacterGroupId) + .Distinct() ?? []; + } + + [Obsolete("Pass ProjectInfo")] + public static IReadOnlyCollection GetParentGroupIdsToTop(this Character? target) { if (target == null) { return []; } - return [.. target.ParentGroups.SelectMany(g => g.FlatTree(gr => gr.ParentGroups)).Select(x => x.GetId()).Order().Distinct()]; - } - - public static IEnumerable GetIntrestingGroupsForDisplayToTop(this Character character) - => character.GetParentGroupsToTop().Where(g => !g.IsRoot && g.IsActive && (!g.IsSpecial || g.ParentGroups.All(g => !g.IsRoot))); - - public static IEnumerable GetChildrenGroupsRecursive( - this CharacterGroup target) - { - ArgumentNullException.ThrowIfNull(target); - - return target.ChildGroups.SelectMany(g => g.FlatTree(gr => gr.ChildGroups)).Distinct(); - } - - [Obsolete] - public static int[] GetChildrenGroupsIdRecursiveIncludingThis(this CharacterGroup target) - { - ArgumentNullException.ThrowIfNull(target); - - return [.. target.GetChildrenGroupsRecursive().Select(g => g.CharacterGroupId), target.CharacterGroupId]; - } - - public static IEnumerable GetChildrenGroupsIdentificationRecursiveIncludingThis(this CharacterGroup target) - { - ArgumentNullException.ThrowIfNull(target); - - return [.. target.GetChildrenGroupsRecursive().Select(g => g.GetId()), target.GetId()]; - } - - public static IEnumerable GetOrderedChildrenGroupsRecursive( - this CharacterGroup target) - { - ArgumentNullException.ThrowIfNull(target); - - return target.GetOrderedChildGroups().SelectMany(g => g.FlatTree(gr => gr.GetOrderedChildGroups())).Distinct(); + return [.. target.Groups.SelectMany(g => g.FlatTree(gr => gr.ParentGroups)).Select(x => x.GetId()).Order().Distinct()]; } } diff --git a/src/JoinRpg.Domain/Problems/CharacterProblemFilters/BrokenCharactersFilter.cs b/src/JoinRpg.Domain/Problems/CharacterProblemFilters/BrokenCharactersFilter.cs index 4ac028b87..6b535c4ed 100644 --- a/src/JoinRpg.Domain/Problems/CharacterProblemFilters/BrokenCharactersFilter.cs +++ b/src/JoinRpg.Domain/Problems/CharacterProblemFilters/BrokenCharactersFilter.cs @@ -4,7 +4,7 @@ internal class BrokenCharactersFilter : IProblemFilter { public IEnumerable GetProblems(Character character, ProjectInfo projectInfo) { - var groups = character.GetParentGroupsToTop().Where(g => g.IsActive && !g.IsSpecial).ToArray(); + var groups = character.GetParentGroupsToTop(projectInfo).Where(g => g.IsActive && !g.IsSpecial).ToArray(); if (!groups.Any()) { yield return new ClaimProblem(ClaimProblemType.NoParentGroup, ProblemSeverity.Fatal); @@ -15,15 +15,15 @@ public IEnumerable GetProblems(Character character, ProjectInfo pr } } - private IEnumerable GetProblemFroGroup(CharacterGroup group) + private IEnumerable GetProblemFroGroup(CharacterGroupInfo group) { if (group.IsRoot) { yield break; } - if (!group.ParentCharacterGroupIds.Any() || group.ParentCharacterGroupIds.Any(id => id == group.CharacterGroupId)) + if (!group.DirectParentGroupIds.Any() || group.DirectParentGroupIds.Any(id => id == group.Id)) { - yield return new ClaimProblem(ClaimProblemType.GroupIsBroken, ProblemSeverity.Fatal, group.CharacterGroupName); + yield return new ClaimProblem(ClaimProblemType.GroupIsBroken, ProblemSeverity.Fatal, group.Name); } } } diff --git a/src/JoinRpg.Domain/Problems/ProblemValidator.cs b/src/JoinRpg.Domain/Problems/ProblemValidator.cs index 4c153a1e8..fdb744202 100644 --- a/src/JoinRpg.Domain/Problems/ProblemValidator.cs +++ b/src/JoinRpg.Domain/Problems/ProblemValidator.cs @@ -27,7 +27,7 @@ public IEnumerable ValidateFieldsOnly(TObject obj, ProjectI FieldWithValue[] fieldWithValues = GetFields(obj, projectInfo).Where(f => fields.Contains(f.Field.Id)).ToArray(); - return ValidateFieldsInternal(obj, fieldWithValues); + return ValidateFieldsInternal(obj, fieldWithValues, projectInfo); } public IEnumerable ValidateFieldsOnly(TObject obj, ProjectInfo projectInfo) @@ -36,12 +36,12 @@ public IEnumerable ValidateFieldsOnly(TObject obj, ProjectI ArgumentNullException.ThrowIfNull(projectInfo); FieldWithValue[] fieldWithValues = GetFields(obj, projectInfo); - return ValidateFieldsInternal(obj, fieldWithValues); + return ValidateFieldsInternal(obj, fieldWithValues, projectInfo); } - private IEnumerable ValidateFieldsInternal(TObject obj, FieldWithValue[] fieldWithValues) + private IEnumerable ValidateFieldsInternal(TObject obj, FieldWithValue[] fieldWithValues, ProjectInfo projectInfo) { - var target = characterBulkLoader.LoadCharacter(GetClaimSource(obj)); + var target = characterBulkLoader.LoadCharacter(GetClaimSource(obj), projectInfo); foreach (var fieldWithValue in fieldWithValues) { diff --git a/src/JoinRpg.Domain/SubscribeExtensions.cs b/src/JoinRpg.Domain/SubscribeExtensions.cs index f048bcfd5..49dc274db 100644 --- a/src/JoinRpg.Domain/SubscribeExtensions.cs +++ b/src/JoinRpg.Domain/SubscribeExtensions.cs @@ -4,18 +4,6 @@ namespace JoinRpg.Domain; public static class SubscribeExtensions { - public static IEnumerable GetSubscriptions( - this ForumThread forumThread, - IEnumerable? extraRecipients, - bool isVisibleToPlayer) - { - return - forumThread.Subscriptions //get subscriptions on forum - .Select(u => u.User) //Select users - .Union(extraRecipients ?? Enumerable.Empty()) //add extra recipients - .VerifySubscriptions(isVisibleToPlayer, forumThread); - } - public static IEnumerable GetSubscriptions( this Character character, Func predicate, diff --git a/src/JoinRpg.DomainTypes/ProjectMetadata/CharacterGroupInfo.cs b/src/JoinRpg.DomainTypes/ProjectMetadata/CharacterGroupInfo.cs index 1a3823c2d..ae61521e6 100644 --- a/src/JoinRpg.DomainTypes/ProjectMetadata/CharacterGroupInfo.cs +++ b/src/JoinRpg.DomainTypes/ProjectMetadata/CharacterGroupInfo.cs @@ -7,6 +7,7 @@ public record CharacterGroupInfo( bool IsActive, bool IsPublic, bool IsSpecial, + bool IsIntresting, IReadOnlyCollection DirectChildGroupIds, IReadOnlyCollection DirectParentGroupIds, IReadOnlyCollection AllChildGroups, diff --git a/src/JoinRpg.DomainTypes/ProjectMetadata/ProjectInfo.cs b/src/JoinRpg.DomainTypes/ProjectMetadata/ProjectInfo.cs index c1b92a2b2..2aae350ea 100644 --- a/src/JoinRpg.DomainTypes/ProjectMetadata/ProjectInfo.cs +++ b/src/JoinRpg.DomainTypes/ProjectMetadata/ProjectInfo.cs @@ -156,6 +156,42 @@ internal ProjectInfo WithAllowManyClaims(bool strictlyOneCharacter) ProjectScheduleSettings, CloneSettings, CreateDate, ProfileRequirementSettings, ClaimSettings with { StrictlyOneCharacter = strictlyOneCharacter }, ProjectRolesLists, Groups); } + + public IEnumerable GetChildGroupIdsIncludingThis(CharacterGroupIdentification groupId) + { + if (!Groups.TryGetValue(groupId, out var groupInfo)) + { + return []; + } + + return [groupId, .. groupInfo.AllChildGroups]; + } + + public IEnumerable GetChildGroupIdsIncludingThis(IEnumerable groupIds) + { + return [.. groupIds.SelectMany(x => GetChildGroupIdsIncludingThis(x)).Distinct()]; + } + + public IEnumerable GetParentGroupIdsIncludingThis(CharacterGroupIdentification groupId) + { + if (!Groups.TryGetValue(groupId, out var groupInfo)) + { + return []; + } + + return [groupId, .. groupInfo.AllParentGroups]; + } + + public IEnumerable GetParentGroupIdsIncludingThis(IEnumerable groupIds) + { + return [.. groupIds.SelectMany(x => GetParentGroupIdsIncludingThis(x)).Distinct()]; + } + + public IEnumerable GetParentGroupsIncludingThis(IEnumerable groupIds) + { + var ids = GetParentGroupIdsIncludingThis(groupIds); + return ids.Select(id => Groups[id]); + } } public record ProjectProfileRequirementSettings( diff --git a/src/JoinRpg.Portal/Controllers/CharacterController.cs b/src/JoinRpg.Portal/Controllers/CharacterController.cs index 831c63f98..dc6d58c40 100644 --- a/src/JoinRpg.Portal/Controllers/CharacterController.cs +++ b/src/JoinRpg.Portal/Controllers/CharacterController.cs @@ -1,7 +1,7 @@ using Joinrpg.AspNetCore.Helpers; -using JoinRpg.Dal.Impl.Repositories; using JoinRpg.Data.Interfaces; using JoinRpg.DataModel; +using JoinRpg.Domain; using JoinRpg.Domain.Access; using JoinRpg.DomainTypes.Characters; using JoinRpg.Interfaces; @@ -70,7 +70,7 @@ public async Task Edit(ProjectIdentification projectId, int charac ProjectName = field.Project.ProjectName, CharacterTypeInfo = view.CharacterTypeInfo, Name = field.CharacterName, - ParentCharacterGroupIds = CharacterGroupIdentification.FromList(field.Groups.Where(gr => !gr.IsSpecial).Select(pg => pg.CharacterGroupId).ToArray(), projectId).ToArray(), + ParentCharacterGroupIds = [.. field.GetParentGroupsToTop(projectInfo).Where(gr => !gr.IsSpecial).Select(pg => pg.Id)], }.Fill(field, CurrentUserId, projectInfo)); } diff --git a/src/JoinRpg.Portal/Controllers/CharacterListController.cs b/src/JoinRpg.Portal/Controllers/CharacterListController.cs index d7cb72235..526659640 100644 --- a/src/JoinRpg.Portal/Controllers/CharacterListController.cs +++ b/src/JoinRpg.Portal/Controllers/CharacterListController.cs @@ -69,20 +69,20 @@ private async Task MasterCharacterList(ProjectIdentification proje return Export(list, exportType.Value, projectInfo); } - [HttpGet("~/{ProjectId}/characters/bygroup/{CharacterGroupId}")] - public async Task ByGroup(ProjectIdentification projectId, int characterGroupId, string export) + [HttpGet("~/{ProjectId}/characters/bygroup/{characterGroupIdInt}")] + public async Task ByGroup(ProjectIdentification projectId, int characterGroupIdInt, string export) { - var characterGroup = await projectRepository.GetGroupAsync(projectId, characterGroupId); + var characterGroupId = new CharacterGroupIdentification(projectId, characterGroupIdInt); + var characterGroup = await projectRepository.GetGroupAsync(characterGroupId); if (characterGroup == null) { return NotFound(); } - var groupIds = characterGroup.GetChildrenGroupsIdentificationRecursiveIncludingThis().ToList(); - var characters = (await projectRepository.GetCharacterByGroups(groupIds)).Where(ch => ch.IsActive).ToList(); - var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); + var groupIds = projectInfo.GetChildGroupIdsIncludingThis(characterGroupId).ToList(); + var characters = (await projectRepository.GetCharacterByGroups(groupIds)).Where(ch => ch.IsActive).ToList(); var list = new CharacterListByGroupViewModel(currentUserAccessor.UserIdentification, characters, characterGroup, projectInfo, problemValidator); diff --git a/src/JoinRpg.Portal/Controllers/ClaimListController.cs b/src/JoinRpg.Portal/Controllers/ClaimListController.cs index 26e971a9e..efacad108 100644 --- a/src/JoinRpg.Portal/Controllers/ClaimListController.cs +++ b/src/JoinRpg.Portal/Controllers/ClaimListController.cs @@ -154,33 +154,40 @@ public ActionResult ListForGroupDirect(int projectId, int characterGroupId, stri [HttpGet("~/{ProjectId}/roles/{CharacterGroupId}/claims")] [MasterAuthorize()] - public async Task ListForGroup(int projectId, int characterGroupId, string export) + public async Task ListForGroup(ProjectIdentification projectId, int characterGroupId, string export) { - var group = await projectRepository.GetGroupAsync(projectId, characterGroupId); + var characterGroupId2 = new CharacterGroupIdentification(projectId, characterGroupId); + var characterGroup = await projectRepository.GetGroupAsync(characterGroupId2); - if (group == null) + if (characterGroup == null) { return NotFound(); } - var groupIds = group.GetChildrenGroupsIdRecursiveIncludingThis(); + + var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); + var groupIds = projectInfo.GetChildGroupIdsIncludingThis(characterGroupId2).ToArray(); var claims = await claimsRepository.GetClaimsForGroups(projectId, ClaimStatusSpec.Active, groupIds); - return await ShowMasterClaimListForGroup(group, export, "Заявки в группу (все)", claims, + return await ShowMasterClaimListForGroup(characterGroup, export, "Заявки в группу (все)", claims, GroupNavigationPage.ClaimsActive, ClaimStatusSpec.Active); } [HttpGet, MasterAuthorize()] public async Task DiscussingForGroup(ProjectIdentification projectId, int characterGroupId, string export) { - var group = await projectRepository.GetGroupAsync(projectId, characterGroupId); - if (group == null) + var characterGroupId2 = new CharacterGroupIdentification(projectId, characterGroupId); + var characterGroup = await projectRepository.GetGroupAsync(characterGroupId2); + + if (characterGroup == null) { return NotFound(); } - var groupIds = group.GetChildrenGroupsIdRecursiveIncludingThis(); + + var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); + var groupIds = projectInfo.GetChildGroupIdsIncludingThis(characterGroupId2).ToArray(); var claims = await claimsRepository.GetClaimsForGroups(projectId, ClaimStatusSpec.Discussion, groupIds); - return await ShowMasterClaimListForGroup(group, export, "Обсуждаемые заявки в группу (все)", + return await ShowMasterClaimListForGroup(characterGroup, export, "Обсуждаемые заявки в группу (все)", claims, GroupNavigationPage.ClaimsDiscussing, ClaimStatusSpec.Active); } diff --git a/src/JoinRpg.Portal/Controllers/PlotListController.cs b/src/JoinRpg.Portal/Controllers/PlotListController.cs index b914ad64c..1bf3d5d8e 100644 --- a/src/JoinRpg.Portal/Controllers/PlotListController.cs +++ b/src/JoinRpg.Portal/Controllers/PlotListController.cs @@ -1,6 +1,5 @@ using JoinRpg.Data.Interfaces; using JoinRpg.DataModel; -using JoinRpg.Domain; using JoinRpg.Interfaces; using JoinRpg.Portal.Controllers.Common; using JoinRpg.Portal.Infrastructure.Authorization; @@ -15,7 +14,8 @@ public class PlotListController( IProjectRepository projectRepository, IPlotRepository plotRepository, IProjectMetadataRepository projectMetadataRepository, - ICurrentUserAccessor currentUser + ICurrentUserAccessor currentUser, + ICharacterRepository characterRepository ) : JoinControllerGameBase { [RequireMasterOrPublish] @@ -43,24 +43,29 @@ public async Task ByTag(int projectid, string tagname) [HttpGet("~/{ProjectId}/roles/{CharacterGroupId}/plots")] [RequireMasterOrPublish] - public async Task ForGroup(int projectId, int characterGroupId) + public async Task ForGroup(ProjectIdentification projectId, int characterGroupId) { - var group = await projectRepository.GetGroupAsync(projectId, characterGroupId); - if (group == null) + + var characterGroupId2 = new CharacterGroupIdentification(projectId, characterGroupId); + var characterGroup = await projectRepository.GetGroupAsync(characterGroupId2); + + if (characterGroup == null) { return NotFound(); } + var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); + var characterGroupIds = projectInfo.GetChildGroupIdsIncludingThis(characterGroupId2).ToArray(); + //TODO slow - var characterGroups = group.GetChildrenGroupsRecursive().Union([group]).ToList(); - var characters = characterGroups.SelectMany(g => g.Characters).Distinct().Select(c => c.CharacterId).ToList(); - var characterGroupIds = characterGroups.Select(c => c.CharacterGroupId).ToList(); - var folders = await plotRepository.GetPlotsForTargets(projectId, characters, characterGroupIds); + var characters = await projectRepository.GetCharacterByGroups(characterGroupIds); + + var folders = await plotRepository.GetPlotsForTargets(projectId, [.. characters.Select(x => x.CharacterId)], [.. characterGroupIds.Select(x => x.Id)]); + + var groupNavigation = new CharacterGroupDetailsViewModel(characterGroup, currentUser.UserIdOrDefault, GroupNavigationPage.Plots); - var groupNavigation = new CharacterGroupDetailsViewModel(group, currentUser.UserIdOrDefault, GroupNavigationPage.Plots); - var projectInfo = await projectMetadataRepository.GetProjectMetadata(new(projectId)); - var list = PlotFolderListViewModelBuilder.ToPlotFolderListViewModel(folders, currentUser, projectInfo, "Сюжеты группы «" + group.CharacterGroupName + "»"); + var list = PlotFolderListViewModelBuilder.ToPlotFolderListViewModel(folders, currentUser, projectInfo, "Сюжеты группы «" + characterGroup.CharacterGroupName + "»"); return View("ForGroup", new PlotFolderListViewModelForGroup(list, groupNavigation)); } diff --git a/src/JoinRpg.Portal/Controllers/XGameApi/Builders.cs b/src/JoinRpg.Portal/Controllers/XGameApi/Builders.cs index 611c90be1..9a615b7ba 100644 --- a/src/JoinRpg.Portal/Controllers/XGameApi/Builders.cs +++ b/src/JoinRpg.Portal/Controllers/XGameApi/Builders.cs @@ -20,6 +20,19 @@ public static GroupHeader[] ToGroupHeaders( .OrderBy(group => group.CharacterGroupId)]; } + public static GroupHeader[] ToGroupHeaders( + IReadOnlyCollection characterDirectGroups) + { + return [.. characterDirectGroups.Where(group => group.IsActive && !group.IsSpecial) + .Select( + group => new GroupHeader + { + CharacterGroupId = group.Id.CharacterGroupId, + CharacterGroupName = group.Name, + }) + .OrderBy(group => group.CharacterGroupId)]; + } + public static CharacterPlayerInfo? CreatePlayerInfo(Claim? claim, ProjectInfo projectInfo) { if (claim is null) diff --git a/src/JoinRpg.Portal/Controllers/XGameApi/CharacterApiController.cs b/src/JoinRpg.Portal/Controllers/XGameApi/CharacterApiController.cs index dc2d1840b..21c1ab49f 100644 --- a/src/JoinRpg.Portal/Controllers/XGameApi/CharacterApiController.cs +++ b/src/JoinRpg.Portal/Controllers/XGameApi/CharacterApiController.cs @@ -50,7 +50,8 @@ private static CharacterHeader BuildCharacterHeader(int projectId, int character public async Task GetOne(int projectId, int characterId) { var character = await characterRepository.GetCharacterViewAsync(projectId, characterId); - var projectInfo = await projectMetadataRepository.GetProjectMetadata(new(projectId)); + ProjectIdentification projectId1 = new(projectId); + var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId1); return new CharacterInfo { @@ -60,7 +61,7 @@ public async Task GetOne(int projectId, int characterId) InGame = character.InGame, BusyStatus = (CharacterBusyStatus)character.GetBusyStatus(), Groups = ApiInfoBuilder.ToGroupHeaders(character.DirectGroups), - AllGroups = ApiInfoBuilder.ToGroupHeaders(character.AllGroups), + AllGroups = ApiInfoBuilder.ToGroupHeaders([.. projectInfo.GetParentGroupsIncludingThis(CharacterGroupIdentification.FromList(character.DirectGroups.Select(x => x.CharacterGroupId), projectId1))]), Fields = [..character.GetFields(projectInfo).Where(field => field.HasViewableValue) .Select(field => new FieldValue { diff --git a/src/JoinRpg.Services.Impl/Claims/ClaimServiceImpl.cs b/src/JoinRpg.Services.Impl/Claims/ClaimServiceImpl.cs index 93d7c9a97..0a498ca04 100644 --- a/src/JoinRpg.Services.Impl/Claims/ClaimServiceImpl.cs +++ b/src/JoinRpg.Services.Impl/Claims/ClaimServiceImpl.cs @@ -324,7 +324,7 @@ public async Task ApproveByMaster(ClaimIdentification claimId, string commentTex if (claim.Character.CharacterType == CharacterType.Slot) { - var character = await CreateCharacterFromSlot(claim.Character, claim.Player); + var character = await CreateCharacterFromSlot(claim.Character, claim.Player, projectInfo); claim.Character = character; claim.CharacterId = character.CharacterId; } @@ -375,7 +375,7 @@ public async Task ApproveByMaster(ClaimIdentification claimId, string commentTex } } - private async Task CreateCharacterFromSlot(Character slot, User player) + private async Task CreateCharacterFromSlot(Character slot, User player, ProjectInfo projectInfo) { switch (slot.CharacterSlotLimit) @@ -427,7 +427,7 @@ private async Task CreateCharacterFromSlot(Character slot, User playe UpdatedById = player.UserId, }; - var plots = await PlotRepository.GetPlotsForCharacter(slot); + var plots = await PlotRepository.GetDirectPlotsForCharacter(slot.GetId()); foreach (var plot in plots) { diff --git a/src/JoinRpg.Services.Impl/Claims/SubscribeCalculator.cs b/src/JoinRpg.Services.Impl/Claims/SubscribeCalculator.cs index ceb268edf..4b5814def 100644 --- a/src/JoinRpg.Services.Impl/Claims/SubscribeCalculator.cs +++ b/src/JoinRpg.Services.Impl/Claims/SubscribeCalculator.cs @@ -11,8 +11,7 @@ namespace JoinRpg.Services.Impl.Claims; internal class SubscribeCalculator( IUserSubscribeRepository userSubscribeRepository, ICharacterRepository characterRepository, - IClaimsRepository claimsRepository, - IProjectRepository projectRepository + IClaimsRepository claimsRepository ) { internal async Task> GetRecepients(SubscribeCalculateArgs args, ProjectInfo projectInfo) @@ -32,7 +31,7 @@ internal async Task> GetRecepients(Su var characters = await characterRepository.GetCharacters(characterIds); var character = await userSubscribeRepository.GetForCharAndGroups( - [.. characters.SelectMany(x => x.GetParentGroupIdsToTop()).Distinct()], + [.. characters.SelectMany(x => x.GetParentGroupIdsToTop(projectInfo)).Distinct()], characterIds); AddIfPredicateAndNotAlreadyPresent(character, SubscriptionReason.SubscribedMaster); @@ -57,9 +56,7 @@ internal async Task> GetRecepients(Fo AddUserInfoHeaderIfNotPresent(list, args.RespondingTo, SubscriptionReason.AnswerToYourComment); - var groups = await projectRepository.LoadGroups(args.Groups); - - var claims = await claimsRepository.GetClaimHeadersWithPlayer([.. groups.SelectMany(g => g.GetChildrenGroupsIdentificationRecursiveIncludingThis())], ClaimStatusSpec.Approved); + var claims = await claimsRepository.GetClaimHeadersWithPlayer([.. projectInfo.GetChildGroupIdsIncludingThis(args.Groups)], ClaimStatusSpec.Approved); AddUserInfoHeaderIfNotPresent(list, claims.Select(c => c.Player), SubscriptionReason.Forum); diff --git a/src/JoinRpg.Services.Impl/PlotServiceImpl.cs b/src/JoinRpg.Services.Impl/PlotServiceImpl.cs index 653e98ba3..2f9becbe4 100644 --- a/src/JoinRpg.Services.Impl/PlotServiceImpl.cs +++ b/src/JoinRpg.Services.Impl/PlotServiceImpl.cs @@ -9,7 +9,8 @@ namespace JoinRpg.Services.Impl; public class PlotServiceImpl(IUnitOfWork unitOfWork, IMassProjectEmailService massProjectEmailService, - ICurrentUserAccessor currentUserAccessor) : DbServiceImplBase(unitOfWork, currentUserAccessor), IPlotService + ICurrentUserAccessor currentUserAccessor, + IProjectMetadataRepository projectMetadataRepository) : DbServiceImplBase(unitOfWork, currentUserAccessor), IPlotService { public async Task CreatePlotFolder(ProjectIdentification projectId, string masterTitle, string todo) { @@ -222,7 +223,7 @@ public async Task MoveElement(int projectId, int plotElementId, int parentCharac var character = await LoadProjectSubEntityAsync(projectId, parentCharacterId); _ = character.RequestMasterAccess(CurrentUserId, Permission.CanEditRoles).EnsureProjectActive(); - var plots = await PlotRepository.GetPlotsForCharacter(character); + var plots = await PlotRepository.GetDirectPlotsForCharacter(new CharacterIdentification(projectId, parentCharacterId)); var voc = character.GetCharacterPlotContainer(plots); var element = plots.Single(p => p.PlotElementId == plotElementId); @@ -326,12 +327,13 @@ public async Task ReorderPlotElements(PlotElementIdentification plotElementId, P public async Task ReorderPlotByChar(CharacterIdentification characterId, PlotElementIdentification targetId, PlotElementIdentification? afterId) { + var projectInfo = await projectMetadataRepository.GetProjectMetadata(characterId.ProjectId); var character = await CharactersRepository.GetCharacterAsync(characterId); - _ = character.RequestMasterAccess(CurrentUserId, Permission.CanManagePlots).EnsureProjectActive(); + _ = projectInfo.RequestMasterAccess(currentUserAccessor, Permission.CanManagePlots).EnsureProjectActive(); - var plotTarget = ToTarget(character); + var plotTarget = ToTarget(character, projectInfo); var plots = await PlotRepository.GetPlotsBySpecification(new PlotSpecification(plotTarget, PlotVersionFilter.PublishedVersion, PlotElementType.RegularPlot)); @@ -342,10 +344,10 @@ public async Task ReorderPlotByChar(CharacterIdentification characterId, PlotEle } - private static TargetsInfo ToTarget(Character character) + private static TargetsInfo ToTarget(Character character, ProjectInfo projectInfo) { return new TargetsInfo( [new(new(character.ProjectId, character.CharacterId), character.CharacterName)], - [.. character.GetParentGroupsToTop().Select(x => new GroupTarget(x.GetId(), x.CharacterGroupName))]); + [.. character.GetParentGroupsToTop(projectInfo).Select(x => new GroupTarget(x.Id, x.Name))]); } } diff --git a/src/JoinRpg.WebPortal.Managers/Plots/CharacterPlotViewService.cs b/src/JoinRpg.WebPortal.Managers/Plots/CharacterPlotViewService.cs index 8bf1a61ac..0d174d51d 100644 --- a/src/JoinRpg.WebPortal.Managers/Plots/CharacterPlotViewService.cs +++ b/src/JoinRpg.WebPortal.Managers/Plots/CharacterPlotViewService.cs @@ -98,7 +98,7 @@ private async Task> LoadPlotInfo characters = [.. characters.Where(c => AccessArgumentsFactory.Create(c, currentUser, projectInfo, characterAccessMode).CharacterPlotAccess)]; - return characters.ToDictionary(x => x.GetId(), x => new ChPlotInfo(ToTarget(x), x.PlotElementOrderData)); + return characters.ToDictionary(x => x.GetId(), x => new ChPlotInfo(ToTarget(x, projectInfo), x.PlotElementOrderData)); } private async Task> LoadPlotInfoForActiveCharacters(ProjectIdentification projectId, CharacterAccessMode characterAccessMode, ProjectInfo projectInfo) @@ -108,16 +108,16 @@ private async Task> LoadPlotInfo characters = [.. characters.Where(c => AccessArgumentsFactory.Create(c, currentUser, projectInfo, characterAccessMode).CharacterPlotAccess)]; - return characters.ToDictionary(x => x.GetId(), x => new ChPlotInfo(ToTarget(x), x.PlotElementOrderData)); + return characters.ToDictionary(x => x.GetId(), x => new ChPlotInfo(ToTarget(x, projectInfo), x.PlotElementOrderData)); } private record ChPlotInfo(TargetsInfo Targets, string Ordering); - private TargetsInfo ToTarget(Character character) + private TargetsInfo ToTarget(Character character, ProjectInfo projectInfo) { return new TargetsInfo( [new(new(character.ProjectId, character.CharacterId), character.CharacterName)], - [.. character.GetParentGroupsToTop().Select(x => new GroupTarget(x.GetId(), x.CharacterGroupName))]); + [.. character.GetParentGroupsToTop(projectInfo).Select(x => new GroupTarget(x.Id, x.Name))]); } public async Task> GetPlotsForCharacter(CharacterIdentification characterId) diff --git a/src/JoinRpg.WebPortal.Managers/Subscribe/SubscribeViewService.cs b/src/JoinRpg.WebPortal.Managers/Subscribe/SubscribeViewService.cs index 47c705269..5ca954add 100644 --- a/src/JoinRpg.WebPortal.Managers/Subscribe/SubscribeViewService.cs +++ b/src/JoinRpg.WebPortal.Managers/Subscribe/SubscribeViewService.cs @@ -17,17 +17,20 @@ internal class SubscribeViewService(IUriService uriService, IFinanceReportRepository financeReportRepository, ICurrentUserAccessor currentUserAccessor, IGameSubscribeService gameSubscribeService, - IClaimsRepository claimsRepository + IClaimsRepository claimsRepository, + IProjectMetadataRepository projectMetadataRepository ) : IGameSubscribeClient { public async Task GetSubscribeForClaim(int projectId, int claimId) { var currentUser = await userRepository.GetWithSubscribe(currentUserAccessor.UserId); + var projectInfo = await projectMetadataRepository.GetProjectMetadata(new ProjectIdentification(projectId)); + var claim = await claimsRepository.GetClaim(projectId, claimId); - return GetFullSubscriptionTooltip(claim.Character.GetParentGroupsToTop(), currentUser.Subscriptions, claimId); + return GetFullSubscriptionTooltip(claim.Character.GetParentGroupsToTop(projectInfo), currentUser.Subscriptions, claimId); } public async Task GetSubscribeForMaster(int projectId, int masterId) @@ -53,7 +56,7 @@ await gameSubscribeService.UpdateSubscribeForGroup(new SubscribeForGroupRequest( }); } - private static ClaimSubscribeViewModel GetFullSubscriptionTooltip(IEnumerable parents, + private static ClaimSubscribeViewModel GetFullSubscriptionTooltip(IEnumerable parents, IReadOnlyCollection subscriptions, int claimId) { var claimStatusChangeGroup = ""; @@ -78,7 +81,7 @@ private static ClaimSubscribeViewModel GetFullSubscriptionTooltip(IEnumerable> IUnifiedGridClient.GetForCaptain(ProjectIdentification projectId, UgStatusFilterView filter) { @@ -19,11 +17,10 @@ async Task> IUnifiedGridClient.Ge { return []; } - var groups = await projectRepository.LoadGroups([.. access.Select(x => x.CharacterGroup)]); - var allGroups = groups.SelectMany(x => x.GetChildrenGroupsIdentificationRecursiveIncludingThis()).Distinct(); + var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); + var allGroups = projectInfo.GetChildGroupIdsIncludingThis([.. access.Select(x => x.CharacterGroup)]); var items = await unifiedGridRepository.GetByGroups(projectId, (UgStatusSpec)filter, [.. allGroups]); - var projectInfo = await projectMetadataRepository.GetProjectMetadata(projectId); return [.. items.Select(claim => ItemBuilder.BuildItemForCaptain(claim, currentUserAccessor, projectInfo)).WhereNotNull()]; } diff --git a/src/JoinRpg.WebPortal.Models/Characters/CharacterDetailsViewModel.cs b/src/JoinRpg.WebPortal.Models/Characters/CharacterDetailsViewModel.cs index 87d065cc9..7f3654450 100644 --- a/src/JoinRpg.WebPortal.Models/Characters/CharacterDetailsViewModel.cs +++ b/src/JoinRpg.WebPortal.Models/Characters/CharacterDetailsViewModel.cs @@ -17,13 +17,13 @@ public class CharacterParentGroupsViewModel [ReadOnly(true), DisplayName("Входит в группы")] public IReadOnlyCollection ParentGroups { get; } - public CharacterParentGroupsViewModel(Character character, bool hasMasterAccess) + public CharacterParentGroupsViewModel(Character character, bool hasMasterAccess, ProjectInfo projectInfo) { ArgumentNullException.ThrowIfNull(character); HasMasterAccess = hasMasterAccess; ParentGroups = character - .GetParentGroupsToTop() + .GetParentGroupsToTop(projectInfo) .Where(group => !group.IsRoot && !group.IsSpecial) .Select(g => new CharacterGroupLinkViewModel(g)).ToList(); HasAnyGroups = ParentGroups.Count > 0; @@ -55,7 +55,7 @@ public CharacterDetailsViewModel( var accessArguments = AccessArgumentsFactory.Create(character, currentUserId, projectInfo) with { EditAllowed = false }; - ParentGroups = new CharacterParentGroupsViewModel(character, accessArguments.MasterAccess); + ParentGroups = new CharacterParentGroupsViewModel(character, accessArguments.MasterAccess, projectInfo); Navigation = CharacterNavigationViewModel.FromCharacter(character, CharacterNavigationPage.Character, currentUserId.UserIdOrDefault, projectInfo); diff --git a/src/JoinRpg.WebPortal.Models/Characters/CharacterGroupLinkViewModel.cs b/src/JoinRpg.WebPortal.Models/Characters/CharacterGroupLinkViewModel.cs index 2c70d18ce..e324c40cc 100644 --- a/src/JoinRpg.WebPortal.Models/Characters/CharacterGroupLinkViewModel.cs +++ b/src/JoinRpg.WebPortal.Models/Characters/CharacterGroupLinkViewModel.cs @@ -14,6 +14,7 @@ public class CharacterGroupLinkViewModel : ILinkable public int ProjectId { get; } public bool IsActive { get; } + [Obsolete] public CharacterGroupLinkViewModel(CharacterGroup group) { CharacterGroupId = group.CharacterGroupId; @@ -22,4 +23,13 @@ public CharacterGroupLinkViewModel(CharacterGroup group) ProjectId = group.ProjectId; IsActive = group.IsActive; } + + public CharacterGroupLinkViewModel(CharacterGroupInfo group) + { + CharacterGroupId = group.Id.CharacterGroupId; + Name = group.Name; + IsPublic = group.IsPublic; + ProjectId = group.Id.ProjectId; + IsActive = group.IsActive; + } } diff --git a/src/JoinRpg.WebPortal.Models/Characters/CharacterListItemViewModel.cs b/src/JoinRpg.WebPortal.Models/Characters/CharacterListItemViewModel.cs index 1e71d4bf9..ef9090bfa 100644 --- a/src/JoinRpg.WebPortal.Models/Characters/CharacterListItemViewModel.cs +++ b/src/JoinRpg.WebPortal.Models/Characters/CharacterListItemViewModel.cs @@ -110,7 +110,7 @@ public CharacterListItemViewModel( Problems = problemValidator.Validate(character, projectInfo).Select(p => new ProblemViewModel(p)).ToList(); Groups = new CharacterParentGroupsViewModel(character, - projectInfo.HasMasterAccess(new UserIdentification(currentUserId))); + projectInfo.HasMasterAccess(new UserIdentification(currentUserId)), projectInfo); Responsible = character.GetResponsibleMaster(); } diff --git a/src/JoinRpg.WebPortal.Models/Claims/ClaimViewModel.cs b/src/JoinRpg.WebPortal.Models/Claims/ClaimViewModel.cs index 1e411770f..287f3010e 100644 --- a/src/JoinRpg.WebPortal.Models/Claims/ClaimViewModel.cs +++ b/src/JoinRpg.WebPortal.Models/Claims/ClaimViewModel.cs @@ -169,7 +169,7 @@ public ClaimViewModel(ICurrentUserAccessor currentUser, } ClaimFee = new ClaimFeeViewModel(claim, this, currentUser.UserId, projectInfo, externalPaymentUrlFactory); - ParentGroups = new CharacterParentGroupsViewModel(claim.Character, HasMasterAccess); + ParentGroups = new CharacterParentGroupsViewModel(claim.Character, HasMasterAccess, projectInfo); Plot = new PlotDisplayViewModel(plotElements, currentUser, diff --git a/src/JoinRpg.WebPortal.Models/Helpers/CharacterAndGroupPrefixer.cs b/src/JoinRpg.WebPortal.Models/Helpers/CharacterAndGroupPrefixer.cs index af0a92e8f..a2ee848df 100644 --- a/src/JoinRpg.WebPortal.Models/Helpers/CharacterAndGroupPrefixer.cs +++ b/src/JoinRpg.WebPortal.Models/Helpers/CharacterAndGroupPrefixer.cs @@ -31,7 +31,7 @@ public static List GetUnprefixedGroups(this IEnume return [.. CharacterGroupIdentification.FromList(targets.UnprefixNumbers(GroupFieldPrefix), projectIdentification)]; } - public static List GetParentGroupsForEdit(this IWorldObject field) => field.ParentGroups.Where(gr => !gr.IsSpecial).Select(pg => pg.CharacterGroupId).PrefixAsGroups().ToList(); + public static List GetParentGroupsForEdit(this CharacterGroup field) => field.ParentGroups.Where(gr => !gr.IsSpecial).Select(pg => pg.CharacterGroupId).PrefixAsGroups().ToList(); public static List AsPossibleParentForEdit(this CharacterGroup field) => new List { field.CharacterGroupId }.PrefixAsGroups().ToList(); diff --git a/src/JoinRpg.WebPortal.Models/Helpers/JoinrpgMarkdownLinkRenderer.cs b/src/JoinRpg.WebPortal.Models/Helpers/JoinrpgMarkdownLinkRenderer.cs index a2ba697a9..5b08925f3 100644 --- a/src/JoinRpg.WebPortal.Models/Helpers/JoinrpgMarkdownLinkRenderer.cs +++ b/src/JoinRpg.WebPortal.Models/Helpers/JoinrpgMarkdownLinkRenderer.cs @@ -19,7 +19,7 @@ public class JoinrpgMarkdownLinkRenderer : ILinkRenderer private delegate void RenderFunc(HtmlRenderer renderer, string match, int index, string extra); private delegate void CharRenderFunc(HtmlRenderer renderer, Character character, string extra); - private delegate void CharGroupRenderFunc(HtmlRenderer renderer, CharacterGroup characterGroup, IReadOnlyCollection characters, string extra); + private delegate void CharGroupRenderFunc(HtmlRenderer renderer, CharacterGroupInfo characterGroup, IReadOnlyCollection characters, string extra); private delegate void FieldColumnRenderFunc(HtmlRenderer renderer, Character character, ProjectInfo projectInfo, Dictionary fields); public JoinrpgMarkdownLinkRenderer(Project project, ProjectInfo projectInfo) @@ -48,13 +48,13 @@ private record class Column(string Name, FieldColumnRenderFunc RenderFunc) - public static Column Groups = new("Группы", (renderer, character, _, _) => GroupListRenderer(renderer, character, g => true)); - public static Column PublicGroups = new("Группы", (renderer, character, _, _) => GroupListRenderer(renderer, character, g => g.IsPublic)); + public static Column Groups = new("Группы", (renderer, character, projectInfo, _) => GroupListRenderer(renderer, character, g => true, projectInfo)); + public static Column PublicGroups = new("Группы", (renderer, character, projectInfo, _) => GroupListRenderer(renderer, character, g => g.IsPublic, projectInfo)); - private static void GroupListRenderer(HtmlRenderer renderer, Character character, Func groupPredicate) + private static void GroupListRenderer(HtmlRenderer renderer, Character character, Func groupPredicate, ProjectInfo projectInfo) { var sep = ""; - foreach (var group in character.GetIntrestingGroupsForDisplayToTop().Where(groupPredicate)) + foreach (var group in character.GetIntrestingGroupsForDisplayToTop(projectInfo).Where(groupPredicate)) { renderer.Write(sep); sep = ", "; @@ -65,7 +65,7 @@ private static void GroupListRenderer(HtmlRenderer renderer, Character character public static Column CharacterName = new Column("Персонаж", (renderer, character, _, _) => CharacterLinkImpl(renderer, character, "")); } - private void ExperimentalTableFunc(HtmlRenderer renderer, CharacterGroup group, IReadOnlyCollection characters, string extra) + private void ExperimentalTableFunc(HtmlRenderer renderer, CharacterGroupInfo group, IReadOnlyCollection characters, string extra) { if (!renderer.EnableHtmlForBlock) { @@ -149,7 +149,7 @@ private List SetupColumnsFromExtra(string extra) return columns; } - private void GroupListFunc(HtmlRenderer renderer, CharacterGroup group, IReadOnlyCollection ch, string extra) + private void GroupListFunc(HtmlRenderer renderer, CharacterGroupInfo group, IReadOnlyCollection ch, string extra) { GroupHeader(renderer, group, extra); bool sep = false; @@ -172,10 +172,10 @@ private void GroupListFunc(HtmlRenderer renderer, CharacterGroup group, IReadOnl } } - private void GroupListFullFunc(HtmlRenderer renderer, CharacterGroup group, IReadOnlyCollection characters, string extra) + private void GroupListFullFunc(HtmlRenderer renderer, CharacterGroupInfo group, IReadOnlyCollection characters, string extra) { GroupHeader(renderer, group, extra); - RenderInnerMarkdown(renderer, group.Description); + RenderInnerMarkdown(renderer, Project.CharacterGroups.Single(x => x.CharacterGroupId == group.Id.CharacterGroupId).Description); foreach (var character in characters) { if (renderer.EnableHtmlForBlock) @@ -207,7 +207,7 @@ private void GroupListFullFunc(HtmlRenderer renderer, CharacterGroup group, IRea } } - private void GroupHeader(HtmlRenderer renderer, CharacterGroup group, string extra) + private void GroupHeader(HtmlRenderer renderer, CharacterGroupInfo group, string extra) { if (renderer.EnableHtmlForInline) { @@ -232,15 +232,15 @@ private static void RenderInnerMarkdown(HtmlRenderer renderer, MarkdownString ma } } - private void GroupName(HtmlRenderer renderer, CharacterGroup characterGroup, IReadOnlyCollection characters, string extra) => GroupLinkImpl(renderer, characterGroup, extra); + private void GroupName(HtmlRenderer renderer, CharacterGroupInfo characterGroup, IReadOnlyCollection characters, string extra) => GroupLinkImpl(renderer, characterGroup, extra); - private static void GroupLinkImpl(HtmlRenderer renderer, CharacterGroup group, string extra) + private static void GroupLinkImpl(HtmlRenderer renderer, CharacterGroupInfo group, string extra) { if (renderer.EnableHtmlForInline) { - renderer.Write($""); + renderer.Write($""); } - renderer.Write(extra == "" ? group.CharacterGroupName : extra); + renderer.Write(extra == "" ? group.Name : extra); if (renderer.EnableHtmlForInline) { renderer.Write(""); @@ -364,19 +364,21 @@ private RenderFunc GroupWrapper(CharGroupRenderFunc inner) { return (renderer, match, index, extra) => { - var group = Project.CharacterGroups.SingleOrDefault(c => c.CharacterGroupId == index); + var groupId = new CharacterGroupIdentification(Project.ProjectId, index); + var group = projectInfo.Groups[groupId]; if (group == null) { Fail(renderer, match, index, extra); return; } + + var groupsIds = projectInfo.GetChildGroupIdsIncludingThis(groupId).ToArray(); + IReadOnlyCollection ch = [ - ..group.GetOrderedCharacters().Where(chr => chr.IsActive) - .Union( - group.GetOrderedChildrenGroupsRecursive().SelectMany(g => g.GetOrderedCharacters().Where(chr => chr.IsActive)) - ) - .Distinct() + ..Project.CharacterGroups.Where(g => groupsIds.Contains(g.GetId())).SelectMany(g => g.GetOrderedCharacters().Where(chr => chr.IsActive)).Distinct() ]; + + inner(renderer, group, ch, extra); }; } diff --git a/src/JoinRpg.WebPortal.Models/Print/ModelBuilders.cs b/src/JoinRpg.WebPortal.Models/Print/ModelBuilders.cs index 4c0e06c5c..c62d8e54d 100644 --- a/src/JoinRpg.WebPortal.Models/Print/ModelBuilders.cs +++ b/src/JoinRpg.WebPortal.Models/Print/ModelBuilders.cs @@ -20,7 +20,7 @@ public static EnvelopeViewModel ToEnvelopeViewModel(this Character character, Pr PlayerDisplayName: approvedClaim?.Player?.ExtractDisplayName(), CharacterName: character.CharacterName, ResponsibleMaster: projectInfo.Masters.First(m => m.UserId == respMasterId).ToUserLinkViewModel(), - Groups: [.. character.GetIntrestingGroupsForDisplayToTop() + Groups: [.. character.GetIntrestingGroupsForDisplayToTop(projectInfo) .Where(g => g.IsPublic) .Select(g => g.ToCharacterGroupLinkSlimViewModel())], PlayerPhoneNumber: approvedClaim?.Player.Extra?.PhoneNumber, @@ -28,5 +28,5 @@ public static EnvelopeViewModel ToEnvelopeViewModel(this Character character, Pr ); } - public static CharacterGroupLinkSlimViewModel ToCharacterGroupLinkSlimViewModel(this CharacterGroup g) => new(g.GetId(), g.CharacterGroupName, g.IsPublic, g.IsActive); + public static CharacterGroupLinkSlimViewModel ToCharacterGroupLinkSlimViewModel(this CharacterGroupInfo g) => new(g.Id, g.Name, g.IsPublic, g.IsActive); }