diff --git a/src/Bonsai.Tests.Search/Fixtures/SearchCollection.cs b/src/Bonsai.Tests.Search/Fixtures/SearchCollection.cs index 7440922b..06a7ee99 100644 --- a/src/Bonsai.Tests.Search/Fixtures/SearchCollection.cs +++ b/src/Bonsai.Tests.Search/Fixtures/SearchCollection.cs @@ -1,9 +1,8 @@ using Xunit; -namespace Bonsai.Tests.Search.Fixtures +namespace Bonsai.Tests.Search.Fixtures; + +[CollectionDefinition("Search tests")] +public class SearchCollection: ICollectionFixture { - [CollectionDefinition("Search tests")] - public class SearchCollection: ICollectionFixture - { - } -} +} \ No newline at end of file diff --git a/src/Bonsai.Tests.Search/Fixtures/SearchEngineFixture.cs b/src/Bonsai.Tests.Search/Fixtures/SearchEngineFixture.cs index 5c64e078..573ef246 100644 --- a/src/Bonsai.Tests.Search/Fixtures/SearchEngineFixture.cs +++ b/src/Bonsai.Tests.Search/Fixtures/SearchEngineFixture.cs @@ -6,51 +6,50 @@ using Microsoft.EntityFrameworkCore; using Xunit; -namespace Bonsai.Tests.Search.Fixtures +namespace Bonsai.Tests.Search.Fixtures; + +/// +/// Shared context for all fulltext search tests. +/// +public class SearchEngineFixture: IAsyncLifetime { + public SearchEngineFixture() + { + var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("default") + .Options; + + Db = new AppDbContext(opts); + // Search = new ElasticService(new ElasticSearchConfig { Host = "http://localhost:9200", IndexName = "test_pages" }); + Search = new LuceneNetService(); + } + + public readonly AppDbContext Db; + public readonly ISearchEngine Search; + + /// + /// Initializes the search index with test data. + /// + public async Task InitializeAsync() + { + var rootPath = Path.GetFullPath(Directory.GetCurrentDirectory()); + var wwwPath = Path.Combine(rootPath, "wwwroot"); + if(Directory.Exists(wwwPath)) + Directory.Delete(wwwPath, true); + await Search.ClearDataAsync(); + + var seedPath = Path.Combine(rootPath, "..", "..", "..", "..", "Bonsai", "Data", "Utils", "Seed"); + await SeedData.EnsureSampleDataSeededAsync(Db, seedPath); + + await foreach (var page in Db.Pages) + await Search.AddPageAsync(page); + } + /// - /// Shared context for all fulltext search tests. + /// Releases the in-memory testing database. /// - public class SearchEngineFixture: IAsyncLifetime + public async Task DisposeAsync() { - public SearchEngineFixture() - { - var opts = new DbContextOptionsBuilder() - .UseInMemoryDatabase("default") - .Options; - - Db = new AppDbContext(opts); - // Search = new ElasticService(new ElasticSearchConfig { Host = "http://localhost:9200", IndexName = "test_pages" }); - Search = new LuceneNetService(); - } - - public readonly AppDbContext Db; - public readonly ISearchEngine Search; - - /// - /// Initializes the search index with test data. - /// - public async Task InitializeAsync() - { - var rootPath = Path.GetFullPath(Directory.GetCurrentDirectory()); - var wwwPath = Path.Combine(rootPath, "wwwroot"); - if(Directory.Exists(wwwPath)) - Directory.Delete(wwwPath, true); - await Search.ClearDataAsync(); - - var seedPath = Path.Combine(rootPath, "..", "..", "..", "..", "Bonsai", "Data", "Utils", "Seed"); - await SeedData.EnsureSampleDataSeededAsync(Db, seedPath); - - await foreach (var page in Db.Pages) - await Search.AddPageAsync(page); - } - - /// - /// Releases the in-memory testing database. - /// - public async Task DisposeAsync() - { - await Db.DisposeAsync(); - } + await Db.DisposeAsync(); } -} +} \ No newline at end of file diff --git a/src/Bonsai.Tests.Search/SearchTests.cs b/src/Bonsai.Tests.Search/SearchTests.cs index 40f64d7f..8c492f79 100644 --- a/src/Bonsai.Tests.Search/SearchTests.cs +++ b/src/Bonsai.Tests.Search/SearchTests.cs @@ -4,69 +4,68 @@ using Bonsai.Tests.Search.Fixtures; using Xunit; -namespace Bonsai.Tests.Search +namespace Bonsai.Tests.Search; + +[Collection("Search tests")] +public class SearchTests { - [Collection("Search tests")] - public class SearchTests + public SearchTests(SearchEngineFixture ctx) { - public SearchTests(SearchEngineFixture ctx) - { - _ctx = ctx; - } + _ctx = ctx; + } - private readonly SearchEngineFixture _ctx; + private readonly SearchEngineFixture _ctx; - [Fact] - public async Task Search_finds_exact_matches() - { - var query = "Иванов Иван Петрович"; + [Fact] + public async Task Search_finds_exact_matches() + { + var query = "Иванов Иван Петрович"; - var result = await _ctx.Search.SearchAsync(query); + var result = await _ctx.Search.SearchAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); + } - [Fact] - public async Task Search_considers_page_body() - { - var query = "Авиастроения"; + [Fact] + public async Task Search_considers_page_body() + { + var query = "Авиастроения"; - var result = await _ctx.Search.SearchAsync(query); + var result = await _ctx.Search.SearchAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); + } - [Theory] - [InlineData("Иванов", "Иванов.*")] - [InlineData("Иванова", "Иванов.*")] - public async Task Search_highlights_matches_in_title(string query, string regex) - { - var result = await _ctx.Search.SearchAsync(query); + [Theory] + [InlineData("Иванов", "Иванов.*")] + [InlineData("Иванова", "Иванов.*")] + public async Task Search_highlights_matches_in_title(string query, string regex) + { + var result = await _ctx.Search.SearchAsync(query); - Assert.NotEmpty(result); - Assert.All(result, x => Assert.True(Regex.IsMatch(x.HighlightedTitle, "" + regex + "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))); - } + Assert.NotEmpty(result); + Assert.All(result, x => Assert.True(Regex.IsMatch(x.HighlightedTitle, "" + regex + "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))); + } - [Theory] - [InlineData("Иванов", "Иванов.*")] - [InlineData("Иванова", "Иванов.*")] - public async Task Search_highlights_matches_in_body(string query, string regex) - { - var result = await _ctx.Search.SearchAsync(query); + [Theory] + [InlineData("Иванов", "Иванов.*")] + [InlineData("Иванова", "Иванов.*")] + public async Task Search_highlights_matches_in_body(string query, string regex) + { + var result = await _ctx.Search.SearchAsync(query); - Assert.NotEmpty(result); - Assert.Contains(result, x => Regex.IsMatch(x.HighlightedDescription, "" + regex + "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)); - } + Assert.NotEmpty(result); + Assert.Contains(result, x => Regex.IsMatch(x.HighlightedDescription, "" + regex + "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)); + } - [Fact] - public async Task Search_considers_pagination() - { - var query = "Иванов"; + [Fact] + public async Task Search_considers_pagination() + { + var query = "Иванов"; - var p1 = await _ctx.Search.SearchAsync(query, 0); - var p2 = await _ctx.Search.SearchAsync(query, 1); + var p1 = await _ctx.Search.SearchAsync(query, 0); + var p2 = await _ctx.Search.SearchAsync(query, 1); - Assert.Empty(p1.Select(x => x.Key).Intersect(p2.Select(x => x.Key))); - } + Assert.Empty(p1.Select(x => x.Key).Intersect(p2.Select(x => x.Key))); } -} +} \ No newline at end of file diff --git a/src/Bonsai.Tests.Search/SuggestTests.cs b/src/Bonsai.Tests.Search/SuggestTests.cs index 3d16213d..278c8910 100644 --- a/src/Bonsai.Tests.Search/SuggestTests.cs +++ b/src/Bonsai.Tests.Search/SuggestTests.cs @@ -7,177 +7,176 @@ using Impworks.Utils.Linq; using Xunit; -namespace Bonsai.Tests.Search +namespace Bonsai.Tests.Search; + +[Collection("Search tests")] +public class SuggestTests { - [Collection("Search tests")] - public class SuggestTests + public SuggestTests(SearchEngineFixture ctx) { - public SuggestTests(SearchEngineFixture ctx) - { - _ctx = ctx; - } + _ctx = ctx; + } - private readonly SearchEngineFixture _ctx; + private readonly SearchEngineFixture _ctx; - [Fact] - public async Task Prefix_search_matches() - { - var query = "Иванов"; + [Fact] + public async Task Prefix_search_matches() + { + var query = "Иванов"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); + } - [Theory] - [InlineData("Вадимович", "Семенов_Николай_Вадимович")] - [InlineData("Ирина", "Семенова_Ирина_Алексеевна")] - public async Task Non_prefix_search_matches(string query, string key) - { - var result = await _ctx.Search.SuggestAsync(query); + [Theory] + [InlineData("Вадимович", "Семенов_Николай_Вадимович")] + [InlineData("Ирина", "Семенова_Ирина_Алексеевна")] + public async Task Non_prefix_search_matches(string query, string key) + { + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == key); - } + Assert.Contains(result, x => x.Key == key); + } - [Fact] - public async Task Family_name_matches_male_to_female() - { - var query = "Иванов"; + [Fact] + public async Task Family_name_matches_male_to_female() + { + var query = "Иванов"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == "Иванова_Екатерина_Валерьевна"); - } + Assert.Contains(result, x => x.Key == "Иванова_Екатерина_Валерьевна"); + } - [Fact] - public async Task Family_name_matches_female_to_male() - { - var query = "Иванова"; + [Fact] + public async Task Family_name_matches_female_to_male() + { + var query = "Иванова"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Иван_Петрович"); + } - [Theory] - [InlineData("Алексеевны", "Семенова_Ирина_Алексеевна")] - [InlineData("Алексеевне", "Семенова_Ирина_Алексеевна")] - [InlineData("Алексеевной", "Семенова_Ирина_Алексеевна")] - [InlineData("Вадимовича", "Семенов_Николай_Вадимович")] - [InlineData("Вадимовичу", "Семенов_Николай_Вадимович")] - [InlineData("Вадимовичем", "Семенов_Николай_Вадимович")] - public async Task Patronym_inflections_match(string query, string key) - { - var result = await _ctx.Search.SuggestAsync(query); + [Theory] + [InlineData("Алексеевны", "Семенова_Ирина_Алексеевна")] + [InlineData("Алексеевне", "Семенова_Ирина_Алексеевна")] + [InlineData("Алексеевной", "Семенова_Ирина_Алексеевна")] + [InlineData("Вадимовича", "Семенов_Николай_Вадимович")] + [InlineData("Вадимовичу", "Семенов_Николай_Вадимович")] + [InlineData("Вадимовичем", "Семенов_Николай_Вадимович")] + public async Task Patronym_inflections_match(string query, string key) + { + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == key); - } + Assert.Contains(result, x => x.Key == key); + } - [Fact] - public async Task Letters_e_yo_are_interchangeable() - { - var query = "Пётр"; + [Fact] + public async Task Letters_e_yo_are_interchangeable() + { + var query = "Пётр"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); + } - [Fact] - public async Task Case_is_ignored() - { - var query = "иВАНОВ"; + [Fact] + public async Task Case_is_ignored() + { + var query = "иВАНОВ"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); - } + Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); + } - [Fact] - public async Task Page_type_filter_works() - { - var query = "Семенова"; + [Fact] + public async Task Page_type_filter_works() + { + var query = "Семенова"; - var result = await _ctx.Search.SuggestAsync(query, new[] { PageType.Person }); + var result = await _ctx.Search.SuggestAsync(query, new[] { PageType.Person }); - Assert.Contains(result, x => x.Key.Contains("Семенова", StringComparison.InvariantCultureIgnoreCase)); - Assert.DoesNotContain(result, x => x.Key.StartsWith("Свадьба", StringComparison.InvariantCultureIgnoreCase)); - } + Assert.Contains(result, x => x.Key.Contains("Семенова", StringComparison.InvariantCultureIgnoreCase)); + Assert.DoesNotContain(result, x => x.Key.StartsWith("Свадьба", StringComparison.InvariantCultureIgnoreCase)); + } - [Fact] - public async Task Empty_result_is_returned_for_unknown_query() - { - var query = "Алевтина"; + [Fact] + public async Task Empty_result_is_returned_for_unknown_query() + { + var query = "Алевтина"; - var result = await _ctx.Search.SuggestAsync(query); + var result = await _ctx.Search.SuggestAsync(query); - Assert.Empty(result); - } + Assert.Empty(result); + } - [Theory] - [InlineData("Ивонов", "Иванов_Иван_Петрович")] - [InlineData("Эванов", "Иванов_Иван_Петрович")] - [InlineData("Иваноф", "Иванов_Иван_Петрович")] - [InlineData("Олех", "Михайлов_Олег_Евгеньевич")] - [InlineData("Мехайлов", "Михайлов_Олег_Евгеньевич")] - [InlineData("Сименов", "Семенов_Евгений_Иванович")] - public async Task Family_name_allows_one_typo(string query, string key) - { - var result = await _ctx.Search.SuggestAsync(query); + [Theory] + [InlineData("Ивонов", "Иванов_Иван_Петрович")] + [InlineData("Эванов", "Иванов_Иван_Петрович")] + [InlineData("Иваноф", "Иванов_Иван_Петрович")] + [InlineData("Олех", "Михайлов_Олег_Евгеньевич")] + [InlineData("Мехайлов", "Михайлов_Олег_Евгеньевич")] + [InlineData("Сименов", "Семенов_Евгений_Иванович")] + public async Task Family_name_allows_one_typo(string query, string key) + { + var result = await _ctx.Search.SuggestAsync(query); - Assert.Contains(result, x => x.Key == key); - } + Assert.Contains(result, x => x.Key == key); + } - [Theory] - [InlineData("семенова", 2)] - [InlineData("семенов", 5)] - [InlineData("иванов", 3)] - public async Task Exact_matches_go_first(string query, int count) - { - var result = await _ctx.Search.SuggestAsync(query); + [Theory] + [InlineData("семенова", 2)] + [InlineData("семенов", 5)] + [InlineData("иванов", 3)] + public async Task Exact_matches_go_first(string query, int count) + { + var result = await _ctx.Search.SuggestAsync(query); - Assert.True(result.Count >= count, "result.Count >= count"); - Assert.All(result.Take(count), x => Assert.StartsWith(query, x.Key, StringComparison.InvariantCultureIgnoreCase)); - } + Assert.True(result.Count >= count, "result.Count >= count"); + Assert.All(result.Take(count), x => Assert.StartsWith(query, x.Key, StringComparison.InvariantCultureIgnoreCase)); + } - [Theory] - [InlineData("Семенова Анна Николаевна")] - [InlineData("Анна Семенова")] - [InlineData("Михайлов Олег Евгеньевич")] - public async Task Autocomplete_doesnt_hide_while_typing(string query) + [Theory] + [InlineData("Семенова Анна Николаевна")] + [InlineData("Анна Семенова")] + [InlineData("Михайлов Олег Евгеньевич")] + public async Task Autocomplete_doesnt_hide_while_typing(string query) + { + var emptyLengths = new List(); + for (var i = 3; i < query.Length; i++) { - var emptyLengths = new List(); - for (var i = 3; i < query.Length; i++) - { - var typing = query.Substring(0, i); - var result = await _ctx.Search.SuggestAsync(typing); - if (result.Count == 0) - emptyLengths.Add(i); - } - - Assert.Equal("", emptyLengths.JoinString(", ")); + var typing = query.Substring(0, i); + var result = await _ctx.Search.SuggestAsync(typing); + if (result.Count == 0) + emptyLengths.Add(i); } - [Fact] - public async Task Family_name_doesnt_discard_patronym() - { - var query = "Михайлов"; + Assert.Equal("", emptyLengths.JoinString(", ")); + } - var result = await _ctx.Search.SuggestAsync(query); + [Fact] + public async Task Family_name_doesnt_discard_patronym() + { + var query = "Михайлов"; - Assert.Contains(result, x => x.Key == "Михайлов_Олег_Евгеньевич"); - Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); - } + var result = await _ctx.Search.SuggestAsync(query); - [Fact] - public async Task Suggest_with_page_types_does_not_include_unrelated_matches() - { - var query = "Барсик"; + Assert.Contains(result, x => x.Key == "Михайлов_Олег_Евгеньевич"); + Assert.Contains(result, x => x.Key == "Иванов_Петр_Михайлович"); + } - var result = await _ctx.Search.SuggestAsync(query, new[] {PageType.Person, PageType.Pet}, 5); + [Fact] + public async Task Suggest_with_page_types_does_not_include_unrelated_matches() + { + var query = "Барсик"; - Assert.Equal(1, result.Count); - Assert.Equal("Барсик", result[0].Key); - } + var result = await _ctx.Search.SuggestAsync(query, new[] {PageType.Person, PageType.Pet}, 5); + + Assert.Equal(1, result.Count); + Assert.Equal("Барсик", result[0].Key); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/AdminMenuComponent.cs b/src/Bonsai/Areas/Admin/Components/AdminMenuComponent.cs index d13f6bbc..bc04381a 100644 --- a/src/Bonsai/Areas/Admin/Components/AdminMenuComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/AdminMenuComponent.cs @@ -9,62 +9,61 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Component for displaying the side menu of the admin panel. +/// +public class AdminMenuComponent: ViewComponent { + public AdminMenuComponent(UserManager userMgr, AppDbContext db) + { + _userMgr = userMgr; + _db = db; + } + + private readonly UserManager _userMgr; + private readonly AppDbContext _db; + /// - /// Component for displaying the side menu of the admin panel. + /// Displays the menu. /// - public class AdminMenuComponent: ViewComponent + public async Task InvokeAsync() { - public AdminMenuComponent(UserManager userMgr, AppDbContext db) - { - _userMgr = userMgr; - _db = db; - } + var user = await _userMgr.GetUserAsync(HttpContext.User); + var roles = await _userMgr.GetRolesAsync(user); - private readonly UserManager _userMgr; - private readonly AppDbContext _db; + var groups = new List(); + groups.Add( + new MenuGroupVM( + new MenuItemVM { Title = Texts.Admin_Menu_Dashboard, Icon = "home", Url = Url.Action("Index", "Dashboard", new { area = "Admin" }) }, + new MenuItemVM { Title = Texts.Admin_Menu_Changesets, Icon = "database", Url = Url.Action("Index", "Changesets", new { area = "Admin" }) } + ) + ); - /// - /// Displays the menu. - /// - public async Task InvokeAsync() - { - var user = await _userMgr.GetUserAsync(HttpContext.User); - var roles = await _userMgr.GetRolesAsync(user); - - var groups = new List(); - groups.Add( - new MenuGroupVM( - new MenuItemVM { Title = Texts.Admin_Menu_Dashboard, Icon = "home", Url = Url.Action("Index", "Dashboard", new { area = "Admin" }) }, - new MenuItemVM { Title = Texts.Admin_Menu_Changesets, Icon = "database", Url = Url.Action("Index", "Changesets", new { area = "Admin" }) } - ) - ); + groups.Add( + new MenuGroupVM( + new MenuItemVM { Title = Texts.Admin_Menu_Pages, Icon = "file-text-o", Url = Url.Action("Index", "Pages", new { area = "Admin" }) }, + new MenuItemVM { Title = Texts.Admin_Menu_Relations, Icon = "retweet", Url = Url.Action("Index", "Relations", new { area = "Admin" }) }, + new MenuItemVM { Title = Texts.Admin_Menu_Media, Icon = "picture-o", Url = Url.Action("Index", "Media", new { area = "Admin" })} + ) + ); + if (roles.Contains(nameof(UserRole.Admin))) + { + var newUsers = await _db.Users.CountAsync(x => !x.IsValidated); groups.Add( new MenuGroupVM( - new MenuItemVM { Title = Texts.Admin_Menu_Pages, Icon = "file-text-o", Url = Url.Action("Index", "Pages", new { area = "Admin" }) }, - new MenuItemVM { Title = Texts.Admin_Menu_Relations, Icon = "retweet", Url = Url.Action("Index", "Relations", new { area = "Admin" }) }, - new MenuItemVM { Title = Texts.Admin_Menu_Media, Icon = "picture-o", Url = Url.Action("Index", "Media", new { area = "Admin" })} + new MenuItemVM { Title = Texts.Admin_Menu_Users, Icon = "user-circle-o", Url = Url.Action("Index", "Users", new { area = "Admin" }), NotificationsCount = newUsers }, + new MenuItemVM { Title = Texts.Admin_Menu_Config, Icon = "cog", Url = Url.Action("Index", "DynamicConfig", new { area = "Admin" }) } ) ); + } - if (roles.Contains(nameof(UserRole.Admin))) - { - var newUsers = await _db.Users.CountAsync(x => !x.IsValidated); - groups.Add( - new MenuGroupVM( - new MenuItemVM { Title = Texts.Admin_Menu_Users, Icon = "user-circle-o", Url = Url.Action("Index", "Users", new { area = "Admin" }), NotificationsCount = newUsers }, - new MenuItemVM { Title = Texts.Admin_Menu_Config, Icon = "cog", Url = Url.Action("Index", "DynamicConfig", new { area = "Admin" }) } - ) - ); - } - - var url = HttpContext.Request.Path; - foreach (var item in groups.SelectMany(x => x.Items)) - item.IsSelected = url.StartsWithSegments(item.Url); + var url = HttpContext.Request.Path; + foreach (var item in groups.SelectMany(x => x.Items)) + item.IsSelected = url.StartsWithSegments(item.Url); - return View("~/Areas/Admin/Views/Components/AdminMenu.cshtml", groups); - } + return View("~/Areas/Admin/Views/Components/AdminMenu.cshtml", groups); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/ListEnumFilterComponent.cs b/src/Bonsai/Areas/Admin/Components/ListEnumFilterComponent.cs index 76b823d1..16a161e0 100644 --- a/src/Bonsai/Areas/Admin/Components/ListEnumFilterComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/ListEnumFilterComponent.cs @@ -9,71 +9,70 @@ using Impworks.Utils.Format; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components -{ +namespace Bonsai.Areas.Admin.Components; + +/// +/// Renders the filter for an enum. +/// +public class ListEnumFilterComponent: ViewComponent +{ /// - /// Renders the filter for an enum. + /// Renders the result. /// - public class ListEnumFilterComponent: ViewComponent - { - /// - /// Renders the result. - /// - public async Task InvokeAsync(ListRequestVM request, string propName) - { - if (request == null || propName == null) - throw new ArgumentNullException(nameof(request), "Either request or propName must be specified!"); + public async Task InvokeAsync(ListRequestVM request, string propName) + { + if (request == null || propName == null) + throw new ArgumentNullException(nameof(request), "Either request or propName must be specified!"); - var prop = request.GetType().GetProperty(propName); - if (prop == null) - throw new ArgumentException($"Request of type '{request.GetType().Name}' does not have a property '{propName}'."); + var prop = request.GetType().GetProperty(propName); + if (prop == null) + throw new ArgumentException($"Request of type '{request.GetType().Name}' does not have a property '{propName}'."); - if (!prop.PropertyType.IsArray) - throw new ArgumentException($"Property {request.GetType().Name}.{propName} must be an array."); + if (!prop.PropertyType.IsArray) + throw new ArgumentException($"Property {request.GetType().Name}.{propName} must be an array."); - var elemType = prop.PropertyType.GetElementType(); - if(!elemType.IsEnum) - throw new ArgumentException($"Type '{prop.PropertyType.Name}' is not an enum."); + var elemType = prop.PropertyType.GetElementType(); + if(!elemType.IsEnum) + throw new ArgumentException($"Type '{prop.PropertyType.Name}' is not an enum."); - var enumValues = GetEnumValues(elemType); - var selected = prop.GetValue(request) as int[]; - var result = new List(); + var enumValues = GetEnumValues(elemType); + var selected = prop.GetValue(request) as int[]; + var result = new List(); - foreach (var enumValue in enumValues) + foreach (var enumValue in enumValues) + { + result.Add(new ListEnumFilterItemVM { - result.Add(new ListEnumFilterItemVM - { - PropertyName = propName, - Title = enumValue.Value, - Value = enumValue.Key.ToInvariantString(), - IsActive = selected?.Any(x => x == enumValue.Key) == true - }); - } - - return View("~/Areas/Admin/Views/Components/ListEnumFilter.cshtml", result); + PropertyName = propName, + Title = enumValue.Value, + Value = enumValue.Key.ToInvariantString(), + IsActive = selected?.Any(x => x == enumValue.Key) == true + }); } - /// - /// Gets the raw enum values and their descriptions. - /// - private Dictionary GetEnumValues(Type enumType) - { - var flags = BindingFlags.DeclaredOnly - | BindingFlags.Static - | BindingFlags.Public - | BindingFlags.GetField; + return View("~/Areas/Admin/Views/Components/ListEnumFilter.cshtml", result); + } - return enumType.GetFields(flags) - .Where(x => x.GetCustomAttribute() == null) - .Select(x => new - { - Description = LocaleProvider.GetLocaleEnumDescription(enumType, x.Name), - Value = x.GetRawConstantValue() - }) - .ToDictionary( - x => Convert.ToInt32(x.Value), - x => x.Description ?? Enum.GetName(enumType, x.Value) - ); - } + /// + /// Gets the raw enum values and their descriptions. + /// + private Dictionary GetEnumValues(Type enumType) + { + var flags = BindingFlags.DeclaredOnly + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.GetField; + + return enumType.GetFields(flags) + .Where(x => x.GetCustomAttribute() == null) + .Select(x => new + { + Description = LocaleProvider.GetLocaleEnumDescription(enumType, x.Name), + Value = x.GetRawConstantValue() + }) + .ToDictionary( + x => Convert.ToInt32(x.Value), + x => x.Description ?? Enum.GetName(enumType, x.Value) + ); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/ListHeaderComponent.cs b/src/Bonsai/Areas/Admin/Components/ListHeaderComponent.cs index 64b5ea4b..0854f476 100644 --- a/src/Bonsai/Areas/Admin/Components/ListHeaderComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/ListHeaderComponent.cs @@ -4,39 +4,38 @@ using Bonsai.Areas.Admin.ViewModels.Components; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Displays the sortable header. +/// +public class ListHeaderComponent: ViewComponent { /// - /// Displays the sortable header. + /// Renders the sortable header. /// - public class ListHeaderComponent: ViewComponent + public async Task InvokeAsync(string url, ListRequestVM request, string propName, string title) { - /// - /// Renders the sortable header. - /// - public async Task InvokeAsync(string url, ListRequestVM request, string propName, string title) + var vm = new ListHeaderVM { - var vm = new ListHeaderVM - { - Title = title - }; - - var cloneRequest = ListRequestVM.Clone(request); - cloneRequest.Page = 0; + Title = title + }; - if (cloneRequest.OrderBy == propName) - { - cloneRequest.OrderDescending = !cloneRequest.OrderDescending; - vm.IsDescending = cloneRequest.OrderDescending; - } - else - { - cloneRequest.OrderBy = propName; - cloneRequest.OrderDescending = false; - } + var cloneRequest = ListRequestVM.Clone(request); + cloneRequest.Page = 0; - vm.Url = ListRequestHelper.GetUrl(url, cloneRequest); - return View("~/Areas/Admin/Views/Components/ListHeader.cshtml", vm); + if (cloneRequest.OrderBy == propName) + { + cloneRequest.OrderDescending = !cloneRequest.OrderDescending; + vm.IsDescending = cloneRequest.OrderDescending; } + else + { + cloneRequest.OrderBy = propName; + cloneRequest.OrderDescending = false; + } + + vm.Url = ListRequestHelper.GetUrl(url, cloneRequest); + return View("~/Areas/Admin/Views/Components/ListHeader.cshtml", vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/ListHiddenFilterComponent.cs b/src/Bonsai/Areas/Admin/Components/ListHiddenFilterComponent.cs index 311858dd..6795a05b 100644 --- a/src/Bonsai/Areas/Admin/Components/ListHiddenFilterComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/ListHiddenFilterComponent.cs @@ -4,25 +4,24 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Renders the hidden filter components. +/// +public class ListHiddenFilterComponent: ViewComponent { /// - /// Renders the hidden filter components. + /// Renders the included fields. /// - public class ListHiddenFilterComponent: ViewComponent + public async Task InvokeAsync(ListRequestVM request, string[] include = null) { - /// - /// Renders the included fields. - /// - public async Task InvokeAsync(ListRequestVM request, string[] include = null) - { - var values = ListRequestHelper.GetValues(request) - .Where(x => x.Key == nameof(ListRequestVM.OrderBy) - || x.Key == nameof(ListRequestVM.OrderDescending) - || include?.Contains(x.Key) == true) - .ToList(); + var values = ListRequestHelper.GetValues(request) + .Where(x => x.Key == nameof(ListRequestVM.OrderBy) + || x.Key == nameof(ListRequestVM.OrderDescending) + || include?.Contains(x.Key) == true) + .ToList(); - return View("~/Areas/Admin/Views/Components/ListHiddenFilter.cshtml", values); - } + return View("~/Areas/Admin/Views/Components/ListHiddenFilter.cshtml", values); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/ListItemFilterComponent.cs b/src/Bonsai/Areas/Admin/Components/ListItemFilterComponent.cs index d3724426..cfec1103 100644 --- a/src/Bonsai/Areas/Admin/Components/ListItemFilterComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/ListItemFilterComponent.cs @@ -5,35 +5,34 @@ using Bonsai.Areas.Admin.ViewModels.Components; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Renders the filter for a related entity. +/// +public class ListItemFilterComponent: ViewComponent { /// - /// Renders the filter for a related entity. + /// Renders the filter item. /// - public class ListItemFilterComponent: ViewComponent + public async Task InvokeAsync(string url, ListRequestVM request, string propName, string title) { - /// - /// Renders the filter item. - /// - public async Task InvokeAsync(string url, ListRequestVM request, string propName, string title) - { - if (request == null || propName == null || title == null) - throw new ArgumentNullException(); + if (request == null || propName == null || title == null) + throw new ArgumentNullException(); - var prop = request.GetType().GetProperty(propName); - if (prop == null) - throw new ArgumentException($"Request of type '{request.GetType().Name}' does not have a property '{propName}'."); + var prop = request.GetType().GetProperty(propName); + if (prop == null) + throw new ArgumentException($"Request of type '{request.GetType().Name}' does not have a property '{propName}'."); - var cloneRequest = ListRequestVM.Clone(request); - prop.SetValue(cloneRequest, prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null); + var cloneRequest = ListRequestVM.Clone(request); + prop.SetValue(cloneRequest, prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null); - var vm = new ListItemFilterVM - { - Title = title, - CancelUrl = ListRequestHelper.GetUrl(url, cloneRequest) - }; + var vm = new ListItemFilterVM + { + Title = title, + CancelUrl = ListRequestHelper.GetUrl(url, cloneRequest) + }; - return View("~/Areas/Admin/Views/Components/ListItemFilter.cshtml", vm); - } + return View("~/Areas/Admin/Views/Components/ListItemFilter.cshtml", vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/ListPaginatorComponent.cs b/src/Bonsai/Areas/Admin/Components/ListPaginatorComponent.cs index 51735d4f..4e432372 100644 --- a/src/Bonsai/Areas/Admin/Components/ListPaginatorComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/ListPaginatorComponent.cs @@ -6,56 +6,55 @@ using Bonsai.Areas.Admin.ViewModels.Components; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Renders the page numbers for a list. +/// +public class ListPaginatorComponent: ViewComponent { /// - /// Renders the page numbers for a list. + /// Renders the pagination control. /// - public class ListPaginatorComponent: ViewComponent + public async Task InvokeAsync(string url, ListRequestVM request, int pageCount) { - /// - /// Renders the pagination control. - /// - public async Task InvokeAsync(string url, ListRequestVM request, int pageCount) - { - var result = new List(); - var pages = GetPageNumbers(request.Page, pageCount); - var prevPage = (int?) null; - - foreach (var page in pages) - { - if(prevPage != null && prevPage != page - 1) - result.Add(new ListPaginatorPageVM { Title = "..." }); + var result = new List(); + var pages = GetPageNumbers(request.Page, pageCount); + var prevPage = (int?) null; - var clone = ListRequestVM.Clone(request); - clone.Page = page; + foreach (var page in pages) + { + if(prevPage != null && prevPage != page - 1) + result.Add(new ListPaginatorPageVM { Title = "..." }); - var cloneUrl = ListRequestHelper.GetUrl(url, clone); - result.Add(new ListPaginatorPageVM - { - Url = cloneUrl, - Title = (page + 1).ToString(), - IsCurrent = page == request.Page - }); + var clone = ListRequestVM.Clone(request); + clone.Page = page; - prevPage = page; - } + var cloneUrl = ListRequestHelper.GetUrl(url, clone); + result.Add(new ListPaginatorPageVM + { + Url = cloneUrl, + Title = (page + 1).ToString(), + IsCurrent = page == request.Page + }); - return View("~/Areas/Admin/Views/Components/ListPaginator.cshtml", result); + prevPage = page; } - /// - /// Returns the page IDs. - /// - private IEnumerable GetPageNumbers(int current, int count) - { - const int Margin = 2; + return View("~/Areas/Admin/Views/Components/ListPaginator.cshtml", result); + } - return Enumerable.Range(0, Margin) - .Concat(Enumerable.Range(current - Margin, Margin * 2 + 1)) - .Concat(Enumerable.Range(count - Margin, Margin)) - .Where(x => x >= 0 && x < count) - .Distinct(); - } + /// + /// Returns the page IDs. + /// + private IEnumerable GetPageNumbers(int current, int count) + { + const int Margin = 2; + + return Enumerable.Range(0, Margin) + .Concat(Enumerable.Range(current - Margin, Margin * 2 + 1)) + .Concat(Enumerable.Range(count - Margin, Margin)) + .Where(x => x >= 0 && x < count) + .Distinct(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/NotificationTagHelper.cs b/src/Bonsai/Areas/Admin/Components/NotificationTagHelper.cs index 3cb605ed..e1e31e3e 100644 --- a/src/Bonsai/Areas/Admin/Components/NotificationTagHelper.cs +++ b/src/Bonsai/Areas/Admin/Components/NotificationTagHelper.cs @@ -1,36 +1,35 @@ using Bonsai.Areas.Admin.Logic; using Microsoft.AspNetCore.Razor.TagHelpers; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +/// +/// Displays a notification if the user has not discarded it. +/// +[HtmlTargetElement("div", Attributes = "notification-id")] +public class NotificationTagHelper: TagHelper { - /// - /// Displays a notification if the user has not discarded it. - /// - [HtmlTargetElement("div", Attributes = "notification-id")] - public class NotificationTagHelper: TagHelper + public NotificationTagHelper(NotificationsService notifications) { - public NotificationTagHelper(NotificationsService notifications) - { - _notifications = notifications; - } + _notifications = notifications; + } - private readonly NotificationsService _notifications; + private readonly NotificationsService _notifications; - /// - /// ID of the notification. - /// - [HtmlAttributeName("notification-id")] - public string NotificationId { get; set; } - - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if(!_notifications.IsShown(NotificationId)) - { - output.SuppressOutput(); - return; - } + /// + /// ID of the notification. + /// + [HtmlAttributeName("notification-id")] + public string NotificationId { get; set; } - output.PreContent.AppendHtml($@""); + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if(!_notifications.IsShown(NotificationId)) + { + output.SuppressOutput(); + return; } + + output.PreContent.AppendHtml($@""); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Components/OperationMessageComponent.cs b/src/Bonsai/Areas/Admin/Components/OperationMessageComponent.cs index f59b1f8c..b1eb0470 100644 --- a/src/Bonsai/Areas/Admin/Components/OperationMessageComponent.cs +++ b/src/Bonsai/Areas/Admin/Components/OperationMessageComponent.cs @@ -3,21 +3,20 @@ using Bonsai.Code.Utils.Helpers; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Components +namespace Bonsai.Areas.Admin.Components; + +public class OperationMessageComponent: ViewComponent { - public class OperationMessageComponent: ViewComponent + /// + /// Displays the notification. + /// + public async Task InvokeAsync() { - /// - /// Displays the notification. - /// - public async Task InvokeAsync() - { - var vm = HttpContext.Session.Get(); - if (vm == null) - return Content(""); + var vm = HttpContext.Session.Get(); + if (vm == null) + return Content(""); - HttpContext.Session.Remove(); - return View("~/Areas/Admin/Views/Components/OperationMessage.cshtml", vm); - } + HttpContext.Session.Remove(); + return View("~/Areas/Admin/Views/Components/OperationMessage.cshtml", vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/AdminControllerBase.cs b/src/Bonsai/Areas/Admin/Controllers/AdminControllerBase.cs index 127bfb3e..89ac1763 100644 --- a/src/Bonsai/Areas/Admin/Controllers/AdminControllerBase.cs +++ b/src/Bonsai/Areas/Admin/Controllers/AdminControllerBase.cs @@ -10,91 +10,90 @@ using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Base class for all admin controllers. +/// +[Area("Admin")] +[Authorize(Policy = AdminAuthRequirement.Name)] +public abstract class AdminControllerBase: AppControllerBase { /// - /// Base class for all admin controllers. + /// Name of the default action to use for redirection when an OperationException occurs. /// - [Area("Admin")] - [Authorize(Policy = AdminAuthRequirement.Name)] - public abstract class AdminControllerBase: AppControllerBase - { - /// - /// Name of the default action to use for redirection when an OperationException occurs. - /// - protected virtual string DefaultActionUrl => Url.Action("Index"); + protected virtual string DefaultActionUrl => Url.Action("Index"); - /// - /// Handles OperationExceptions. - /// - public override void OnActionExecuted(ActionExecutedContext context) + /// + /// Handles OperationExceptions. + /// + public override void OnActionExecuted(ActionExecutedContext context) + { + if (context.Exception is OperationException oe) { - if (context.Exception is OperationException oe) + Session.Set(new OperationResultMessage { - Session.Set(new OperationResultMessage - { - IsSuccess = false, - Message = oe.Message - }); - context.Result = Redirect(DefaultActionUrl); - context.ExceptionHandled = true; - return; - } - - base.OnActionExecuted(context); + IsSuccess = false, + Message = oe.Message + }); + context.Result = Redirect(DefaultActionUrl); + context.ExceptionHandled = true; + return; } - /// - /// Saves the message for the next page. - /// - protected ActionResult RedirectToSuccess(string msg = null) - { - if(!string.IsNullOrEmpty(msg)) - ShowMessage(msg); + base.OnActionExecuted(context); + } - if (TempData[ListStateKey] is string listState) - { - try - { - var json = (ListRequestVM) JsonConvert.DeserializeObject(listState, ListStateType); - var url = ListRequestHelper.GetUrl(DefaultActionUrl, json); - return Redirect(url); - } - catch - { - } - } - return Redirect(DefaultActionUrl); - } + /// + /// Saves the message for the next page. + /// + protected ActionResult RedirectToSuccess(string msg = null) + { + if(!string.IsNullOrEmpty(msg)) + ShowMessage(msg); - /// - /// Displays a one-time message on the next page. - /// - protected void ShowMessage(string msg, bool success = true) + if (TempData[ListStateKey] is string listState) { - Session.Set(new OperationResultMessage + try { - IsSuccess = success, - Message = msg - }); + var json = (ListRequestVM) JsonConvert.DeserializeObject(listState, ListStateType); + var url = ListRequestHelper.GetUrl(DefaultActionUrl, json); + return Redirect(url); + } + catch + { + } } + return Redirect(DefaultActionUrl); + } - /// - /// Stores the last list state in the temporary storage. - /// - protected void PersistListState(ListRequestVM request) + /// + /// Displays a one-time message on the next page. + /// + protected void ShowMessage(string msg, bool success = true) + { + Session.Set(new OperationResultMessage { - TempData[ListStateKey] = JsonConvert.SerializeObject(request, Formatting.None); - } - - /// - /// Returns the key for persisting storage. - /// - private string ListStateKey => GetType().Name + "." + nameof(ListRequestVM); + IsSuccess = success, + Message = msg + }); + } - /// - /// Default type to use for list state deserialization. - /// - protected virtual Type ListStateType => typeof(ListRequestVM); + /// + /// Stores the last list state in the temporary storage. + /// + protected void PersistListState(ListRequestVM request) + { + TempData[ListStateKey] = JsonConvert.SerializeObject(request, Formatting.None); } -} + + /// + /// Returns the key for persisting storage. + /// + private string ListStateKey => GetType().Name + "." + nameof(ListRequestVM); + + /// + /// Default type to use for list state deserialization. + /// + protected virtual Type ListStateType => typeof(ListRequestVM); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/ChangesetsController.cs b/src/Bonsai/Areas/Admin/Controllers/ChangesetsController.cs index 52e8ee4e..4b6d109b 100644 --- a/src/Bonsai/Areas/Admin/Controllers/ChangesetsController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/ChangesetsController.cs @@ -7,80 +7,79 @@ using Bonsai.Localization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for managing changesets. +/// +[Route("admin/changes")] +public class ChangesetsController: AdminControllerBase { - /// - /// Controller for managing changesets. - /// - [Route("admin/changes")] - public class ChangesetsController: AdminControllerBase + public ChangesetsController(ChangesetsManagerService changes, AppDbContext db) { - public ChangesetsController(ChangesetsManagerService changes, AppDbContext db) - { - _changes = changes; - _db = db; - } - - private readonly ChangesetsManagerService _changes; - private readonly AppDbContext _db; + _changes = changes; + _db = db; + } - protected override Type ListStateType => typeof(ChangesetsListRequestVM); + private readonly ChangesetsManagerService _changes; + private readonly AppDbContext _db; - #region Public methods + protected override Type ListStateType => typeof(ChangesetsListRequestVM); - /// - /// Displays the list of all changesets. - /// - [HttpGet] - public async Task Index(ChangesetsListRequestVM request) - { - PersistListState(request); - var vm = await _changes.GetChangesetsAsync(request); - return View(vm); - } + #region Public methods - /// - /// Displays the information about a particular changeset. - /// - [HttpGet] - [Route("details")] - public async Task Details(Guid id) - { - var vm = await _changes.GetChangesetDetailsAsync(id); - return View(vm); - } + /// + /// Displays the list of all changesets. + /// + [HttpGet] + public async Task Index(ChangesetsListRequestVM request) + { + PersistListState(request); + var vm = await _changes.GetChangesetsAsync(request); + return View(vm); + } - /// - /// Displays the changeset revert confirmation. - /// - [HttpGet] - [Route("revert")] - public async Task Revert(Guid id) - { - var vm = await _changes.GetChangesetDetailsAsync(id); - if (!vm.CanRevert) - throw new OperationException(Texts.Admin_Changesets_CannotRevertMessage); + /// + /// Displays the information about a particular changeset. + /// + [HttpGet] + [Route("details")] + public async Task Details(Guid id) + { + var vm = await _changes.GetChangesetDetailsAsync(id); + return View(vm); + } - return View(vm); - } + /// + /// Displays the changeset revert confirmation. + /// + [HttpGet] + [Route("revert")] + public async Task Revert(Guid id) + { + var vm = await _changes.GetChangesetDetailsAsync(id); + if (!vm.CanRevert) + throw new OperationException(Texts.Admin_Changesets_CannotRevertMessage); - /// - /// Reverts the edit to its original state. - /// - [HttpPost] - [Route("revert")] - public async Task Revert(Guid id, bool confirm) - { - var editVm = await _changes.GetChangesetDetailsAsync(id); - if (!editVm.CanRevert) - throw new OperationException(Texts.Admin_Changesets_CannotRevertMessage); + return View(vm); + } - await _changes.RevertChangeAsync(id, User); - await _db.SaveChangesAsync(); + /// + /// Reverts the edit to its original state. + /// + [HttpPost] + [Route("revert")] + public async Task Revert(Guid id, bool confirm) + { + var editVm = await _changes.GetChangesetDetailsAsync(id); + if (!editVm.CanRevert) + throw new OperationException(Texts.Admin_Changesets_CannotRevertMessage); - return RedirectToSuccess(Texts.Admin_Changesets_RevertedMessage); - } + await _changes.RevertChangeAsync(id, User); + await _db.SaveChangesAsync(); - #endregion + return RedirectToSuccess(Texts.Admin_Changesets_RevertedMessage); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/DashboardController.cs b/src/Bonsai/Areas/Admin/Controllers/DashboardController.cs index 59a84c74..c552468d 100644 --- a/src/Bonsai/Areas/Admin/Controllers/DashboardController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/DashboardController.cs @@ -3,39 +3,38 @@ using Bonsai.Code.Utils.Helpers; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for the default admin page. +/// +[Route("admin/home")] +public class DashboardController: AdminControllerBase { - /// - /// Controller for the default admin page. - /// - [Route("admin/home")] - public class DashboardController: AdminControllerBase + public DashboardController(DashboardPresenterService dash) { - public DashboardController(DashboardPresenterService dash) - { - _dash = dash; - } + _dash = dash; + } - private readonly DashboardPresenterService _dash; + private readonly DashboardPresenterService _dash; - /// - /// Displays the main page. - /// - [Route("")] - public async Task Index() - { - var vm = await _dash.GetDashboardAsync(); - return View(vm); - } + /// + /// Displays the main page. + /// + [Route("")] + public async Task Index() + { + var vm = await _dash.GetDashboardAsync(); + return View(vm); + } - /// - /// Returns the list of events at specified page. - /// - [Route("events")] - public async Task Events([FromQuery] int page = 0) - { - var vm = await _dash.GetEventsAsync(page).ToListAsync(); - return View(vm); - } + /// + /// Returns the list of events at specified page. + /// + [Route("events")] + public async Task Events([FromQuery] int page = 0) + { + var vm = await _dash.GetEventsAsync(page).ToListAsync(); + return View(vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/DraftsController.cs b/src/Bonsai/Areas/Admin/Controllers/DraftsController.cs index 24ba3b93..0b9a53b8 100644 --- a/src/Bonsai/Areas/Admin/Controllers/DraftsController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/DraftsController.cs @@ -8,72 +8,71 @@ using Bonsai.Data.Models; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for handling drafts. +/// +[Route("admin/drafts")] +public class DraftsController : AdminControllerBase { + public DraftsController(PagesManagerService pages, PagePresenterService pagePresenter, AppDbContext db) + { + _pages = pages; + _pagePresenter = pagePresenter; + _db = db; + } + + private readonly PagesManagerService _pages; + private readonly PagePresenterService _pagePresenter; + private readonly AppDbContext _db; + /// - /// Controller for handling drafts. + /// Discards the draft of a page. /// - [Route("admin/drafts")] - public class DraftsController : AdminControllerBase + [HttpPost] + [Route("update")] + public async Task Update(PageEditorVM vm) { - public DraftsController(PagesManagerService pages, PagePresenterService pagePresenter, AppDbContext db) - { - _pages = pages; - _pagePresenter = pagePresenter; - _db = db; - } + var info = await _pages.UpdatePageDraftAsync(vm, User); + await _db.SaveChangesAsync(); - private readonly PagesManagerService _pages; - private readonly PagePresenterService _pagePresenter; - private readonly AppDbContext _db; - - /// - /// Discards the draft of a page. - /// - [HttpPost] - [Route("update")] - public async Task Update(PageEditorVM vm) - { - var info = await _pages.UpdatePageDraftAsync(vm, User); - await _db.SaveChangesAsync(); + return Json(info); + } - return Json(info); - } + /// + /// Discards the draft of a page. + /// + [HttpPost] + [Route("remove")] + public async Task Remove(Guid? id, PageType? type) + { + await _pages.DiscardPageDraftAsync(id, User); + await _db.SaveChangesAsync(); - /// - /// Discards the draft of a page. - /// - [HttpPost] - [Route("remove")] - public async Task Remove(Guid? id, PageType? type) - { - await _pages.DiscardPageDraftAsync(id, User); - await _db.SaveChangesAsync(); + if (id != null && id != Guid.Empty) + return RedirectToAction("Update", "Pages", new { id = id }); - if (id != null && id != Guid.Empty) - return RedirectToAction("Update", "Pages", new { id = id }); + return type == null + ? RedirectToAction("Index", "Pages") + : RedirectToAction("Create", "Pages", new { type = type }); + } - return type == null - ? RedirectToAction("Index", "Pages") - : RedirectToAction("Create", "Pages", new { type = type }); - } + [HttpGet] + [Route("preview")] + public async Task Preview(Guid? id) + { + var page = await _pages.GetPageDraftPreviewAsync(id, User); + if (page == null) + return NotFound(); - [HttpGet] - [Route("preview")] - public async Task Preview(Guid? id) + var vm = new PageVM { - var page = await _pages.GetPageDraftPreviewAsync(id, User); - if (page == null) - return NotFound(); - - var vm = new PageVM - { - Body = await _pagePresenter.GetPageDescriptionAsync(page), - InfoBlock = await _pagePresenter.GetPageInfoBlockAsync(page) - }; - ViewBag.IsPreview = true; - ViewBag.DisableSearch = true; - return View("~/Areas/Front/Views/Page/Description.cshtml", vm); - } + Body = await _pagePresenter.GetPageDescriptionAsync(page), + InfoBlock = await _pagePresenter.GetPageInfoBlockAsync(page) + }; + ViewBag.IsPreview = true; + ViewBag.DisableSearch = true; + return View("~/Areas/Front/Views/Page/Description.cshtml", vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/DynamicConfigController.cs b/src/Bonsai/Areas/Admin/Controllers/DynamicConfigController.cs index 585be62f..9001aff1 100644 --- a/src/Bonsai/Areas/Admin/Controllers/DynamicConfigController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/DynamicConfigController.cs @@ -11,66 +11,65 @@ using Impworks.Utils.Linq; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for managing the global configuration. +/// +[Route("admin/config")] +public class DynamicConfigController: AdminControllerBase { + public DynamicConfigController(DynamicConfigManagerService configMgr, BonsaiConfigService config, IBackgroundJobService jobs, AppDbContext db, CacheService cache) + { + _configMgr = configMgr; + _config = config; + _jobs = jobs; + _db = db; + _cache = cache; + } + + private readonly DynamicConfigManagerService _configMgr; + private readonly BonsaiConfigService _config; + private readonly IBackgroundJobService _jobs; + private readonly AppDbContext _db; + private readonly CacheService _cache; + /// - /// Controller for managing the global configuration. + /// Displays the form and edits the config. /// - [Route("admin/config")] - public class DynamicConfigController: AdminControllerBase + [HttpGet] + [Route("")] + public async Task Index() { - public DynamicConfigController(DynamicConfigManagerService configMgr, BonsaiConfigService config, IBackgroundJobService jobs, AppDbContext db, CacheService cache) - { - _configMgr = configMgr; - _config = config; - _jobs = jobs; - _db = db; - _cache = cache; - } + var vm = await _configMgr.RequestUpdateAsync(); + return View(vm); + } - private readonly DynamicConfigManagerService _configMgr; - private readonly BonsaiConfigService _config; - private readonly IBackgroundJobService _jobs; - private readonly AppDbContext _db; - private readonly CacheService _cache; + /// + /// Updates the settings. + /// + [HttpPost] + [Route("")] + public async Task Update(UpdateDynamicConfigVM vm) + { + var oldValue = await _configMgr.RequestUpdateAsync(); + await _configMgr.UpdateAsync(vm); + await _db.SaveChangesAsync(); - /// - /// Displays the form and edits the config. - /// - [HttpGet] - [Route("")] - public async Task Index() - { - var vm = await _configMgr.RequestUpdateAsync(); - return View(vm); - } + _config.ResetCache(); - /// - /// Updates the settings. - /// - [HttpPost] - [Route("")] - public async Task Update(UpdateDynamicConfigVM vm) + if (IsTreeConfigChanged()) { - var oldValue = await _configMgr.RequestUpdateAsync(); - await _configMgr.UpdateAsync(vm); - await _db.SaveChangesAsync(); - - _config.ResetCache(); - - if (IsTreeConfigChanged()) - { - _cache.Remove(); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - } + _cache.Remove(); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); + } - return RedirectToSuccess(Texts.Admin_Config_SavedMessage); + return RedirectToSuccess(Texts.Admin_Config_SavedMessage); - bool IsTreeConfigChanged() - { - return oldValue.TreeRenderThoroughness != vm.TreeRenderThoroughness - || oldValue.TreeKinds?.JoinString(",") != vm.TreeKinds?.JoinString(","); - } + bool IsTreeConfigChanged() + { + return oldValue.TreeRenderThoroughness != vm.TreeRenderThoroughness + || oldValue.TreeKinds?.JoinString(",") != vm.TreeKinds?.JoinString(","); } } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/HelpController.cs b/src/Bonsai/Areas/Admin/Controllers/HelpController.cs index d05cce64..bb29f64a 100644 --- a/src/Bonsai/Areas/Admin/Controllers/HelpController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/HelpController.cs @@ -1,30 +1,29 @@ using Bonsai.Code.Services; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for displaying static guide pages. +/// +[Route("admin/help")] +public class HelpController: AdminControllerBase { /// - /// Controller for displaying static guide pages. + /// Displays the Markdown guide. /// - [Route("admin/help")] - public class HelpController: AdminControllerBase + [Route("markdown")] + public ActionResult Markdown() { - /// - /// Displays the Markdown guide. - /// - [Route("markdown")] - public ActionResult Markdown() - { - return View("Markdown." + LocaleProvider.GetLocaleCode()); - } + return View("Markdown." + LocaleProvider.GetLocaleCode()); + } - /// - /// Displays the editor guidelines. - /// - [Route("guidelines")] - public ActionResult Guidelines() - { - return View("Guidelines." + LocaleProvider.GetLocaleCode()); - } + /// + /// Displays the editor guidelines. + /// + [Route("guidelines")] + public ActionResult Guidelines() + { + return View("Guidelines." + LocaleProvider.GetLocaleCode()); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/MediaController.cs b/src/Bonsai/Areas/Admin/Controllers/MediaController.cs index d28d67ae..4a74f9f4 100644 --- a/src/Bonsai/Areas/Admin/Controllers/MediaController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/MediaController.cs @@ -20,232 +20,231 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for managing media files. +/// +[Route("admin/media")] +public class MediaController: AdminControllerBase { - /// - /// Controller for managing media files. - /// - [Route("admin/media")] - public class MediaController: AdminControllerBase + public MediaController(MediaManagerService media, PagesManagerService pages, IBackgroundJobService jobs, AppDbContext db) { - public MediaController(MediaManagerService media, PagesManagerService pages, IBackgroundJobService jobs, AppDbContext db) - { - _media = media; - _pages = pages; - _jobs = jobs; - _db = db; - } + _media = media; + _pages = pages; + _jobs = jobs; + _db = db; + } - private readonly MediaManagerService _media; - private readonly PagesManagerService _pages; - private readonly IBackgroundJobService _jobs; - private readonly AppDbContext _db; + private readonly MediaManagerService _media; + private readonly PagesManagerService _pages; + private readonly IBackgroundJobService _jobs; + private readonly AppDbContext _db; - protected override Type ListStateType => typeof(MediaListRequestVM); + protected override Type ListStateType => typeof(MediaListRequestVM); - /// - /// Displays the list of pages. - /// - [HttpGet] - public async Task Index(MediaListRequestVM request) - { - PersistListState(request); - var vm = await _media.GetMediaAsync(request); - return View(vm); - } + /// + /// Displays the list of pages. + /// + [HttpGet] + public async Task Index(MediaListRequestVM request) + { + PersistListState(request); + var vm = await _media.GetMediaAsync(request); + return View(vm); + } - /// - /// Displays the uploader form. - /// - [HttpGet] - [Route("upload")] - public async Task Upload() - { - return View(); - } + /// + /// Displays the uploader form. + /// + [HttpGet] + [Route("upload")] + public async Task Upload() + { + return View(); + } - /// - /// Handles a single file upload. - /// - [HttpPost] - [Route("upload")] - [ConfigurableRequestSizeLimit] - public async Task Upload(MediaUploadRequestVM vm, IFormFile file) + /// + /// Handles a single file upload. + /// + [HttpPost] + [Route("upload")] + [ConfigurableRequestSizeLimit] + public async Task Upload(MediaUploadRequestVM vm, IFormFile file) + { + try { - try - { - var result = await _media.UploadAsync(vm, file, User); - result.ThumbnailPath = Url.Content(result.ThumbnailPath); + var result = await _media.UploadAsync(vm, file, User); + result.ThumbnailPath = Url.Content(result.ThumbnailPath); - await _db.SaveChangesAsync(); + await _db.SaveChangesAsync(); - if (!result.IsProcessed) - await _jobs.RunAsync(JobBuilder.For().WithArgs(result.Id)); + if (!result.IsProcessed) + await _jobs.RunAsync(JobBuilder.For().WithArgs(result.Id)); - return Json(result); - } - catch (Exception ex) + return Json(result); + } + catch (Exception ex) + { + return Json(new { - return Json(new - { - Error = true, - Description = ex.Message - }); - } + Error = true, + Description = ex.Message + }); } + } - /// - /// Returns the thumbnail status for all specified media files. - /// - [HttpGet] - [Route("thumbs")] - public async Task GetThumbnails(IEnumerable ids) - { - var vms = await _media.GetThumbnailsAsync(ids); + /// + /// Returns the thumbnail status for all specified media files. + /// + [HttpGet] + [Route("thumbs")] + public async Task GetThumbnails(IEnumerable ids) + { + var vms = await _media.GetThumbnailsAsync(ids); - foreach(var vm in vms) - vm.ThumbnailPath = Url.Content(vm.ThumbnailPath); + foreach(var vm in vms) + vm.ThumbnailPath = Url.Content(vm.ThumbnailPath); - return Json(vms); - } + return Json(vms); + } - /// - /// Displays the update form for a media file. - /// - [HttpGet] - [Route("update")] - public async Task Update(Guid id, MediaEditorSaveAction? saveAction = null) - { - var vm = await _media.RequestUpdateAsync(id); + /// + /// Displays the update form for a media file. + /// + [HttpGet] + [Route("update")] + public async Task Update(Guid id, MediaEditorSaveAction? saveAction = null) + { + var vm = await _media.RequestUpdateAsync(id); - if (saveAction != null) - vm.SaveAction = saveAction.Value; + if (saveAction != null) + vm.SaveAction = saveAction.Value; + + return await ViewEditorFormAsync(vm); + } + /// + /// Updates the media data. + /// + [HttpPost] + [Route("update")] + public async Task Update(MediaEditorVM vm) + { + if(!ModelState.IsValid) return await ViewEditorFormAsync(vm); - } - /// - /// Updates the media data. - /// - [HttpPost] - [Route("update")] - public async Task Update(MediaEditorVM vm) + try { - if(!ModelState.IsValid) - return await ViewEditorFormAsync(vm); - - try - { - await _media.UpdateAsync(vm, User); - await _db.SaveChangesAsync(); - - ShowMessage(Texts.Admin_Media_UpdatedMessage); + await _media.UpdateAsync(vm, User); + await _db.SaveChangesAsync(); - if (vm.SaveAction == MediaEditorSaveAction.SaveAndShowNext) - { - var nextId = await _media.GetNextUntaggedMediaAsync(); - if (nextId != null) - return RedirectToAction("Update", new {id = nextId.Value, saveAction = vm.SaveAction}); - } + ShowMessage(Texts.Admin_Media_UpdatedMessage); - return RedirectToSuccess(); - } - catch (ValidationException ex) + if (vm.SaveAction == MediaEditorSaveAction.SaveAndShowNext) { - SetModelState(ex); - return await ViewEditorFormAsync(vm); + var nextId = await _media.GetNextUntaggedMediaAsync(); + if (nextId != null) + return RedirectToAction("Update", new {id = nextId.Value, saveAction = vm.SaveAction}); } - } - /// - /// Removes the media file. - /// - [HttpGet] - [Route("remove")] - public async Task Remove(Guid id) + return RedirectToSuccess(); + } + catch (ValidationException ex) { - ViewBag.Info = await _media.RequestRemoveAsync(id, User); - return View(new RemoveEntryRequestVM { Id = id }); + SetModelState(ex); + return await ViewEditorFormAsync(vm); } + } - /// - /// Removes the media file. - /// - [HttpPost] - [Route("remove")] - public async Task Remove(RemoveEntryRequestVM vm) - { - if (vm.RemoveCompletely) - await _media.RemoveCompletelyAsync(vm.Id, User); - else - await _media.RemoveAsync(vm.Id, User); + /// + /// Removes the media file. + /// + [HttpGet] + [Route("remove")] + public async Task Remove(Guid id) + { + ViewBag.Info = await _media.RequestRemoveAsync(id, User); + return View(new RemoveEntryRequestVM { Id = id }); + } + + /// + /// Removes the media file. + /// + [HttpPost] + [Route("remove")] + public async Task Remove(RemoveEntryRequestVM vm) + { + if (vm.RemoveCompletely) + await _media.RemoveCompletelyAsync(vm.Id, User); + else + await _media.RemoveAsync(vm.Id, User); - await _db.SaveChangesAsync(); + await _db.SaveChangesAsync(); - return RedirectToSuccess(Texts.Admin_Media_RemovedMessage); - } + return RedirectToSuccess(Texts.Admin_Media_RemovedMessage); + } - #region Helpers + #region Helpers - /// - /// Displays the editor. - /// - private async Task ViewEditorFormAsync(MediaEditorVM vm) - { - var depictedEntities = JsonConvert.DeserializeObject(vm.DepictedEntities); - var ids = depictedEntities.Select(x => x.PageId) - .Concat(new[] {vm.Location, vm.Event}.Select(x => x.TryParse())) - .ToList(); + /// + /// Displays the editor. + /// + private async Task ViewEditorFormAsync(MediaEditorVM vm) + { + var depictedEntities = JsonConvert.DeserializeObject(vm.DepictedEntities); + var ids = depictedEntities.Select(x => x.PageId) + .Concat(new[] {vm.Location, vm.Event}.Select(x => x.TryParse())) + .ToList(); - var pageLookup = await _pages.FindPagesByIdsAsync(ids); + var pageLookup = await _pages.FindPagesByIdsAsync(ids); - foreach(var depicted in depictedEntities) - if (depicted.PageId != null && pageLookup.TryGetValue(depicted.PageId.Value, out var depPage)) - depicted.ObjectTitle = depPage.Title; + foreach(var depicted in depictedEntities) + if (depicted.PageId != null && pageLookup.TryGetValue(depicted.PageId.Value, out var depPage)) + depicted.ObjectTitle = depPage.Title; - ViewBag.Data = new MediaEditorDataVM - { - EventItem = GetPageLookup(vm.Event), - LocationItem = GetPageLookup(vm.Location), - DepictedEntityItems = GetDepictedEntitiesList(), - SaveActions = ViewHelper.GetEnumSelectList(vm.SaveAction), - ThumbnailUrl = MediaPresenterService.GetSizedMediaPath(vm.FilePath, MediaSize.Large) - }; + ViewBag.Data = new MediaEditorDataVM + { + EventItem = GetPageLookup(vm.Event), + LocationItem = GetPageLookup(vm.Location), + DepictedEntityItems = GetDepictedEntitiesList(), + SaveActions = ViewHelper.GetEnumSelectList(vm.SaveAction), + ThumbnailUrl = MediaPresenterService.GetSizedMediaPath(vm.FilePath, MediaSize.Large) + }; - return View("Editor", vm); + return View("Editor", vm); - SelectListItem[] GetPageLookup(string key) - { - if (key.TryParse() is Guid id) - return pageLookup.TryGetValue(id, out var page) - ? new[] {new SelectListItem {Selected = true, Text = page.Title, Value = page.Id.ToString()}} - : Array.Empty(); + SelectListItem[] GetPageLookup(string key) + { + if (key.TryParse() is Guid id) + return pageLookup.TryGetValue(id, out var page) + ? [new SelectListItem {Selected = true, Text = page.Title, Value = page.Id.ToString()}] + : Array.Empty(); - return new[] {new SelectListItem {Selected = true, Text = key, Value = key}}; - } + return [new SelectListItem {Selected = true, Text = key, Value = key}]; + } + + IReadOnlyList GetDepictedEntitiesList() + { + var result = new List(); - IReadOnlyList GetDepictedEntitiesList() + foreach (var entity in depictedEntities) { - var result = new List(); + var title = entity.PageId is Guid id && pageLookup.TryGetValue(id, out var page) + ? page.Title + : entity.ObjectTitle; - foreach (var entity in depictedEntities) + result.Add(new SelectListItem { - var title = entity.PageId is Guid id && pageLookup.TryGetValue(id, out var page) - ? page.Title - : entity.ObjectTitle; - - result.Add(new SelectListItem - { - Selected = true, - Text = title, - Value = entity.PageId?.ToString() ?? entity.ObjectTitle - }); - } - - return result; + Selected = true, + Text = title, + Value = entity.PageId?.ToString() ?? entity.ObjectTitle + }); } - } - #endregion + return result; + } } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/PagesController.cs b/src/Bonsai/Areas/Admin/Controllers/PagesController.cs index a8f44780..4a3ca6d2 100644 --- a/src/Bonsai/Areas/Admin/Controllers/PagesController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/PagesController.cs @@ -23,221 +23,220 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for handling pages. +/// +[Route("admin/pages")] +public class PagesController: AdminControllerBase { + public PagesController(PagesManagerService pages, ISearchEngine search, AppDbContext db, IBackgroundJobService jobs) + { + _pages = pages; + _search = search; + _db = db; + _jobs = jobs; + } + + private readonly PagesManagerService _pages; + private readonly ISearchEngine _search; + private readonly AppDbContext _db; + private readonly IBackgroundJobService _jobs; + + protected override Type ListStateType => typeof(PagesListRequestVM); + + /// + /// Readable captions of the fields. + /// + private static IDictionary FieldCaptions = new Dictionary + { + [nameof(PageEditorVM.Title)] = Texts.Admin_Pages_Caption_Title, + [nameof(PageEditorVM.Description)] = Texts.Admin_Pages_Caption_Description, + [nameof(PageEditorVM.Facts)] = Texts.Admin_Pages_Caption_Facts, + [nameof(PageEditorVM.Aliases)] = Texts.Admin_Pages_Caption_Aliases, + [nameof(PageEditorVM.MainPhotoKey)] = Texts.Admin_Pages_Caption_Photo, + }; + /// - /// Controller for handling pages. + /// Displays the list of pages. /// - [Route("admin/pages")] - public class PagesController: AdminControllerBase + [HttpGet] + public async Task Index(PagesListRequestVM request) { - public PagesController(PagesManagerService pages, ISearchEngine search, AppDbContext db, IBackgroundJobService jobs) + PersistListState(request); + var vm = await _pages.GetPagesAsync(request); + return View(vm); + } + + /// + /// Dispays the editor form for a new page. + /// + [HttpGet] + [Route("create")] + public async Task Create([FromQuery]PageType type = PageType.Person) + { + var vm = await _pages.RequestCreateAsync(type, User); + if (vm.Type != type) { - _pages = pages; - _search = search; - _db = db; - _jobs = jobs; + TempData[NotificationsService.NOTE_PAGETYPE_RESET_FROM_DRAFT] = type; + return RedirectToAction("Create", "Pages", new {area = "Admin", type = vm.Type}); } - private readonly PagesManagerService _pages; - private readonly ISearchEngine _search; - private readonly AppDbContext _db; - private readonly IBackgroundJobService _jobs; + return await ViewEditorFormAsync(vm, displayDraft: true); + } - protected override Type ListStateType => typeof(PagesListRequestVM); + /// + /// Attempts to create a new page. + /// + [HttpPost] + [Route("create")] + public async Task Create(PageEditorVM vm) + { + if(!ModelState.IsValid) + return await ViewEditorFormAsync(vm); - /// - /// Readable captions of the fields. - /// - private static IDictionary FieldCaptions = new Dictionary + try { - [nameof(PageEditorVM.Title)] = Texts.Admin_Pages_Caption_Title, - [nameof(PageEditorVM.Description)] = Texts.Admin_Pages_Caption_Description, - [nameof(PageEditorVM.Facts)] = Texts.Admin_Pages_Caption_Facts, - [nameof(PageEditorVM.Aliases)] = Texts.Admin_Pages_Caption_Aliases, - [nameof(PageEditorVM.MainPhotoKey)] = Texts.Admin_Pages_Caption_Photo, - }; + var page = await _pages.CreateAsync(vm, User); + await _db.SaveChangesAsync(); + await _search.AddPageAsync(page); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - /// - /// Displays the list of pages. - /// - [HttpGet] - public async Task Index(PagesListRequestVM request) - { - PersistListState(request); - var vm = await _pages.GetPagesAsync(request); - return View(vm); + return RedirectToSuccess(Texts.Admin_Pages_CreatedMessage); } - - /// - /// Dispays the editor form for a new page. - /// - [HttpGet] - [Route("create")] - public async Task Create([FromQuery]PageType type = PageType.Person) + catch (ValidationException ex) { - var vm = await _pages.RequestCreateAsync(type, User); - if (vm.Type != type) - { - TempData[NotificationsService.NOTE_PAGETYPE_RESET_FROM_DRAFT] = type; - return RedirectToAction("Create", "Pages", new {area = "Admin", type = vm.Type}); - } - - return await ViewEditorFormAsync(vm, displayDraft: true); + SetModelState(ex); + return await ViewEditorFormAsync(vm); } + } - /// - /// Attempts to create a new page. - /// - [HttpPost] - [Route("create")] - public async Task Create(PageEditorVM vm) - { - if(!ModelState.IsValid) - return await ViewEditorFormAsync(vm); - - try - { - var page = await _pages.CreateAsync(vm, User); - await _db.SaveChangesAsync(); - await _search.AddPageAsync(page); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - - return RedirectToSuccess(Texts.Admin_Pages_CreatedMessage); - } - catch (ValidationException ex) - { - SetModelState(ex); - return await ViewEditorFormAsync(vm); - } - } + /// + /// Displays the editor for an existing page. + /// + [HttpGet] + [Route("update")] + public async Task Update(Guid id) + { + var vm = await _pages.RequestUpdateAsync(id, User); + return await ViewEditorFormAsync(vm, displayDraft: true); + } + + /// + /// Attempts to update the existing page. + /// + [HttpPost] + [Route("update")] + public async Task Update(PageEditorVM vm, string tab) + { + if(!ModelState.IsValid) + return await ViewEditorFormAsync(vm, tab); - /// - /// Displays the editor for an existing page. - /// - [HttpGet] - [Route("update")] - public async Task Update(Guid id) + try { - var vm = await _pages.RequestUpdateAsync(id, User); - return await ViewEditorFormAsync(vm, displayDraft: true); - } + var page = await _pages.UpdateAsync(vm, User); + await _db.SaveChangesAsync(); + await _search.AddPageAsync(page); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - /// - /// Attempts to update the existing page. - /// - [HttpPost] - [Route("update")] - public async Task Update(PageEditorVM vm, string tab) + return RedirectToSuccess(Texts.Admin_Pages_UpdatedMessage); + } + catch (ValidationException ex) { - if(!ModelState.IsValid) - return await ViewEditorFormAsync(vm, tab); - - try - { - var page = await _pages.UpdateAsync(vm, User); - await _db.SaveChangesAsync(); - await _search.AddPageAsync(page); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - - return RedirectToSuccess(Texts.Admin_Pages_UpdatedMessage); - } - catch (ValidationException ex) - { - SetModelState(ex); - return await ViewEditorFormAsync(vm, tab); - } + SetModelState(ex); + return await ViewEditorFormAsync(vm, tab); } + } - /// - /// Removes the page file. - /// - [HttpGet] - [Route("remove")] - public async Task Remove(Guid id) - { - ViewBag.Info = await _pages.RequestRemoveAsync(id, User); - return View(new RemoveEntryRequestVM { Id = id }); - } + /// + /// Removes the page file. + /// + [HttpGet] + [Route("remove")] + public async Task Remove(Guid id) + { + ViewBag.Info = await _pages.RequestRemoveAsync(id, User); + return View(new RemoveEntryRequestVM { Id = id }); + } - /// - /// Removes the page file. - /// - [HttpPost] - [Route("remove")] - public async Task Remove(RemoveEntryRequestVM vm) - { - if (vm.RemoveCompletely) - await _pages.RemoveCompletelyAsync(vm.Id, User); - else - await _pages.RemoveAsync(vm.Id, User); + /// + /// Removes the page file. + /// + [HttpPost] + [Route("remove")] + public async Task Remove(RemoveEntryRequestVM vm) + { + if (vm.RemoveCompletely) + await _pages.RemoveCompletelyAsync(vm.Id, User); + else + await _pages.RemoveAsync(vm.Id, User); - await _db.SaveChangesAsync(); - await _search.RemovePageAsync(vm.Id); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); + await _db.SaveChangesAsync(); + await _search.RemovePageAsync(vm.Id); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - return RedirectToSuccess(Texts.Admin_Pages_RemovedMessage); - } + return RedirectToSuccess(Texts.Admin_Pages_RemovedMessage); + } - #region Helpers + #region Helpers - /// - /// Displays the editor form. - /// - private async Task ViewEditorFormAsync(PageEditorVM vm, string tab = null, bool displayDraft = false) - { - var groups = FactDefinitions.Groups[vm.Type]; - var editorTpls = groups.SelectMany(x => x.Defs) - .Select(x => x.Kind) - .Distinct() - .Select(x => $"~/Areas/Admin/Views/Pages/Facts/{x.Name}.cshtml") - .ToList(); - - var errorFields = ModelState.Where(x => x.Value.ValidationState == ModelValidationState.Invalid) - .Select(x => FieldCaptions.TryGetValue(x.Key)) - .JoinString(", "); - - var photoThumbUrl = await GetMainPhotoThumbnailUrlAsync(vm.MainPhotoKey); - var draft = await _pages.GetPageDraftAsync(vm.Id, User); - - ViewBag.Data = new PageEditorDataVM - { - IsNew = vm.Id == Guid.Empty, - PageTypes = ViewHelper.GetEnumSelectList(vm.Type), - FactGroups = groups, - EditorTemplates = editorTpls, - Tab = StringHelper.Coalesce(tab, "main"), - ErrorFields = errorFields, - MainPhotoThumbnailUrl = photoThumbUrl, - DraftId = draft?.Id, - DraftLastUpdateDate = draft?.LastUpdateDate, - DraftDisplayNotification = draft != null && displayDraft - - }; - - return View("Editor", vm); - } + /// + /// Displays the editor form. + /// + private async Task ViewEditorFormAsync(PageEditorVM vm, string tab = null, bool displayDraft = false) + { + var groups = FactDefinitions.Groups[vm.Type]; + var editorTpls = groups.SelectMany(x => x.Defs) + .Select(x => x.Kind) + .Distinct() + .Select(x => $"~/Areas/Admin/Views/Pages/Facts/{x.Name}.cshtml") + .ToList(); + + var errorFields = ModelState.Where(x => x.Value.ValidationState == ModelValidationState.Invalid) + .Select(x => FieldCaptions.TryGetValue(x.Key)) + .JoinString(", "); - /// - /// Returns the thumbnail preview for a photo. - /// - private async Task GetMainPhotoThumbnailUrlAsync(string key) + var photoThumbUrl = await GetMainPhotoThumbnailUrlAsync(vm.MainPhotoKey); + var draft = await _pages.GetPageDraftAsync(vm.Id, User); + + ViewBag.Data = new PageEditorDataVM { - if (string.IsNullOrEmpty(key)) - return null; + IsNew = vm.Id == Guid.Empty, + PageTypes = ViewHelper.GetEnumSelectList(vm.Type), + FactGroups = groups, + EditorTemplates = editorTpls, + Tab = StringHelper.Coalesce(tab, "main"), + ErrorFields = errorFields, + MainPhotoThumbnailUrl = photoThumbUrl, + DraftId = draft?.Id, + DraftLastUpdateDate = draft?.LastUpdateDate, + DraftDisplayNotification = draft != null && displayDraft - var path = await _db.Media - .Where(x => x.Key == key && x.Type == MediaType.Photo) - .Select(x => x.FilePath) - .FirstOrDefaultAsync(); + }; - if (string.IsNullOrEmpty(path)) - return null; + return View("Editor", vm); + } - return MediaPresenterService.GetSizedMediaPath(path, MediaSize.Small); - } + /// + /// Returns the thumbnail preview for a photo. + /// + private async Task GetMainPhotoThumbnailUrlAsync(string key) + { + if (string.IsNullOrEmpty(key)) + return null; + + var path = await _db.Media + .Where(x => x.Key == key && x.Type == MediaType.Photo) + .Select(x => x.FilePath) + .FirstOrDefaultAsync(); - #endregion + if (string.IsNullOrEmpty(path)) + return null; + + return MediaPresenterService.GetSizedMediaPath(path, MediaSize.Small); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/RelationsController.cs b/src/Bonsai/Areas/Admin/Controllers/RelationsController.cs index f7de4642..5f2e8e9a 100644 --- a/src/Bonsai/Areas/Admin/Controllers/RelationsController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/RelationsController.cs @@ -17,219 +17,218 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for managing relations. +/// +[Route("admin/relations")] +public class RelationsController: AdminControllerBase { + public RelationsController(RelationsManagerService rels, PagesManagerService pages, AppDbContext db, IBackgroundJobService jobs) + { + _rels = rels; + _pages = pages; + _db = db; + _jobs = jobs; + } + + private readonly RelationsManagerService _rels; + private readonly PagesManagerService _pages; + private readonly AppDbContext _db; + private readonly IBackgroundJobService _jobs; + + protected override Type ListStateType => typeof(RelationsListRequestVM); + /// - /// Controller for managing relations. + /// Displays the list of relations. /// - [Route("admin/relations")] - public class RelationsController: AdminControllerBase + [HttpGet] + [Route("")] + public async Task Index(RelationsListRequestVM request) { - public RelationsController(RelationsManagerService rels, PagesManagerService pages, AppDbContext db, IBackgroundJobService jobs) - { - _rels = rels; - _pages = pages; - _db = db; - _jobs = jobs; - } - - private readonly RelationsManagerService _rels; - private readonly PagesManagerService _pages; - private readonly AppDbContext _db; - private readonly IBackgroundJobService _jobs; + PersistListState(request); + ViewBag.Data = await GetDataAsync(request); + var rels = await _rels.GetRelationsAsync(request); + return View(rels); + } - protected override Type ListStateType => typeof(RelationsListRequestVM); + /// + /// Dispays the editor form for a new relation. + /// + [HttpGet] + [Route("create")] + public async Task Create() + { + return await ViewEditorFormAsync(new RelationEditorVM { Type = RelationType.Child }); + } - /// - /// Displays the list of relations. - /// - [HttpGet] - [Route("")] - public async Task Index(RelationsListRequestVM request) - { - PersistListState(request); - ViewBag.Data = await GetDataAsync(request); - var rels = await _rels.GetRelationsAsync(request); - return View(rels); - } + /// + /// Attempts to create a new page. + /// + [HttpPost] + [Route("create")] + public async Task Create(RelationEditorVM vm) + { + if(!ModelState.IsValid) + return await ViewEditorFormAsync(vm); - /// - /// Dispays the editor form for a new relation. - /// - [HttpGet] - [Route("create")] - public async Task Create() + try { - return await ViewEditorFormAsync(new RelationEditorVM { Type = RelationType.Child }); - } + await _rels.CreateAsync(vm, User); + await _db.SaveChangesAsync(); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - /// - /// Attempts to create a new page. - /// - [HttpPost] - [Route("create")] - public async Task Create(RelationEditorVM vm) - { - if(!ModelState.IsValid) - return await ViewEditorFormAsync(vm); - - try - { - await _rels.CreateAsync(vm, User); - await _db.SaveChangesAsync(); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - - return RedirectToSuccess(Texts.Admin_Relations_CreatedMessage); - } - catch(ValidationException ex) - { - SetModelState(ex); - return await ViewEditorFormAsync(vm); - } + return RedirectToSuccess(Texts.Admin_Relations_CreatedMessage); } - - /// - /// Displays the editor for an existing relation. - /// - [HttpGet] - [Route("update")] - public async Task Update(Guid id) + catch(ValidationException ex) { - var vm = await _rels.RequestUpdateAsync(id); + SetModelState(ex); return await ViewEditorFormAsync(vm); } + } - /// - /// Attempts to update the existing relation. - /// - [HttpPost] - [Route("update")] - public async Task Update(RelationEditorVM vm) - { - if(!ModelState.IsValid) - return await ViewEditorFormAsync(vm); - - try - { - await _rels.UpdateAsync(vm, User); - await _db.SaveChangesAsync(); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - - return RedirectToSuccess(Texts.Admin_Relations_UpdatedMessage); - } - catch(ValidationException ex) - { - SetModelState(ex); - return await ViewEditorFormAsync(vm); - } - } + /// + /// Displays the editor for an existing relation. + /// + [HttpGet] + [Route("update")] + public async Task Update(Guid id) + { + var vm = await _rels.RequestUpdateAsync(id); + return await ViewEditorFormAsync(vm); + } - /// - /// Displays the relation removal confirmation. - /// - [HttpGet] - [Route("remove")] - public async Task Remove(Guid id) - { - ViewBag.Info = await _rels.RequestRemoveAsync(id, User); - return View(new RemoveEntryRequestVM { Id = id }); - } + /// + /// Attempts to update the existing relation. + /// + [HttpPost] + [Route("update")] + public async Task Update(RelationEditorVM vm) + { + if(!ModelState.IsValid) + return await ViewEditorFormAsync(vm); - /// - /// Removes the relation. - /// - [HttpPost] - [Route("remove")] - public async Task Remove(RemoveEntryRequestVM vm) + try { - if (vm.RemoveCompletely) - await _rels.RemoveCompletelyAsync(vm.Id, User); - else - await _rels.RemoveAsync(vm.Id, User); - + await _rels.UpdateAsync(vm, User); await _db.SaveChangesAsync(); await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - return RedirectToSuccess(Texts.Admin_Relations_RemovedMessage); + return RedirectToSuccess(Texts.Admin_Relations_UpdatedMessage); } - - /// - /// Returns editor properties for the selected relation type. - /// - [HttpGet] - [Route("editorProps")] - public async Task EditorProperties(RelationType relType) + catch(ValidationException ex) { - return Json(_rels.GetPropertiesForRelationType(relType)); + SetModelState(ex); + return await ViewEditorFormAsync(vm); } + } + + /// + /// Displays the relation removal confirmation. + /// + [HttpGet] + [Route("remove")] + public async Task Remove(Guid id) + { + ViewBag.Info = await _rels.RequestRemoveAsync(id, User); + return View(new RemoveEntryRequestVM { Id = id }); + } + + /// + /// Removes the relation. + /// + [HttpPost] + [Route("remove")] + public async Task Remove(RemoveEntryRequestVM vm) + { + if (vm.RemoveCompletely) + await _rels.RemoveCompletelyAsync(vm.Id, User); + else + await _rels.RemoveAsync(vm.Id, User); + + await _db.SaveChangesAsync(); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); + + return RedirectToSuccess(Texts.Admin_Relations_RemovedMessage); + } + + /// + /// Returns editor properties for the selected relation type. + /// + [HttpGet] + [Route("editorProps")] + public async Task EditorProperties(RelationType relType) + { + return Json(_rels.GetPropertiesForRelationType(relType)); + } + + #region Helpers + + /// + /// Displays the editor. + /// + private async Task ViewEditorFormAsync(RelationEditorVM vm) + { + if(vm.SourceIds == null) + vm.SourceIds = Array.Empty(); - #region Helpers + var pageIds = new[] {vm.DestinationId, vm.EventId}.Concat(vm.SourceIds.Cast()).ToList(); + var pageLookup = await _pages.FindPagesByIdsAsync(pageIds); - /// - /// Displays the editor. - /// - private async Task ViewEditorFormAsync(RelationEditorVM vm) + ViewBag.Data = new RelationEditorDataVM { - if(vm.SourceIds == null) - vm.SourceIds = Array.Empty(); - - var pageIds = new[] {vm.DestinationId, vm.EventId}.Concat(vm.SourceIds.Cast()).ToList(); - var pageLookup = await _pages.FindPagesByIdsAsync(pageIds); - - ViewBag.Data = new RelationEditorDataVM - { - IsNew = vm.Id == Guid.Empty, - SourceItems = GetPageLookup(vm.SourceIds), - DestinationItem = GetPageLookup(vm.DestinationId ?? Guid.Empty), - EventItem = GetPageLookup(vm.EventId ?? Guid.Empty), - - Properties = _rels.GetPropertiesForRelationType(vm.Type), - RelationTypes = EnumHelper.GetEnumValues() - .Select(x => new SelectListItem - { - Value = x.ToString(), - Text = x.GetLocaleEnumDescription(), - Selected = x == vm.Type - }) - }; - - return View("Editor", vm); - - IReadOnlyList GetPageLookup(params Guid[] ids) - { - var result = new List(); - - foreach(var id in ids) - if(pageLookup.TryGetValue(id, out var page)) - result.Add(new SelectListItem { Selected = true, Text = page.Title, Value = page.Id.ToString() }); - - return result; - } + IsNew = vm.Id == Guid.Empty, + SourceItems = GetPageLookup(vm.SourceIds), + DestinationItem = GetPageLookup(vm.DestinationId ?? Guid.Empty), + EventItem = GetPageLookup(vm.EventId ?? Guid.Empty), + + Properties = _rels.GetPropertiesForRelationType(vm.Type), + RelationTypes = EnumHelper.GetEnumValues() + .Select(x => new SelectListItem + { + Value = x.ToString(), + Text = x.GetLocaleEnumDescription(), + Selected = x == vm.Type + }) + }; + + return View("Editor", vm); + + IReadOnlyList GetPageLookup(params Guid[] ids) + { + var result = new List(); + + foreach(var id in ids) + if(pageLookup.TryGetValue(id, out var page)) + result.Add(new SelectListItem { Selected = true, Text = page.Title, Value = page.Id.ToString() }); + + return result; } + } - /// - /// Loads extra data for the filter. - /// - private async Task GetDataAsync(RelationsListRequestVM request) + /// + /// Loads extra data for the filter. + /// + private async Task GetDataAsync(RelationsListRequestVM request) + { + var data = new RelationsListDataVM(); + + if (request.EntityId != null) { - var data = new RelationsListDataVM(); - - if (request.EntityId != null) - { - var title = await _db.Pages - .Where(x => x.Id == request.EntityId) - .Select(x => x.Title) - .FirstOrDefaultAsync(); - - if (title != null) - data.EntityTitle = title; - else - request.EntityId = null; - } - - return data; + var title = await _db.Pages + .Where(x => x.Id == request.EntityId) + .Select(x => x.Title) + .FirstOrDefaultAsync(); + + if (title != null) + data.EntityTitle = title; + else + request.EntityId = null; } - #endregion + return data; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/SuggestController.cs b/src/Bonsai/Areas/Admin/Controllers/SuggestController.cs index b6d3228c..66106294 100644 --- a/src/Bonsai/Areas/Admin/Controllers/SuggestController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/SuggestController.cs @@ -5,63 +5,62 @@ using Bonsai.Data.Models; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for ajax lookups. +/// +[Route("admin")] +public class SuggestController: AdminControllerBase { - /// - /// Controller for ajax lookups. - /// - [Route("admin")] - public class SuggestController: AdminControllerBase + public SuggestController(SuggestService suggest) { - public SuggestController(SuggestService suggest) - { - _suggest = suggest; - } + _suggest = suggest; + } - private readonly SuggestService _suggest; + private readonly SuggestService _suggest; - /// - /// Suggests pages for relation destination / media tag. - /// - [HttpGet] - [Route("suggest/pages")] - public async Task SuggestPages([FromQuery] PickRequestVM vm) - { - var pages = await _suggest.SuggestPagesAsync(vm); - return Json(pages); - } + /// + /// Suggests pages for relation destination / media tag. + /// + [HttpGet] + [Route("suggest/pages")] + public async Task SuggestPages([FromQuery] PickRequestVM vm) + { + var pages = await _suggest.SuggestPagesAsync(vm); + return Json(pages); + } - /// - /// Suggests pages for relation source. - /// - [HttpGet] - [Route("suggest/pages/rel")] - public async Task SuggestPagesForRelationSource([FromQuery] RelationSuggestQueryVM vm) - { - var pages = await _suggest.SuggestRelationPagesAsync(vm); - return Json(pages); - } + /// + /// Suggests pages for relation source. + /// + [HttpGet] + [Route("suggest/pages/rel")] + public async Task SuggestPagesForRelationSource([FromQuery] RelationSuggestQueryVM vm) + { + var pages = await _suggest.SuggestRelationPagesAsync(vm); + return Json(pages); + } - /// - /// Returns data for page picker. - /// - [HttpGet] - [Route("pick/pages")] - public async Task PickPages([FromQuery] PickRequestVM vm) - { - var media = await _suggest.GetPickablePagesAsync(vm); - return Json(media); - } + /// + /// Returns data for page picker. + /// + [HttpGet] + [Route("pick/pages")] + public async Task PickPages([FromQuery] PickRequestVM vm) + { + var media = await _suggest.GetPickablePagesAsync(vm); + return Json(media); + } - /// - /// Returns data for media picker. - /// - [HttpGet] - [Route("pick/media")] - public async Task PickMedia([FromQuery] PickRequestVM vm) - { - var media = await _suggest.GetPickableMediaAsync(vm); - return Json(media); - } + /// + /// Returns data for media picker. + /// + [HttpGet] + [Route("pick/media")] + public async Task PickMedia([FromQuery] PickRequestVM vm) + { + var media = await _suggest.GetPickableMediaAsync(vm); + return Json(media); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/UsersController.cs b/src/Bonsai/Areas/Admin/Controllers/UsersController.cs index ff994688..168a2a13 100644 --- a/src/Bonsai/Areas/Admin/Controllers/UsersController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/UsersController.cs @@ -16,266 +16,265 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for managing users. +/// +[Route("admin/users")] +public class UsersController : AdminControllerBase { - /// - /// Controller for managing users. - /// - [Route("admin/users")] - public class UsersController : AdminControllerBase + public UsersController(UsersManagerService users, PagesManagerService pages, BonsaiConfigService config, ISearchEngine search, AppDbContext db) { - public UsersController(UsersManagerService users, PagesManagerService pages, BonsaiConfigService config, ISearchEngine search, AppDbContext db) - { - _users = users; - _pages = pages; - _config = config; - _search = search; - _db = db; - } + _users = users; + _pages = pages; + _config = config; + _search = search; + _db = db; + } - private readonly UsersManagerService _users; - private readonly PagesManagerService _pages; - private readonly BonsaiConfigService _config; - private readonly ISearchEngine _search; - private readonly AppDbContext _db; + private readonly UsersManagerService _users; + private readonly PagesManagerService _pages; + private readonly BonsaiConfigService _config; + private readonly ISearchEngine _search; + private readonly AppDbContext _db; - protected override Type ListStateType => typeof(UsersListRequestVM); + protected override Type ListStateType => typeof(UsersListRequestVM); - /// - /// Displays the list of users. - /// - [HttpGet] - [Route("")] - public async Task Index([FromQuery] UsersListRequestVM request) - { - PersistListState(request); - var users = await _users.GetUsersAsync(request); - ViewBag.AllowPasswordAuth = _config.GetStaticConfig().Auth.AllowPasswordAuth; - return View(users); - } + /// + /// Displays the list of users. + /// + [HttpGet] + [Route("")] + public async Task Index([FromQuery] UsersListRequestVM request) + { + PersistListState(request); + var users = await _users.GetUsersAsync(request); + ViewBag.AllowPasswordAuth = _config.GetStaticConfig().Auth.AllowPasswordAuth; + return View(users); + } - /// - /// Displays the remove confirmation for a user. - /// - [HttpGet] - [Route("remove")] - public async Task Remove(string id) - { - var vm = await _users.RequestRemoveAsync(id, User); - return View(vm); - } + /// + /// Displays the remove confirmation for a user. + /// + [HttpGet] + [Route("remove")] + public async Task Remove(string id) + { + var vm = await _users.RequestRemoveAsync(id, User); + return View(vm); + } - /// - /// Displays the remove confirmation for a user. - /// - [HttpPost] - [Route("remove")] - public async Task Remove(string id, bool confirm) - { - await _users.RemoveAsync(id, User); - return RedirectToSuccess(Texts.Admin_Users_RemovedMessage); - } + /// + /// Displays the remove confirmation for a user. + /// + [HttpPost] + [Route("remove")] + public async Task Remove(string id, bool confirm) + { + await _users.RemoveAsync(id, User); + return RedirectToSuccess(Texts.Admin_Users_RemovedMessage); + } - /// - /// Displays the update form for a user. - /// - [HttpGet] - [Route("update")] - public async Task Update(string id) - { - var vm = await _users.RequestUpdateAsync(id); + /// + /// Displays the update form for a user. + /// + [HttpGet] + [Route("update")] + public async Task Update(string id) + { + var vm = await _users.RequestUpdateAsync(id); + return await ViewUpdateFormAsync(vm); + } + + /// + /// Updates the user. + /// + [HttpPost] + [Route("update")] + public async Task Update(UserEditorVM vm) + { + if (!ModelState.IsValid) return await ViewUpdateFormAsync(vm); - } - /// - /// Updates the user. - /// - [HttpPost] - [Route("update")] - public async Task Update(UserEditorVM vm) + try { - if (!ModelState.IsValid) - return await ViewUpdateFormAsync(vm); - - try + if (vm.CreatePersonalPage && await _users.CanCreatePersonalPageAsync(vm)) { - if (vm.CreatePersonalPage && await _users.CanCreatePersonalPageAsync(vm)) - { - var page = await _pages.CreateDefaultUserPageAsync(vm, User); - vm.PersonalPageId = page.Id; - vm.CreatePersonalPage = false; + var page = await _pages.CreateDefaultUserPageAsync(vm, User); + vm.PersonalPageId = page.Id; + vm.CreatePersonalPage = false; - await _search.AddPageAsync(page); - } + await _search.AddPageAsync(page); + } - await _users.UpdateAsync(vm, User); + await _users.UpdateAsync(vm, User); - await _db.SaveChangesAsync(); + await _db.SaveChangesAsync(); - return RedirectToSuccess(Texts.Admin_Users_UpdatedMessage); - } - catch (ValidationException ex) - { - SetModelState(ex); - return await ViewUpdateFormAsync(vm); - } + return RedirectToSuccess(Texts.Admin_Users_UpdatedMessage); } - - /// - /// Displays the form for creating a new user. - /// - [HttpGet] - [Route("create")] - public async Task Create() + catch (ValidationException ex) { - CheckPasswordAuth(); - - return await ViewCreateFormAsync(new UserCreatorVM { Role = UserRole.User }); + SetModelState(ex); + return await ViewUpdateFormAsync(vm); } + } - /// - /// Creates a new user. - /// - [HttpPost] - [Route("create")] - public async Task Create(UserCreatorVM vm) - { - CheckPasswordAuth(); + /// + /// Displays the form for creating a new user. + /// + [HttpGet] + [Route("create")] + public async Task Create() + { + CheckPasswordAuth(); - if (!ModelState.IsValid) - return await ViewCreateFormAsync(vm); + return await ViewCreateFormAsync(new UserCreatorVM { Role = UserRole.User }); + } - try - { - if (vm.CreatePersonalPage) - { - var page = await _pages.CreateDefaultUserPageAsync(vm, User); - vm.PersonalPageId = page.Id; - vm.CreatePersonalPage = false; - - await _search.AddPageAsync(page); - } + /// + /// Creates a new user. + /// + [HttpPost] + [Route("create")] + public async Task Create(UserCreatorVM vm) + { + CheckPasswordAuth(); - await _users.CreateAsync(vm); - await _db.SaveChangesAsync(); + if (!ModelState.IsValid) + return await ViewCreateFormAsync(vm); - return RedirectToSuccess(Texts.Admin_Users_CreatedMessage); - } - catch (ValidationException ex) + try + { + if (vm.CreatePersonalPage) { - SetModelState(ex); - return await ViewCreateFormAsync(vm); + var page = await _pages.CreateDefaultUserPageAsync(vm, User); + vm.PersonalPageId = page.Id; + vm.CreatePersonalPage = false; + + await _search.AddPageAsync(page); } - } - /// - /// Displays the form for resetting a user password. - /// - [HttpGet] - [Route("reset-password")] - public async Task ResetPassword(string id) - { - CheckPasswordAuth(); + await _users.CreateAsync(vm); + await _db.SaveChangesAsync(); - return await ViewResetPasswordFormAsync(id); + return RedirectToSuccess(Texts.Admin_Users_CreatedMessage); } - - /// - /// Creates a new user. - /// - [HttpPost] - [Route("reset-password")] - public async Task ResetPassword(UserPasswordEditorVM vm) + catch (ValidationException ex) { - CheckPasswordAuth(); - - try - { - await _users.ResetPasswordAsync(vm); - return RedirectToSuccess(Texts.Admin_Users_PasswordResetMessage); - } - catch (ValidationException ex) - { - SetModelState(ex); - return await ViewResetPasswordFormAsync(vm.Id); - } + SetModelState(ex); + return await ViewCreateFormAsync(vm); } + } - #region Helpers + /// + /// Displays the form for resetting a user password. + /// + [HttpGet] + [Route("reset-password")] + public async Task ResetPassword(string id) + { + CheckPasswordAuth(); - /// - /// Displays the form for creating a new password-authorized user. - /// - private async Task ViewCreateFormAsync(UserCreatorVM vm) - { - var pageItems = await GetPageItemsAsync(vm.PersonalPageId); + return await ViewResetPasswordFormAsync(id); + } - ViewBag.Data = new UserEditorDataVM - { - IsSelf = false, - UserRoleItems = ViewHelper.GetEnumSelectList(vm.Role, except: new[] { UserRole.Unvalidated }), - PageItems = pageItems - }; + /// + /// Creates a new user. + /// + [HttpPost] + [Route("reset-password")] + public async Task ResetPassword(UserPasswordEditorVM vm) + { + CheckPasswordAuth(); - return View("Create", vm); + try + { + await _users.ResetPasswordAsync(vm); + return RedirectToSuccess(Texts.Admin_Users_PasswordResetMessage); } - - /// - /// Displays the password reset form. - /// - private async Task ViewResetPasswordFormAsync(string id) + catch (ValidationException ex) { - ViewBag.Data = await _users.GetAsync(id); - return View("ResetPassword", new UserPasswordEditorVM { Id = id }); + SetModelState(ex); + return await ViewResetPasswordFormAsync(vm.Id); } + } + + #region Helpers + + /// + /// Displays the form for creating a new password-authorized user. + /// + private async Task ViewCreateFormAsync(UserCreatorVM vm) + { + var pageItems = await GetPageItemsAsync(vm.PersonalPageId); - /// - /// Displays the UpdateUser form. - /// - private async Task ViewUpdateFormAsync(UserEditorVM vm) + ViewBag.Data = new UserEditorDataVM { - var canCreate = await _users.CanCreatePersonalPageAsync(vm); - var pageItems = await GetPageItemsAsync(vm.PersonalPageId); + IsSelf = false, + UserRoleItems = ViewHelper.GetEnumSelectList(vm.Role, except: [UserRole.Unvalidated]), + PageItems = pageItems + }; - ViewBag.Data = new UserEditorDataVM - { - IsSelf = _users.IsSelf(vm.Id, User), - UserRoleItems = ViewHelper.GetEnumSelectList(vm.Role), - CanCreatePersonalPage = canCreate, - PageItems = pageItems - }; + return View("Create", vm); + } - return View("Update", vm); - } + /// + /// Displays the password reset form. + /// + private async Task ViewResetPasswordFormAsync(string id) + { + ViewBag.Data = await _users.GetAsync(id); + return View("ResetPassword", new UserPasswordEditorVM { Id = id }); + } - /// - /// Returns the select list for a page picker. - /// - private async Task> GetPageItemsAsync(Guid? pageId) - { - if (pageId != null) - { - var page = await _db.Pages - .Where(x => x.Id == pageId) - .Select(x => x.Title) - .FirstOrDefaultAsync(); + /// + /// Displays the UpdateUser form. + /// + private async Task ViewUpdateFormAsync(UserEditorVM vm) + { + var canCreate = await _users.CanCreatePersonalPageAsync(vm); + var pageItems = await GetPageItemsAsync(vm.PersonalPageId); - if (!string.IsNullOrEmpty(page)) - return new[] { new SelectListItem(page, pageId.Value.ToString(), true) }; - } + ViewBag.Data = new UserEditorDataVM + { + IsSelf = _users.IsSelf(vm.Id, User), + UserRoleItems = ViewHelper.GetEnumSelectList(vm.Role), + CanCreatePersonalPage = canCreate, + PageItems = pageItems + }; - return Array.Empty(); - } + return View("Update", vm); + } - /// - /// Checks if password authorization is enabled. - /// - private void CheckPasswordAuth() + /// + /// Returns the select list for a page picker. + /// + private async Task> GetPageItemsAsync(Guid? pageId) + { + if (pageId != null) { - if (!_config.GetStaticConfig().Auth.AllowPasswordAuth) - throw new OperationException(Texts.Admin_Users_PasswordAuthRestrictedMessage); + var page = await _db.Pages + .Where(x => x.Id == pageId) + .Select(x => x.Title) + .FirstOrDefaultAsync(); + + if (!string.IsNullOrEmpty(page)) + return new[] { new SelectListItem(page, pageId.Value.ToString(), true) }; } - #endregion + return Array.Empty(); } -} + + /// + /// Checks if password authorization is enabled. + /// + private void CheckPasswordAuth() + { + if (!_config.GetStaticConfig().Auth.AllowPasswordAuth) + throw new OperationException(Texts.Admin_Users_PasswordAuthRestrictedMessage); + } + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Controllers/UtilityController.cs b/src/Bonsai/Areas/Admin/Controllers/UtilityController.cs index 32f13a8e..c2c4262c 100644 --- a/src/Bonsai/Areas/Admin/Controllers/UtilityController.cs +++ b/src/Bonsai/Areas/Admin/Controllers/UtilityController.cs @@ -2,29 +2,28 @@ using Bonsai.Areas.Admin.Logic; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Admin.Controllers +namespace Bonsai.Areas.Admin.Controllers; + +/// +/// Controller for various utility methods. +/// +[Route("admin/util")] +public class UtilityController: AdminControllerBase { - /// - /// Controller for various utility methods. - /// - [Route("admin/util")] - public class UtilityController: AdminControllerBase + public UtilityController(NotificationsService notifications) { - public UtilityController(NotificationsService notifications) - { - _notifications = notifications; - } + _notifications = notifications; + } - private readonly NotificationsService _notifications; + private readonly NotificationsService _notifications; - /// - /// Hides the notification. - /// - [Route("hideNotification")] - public async Task HideNotification(string id) - { - _notifications.Hide(id); - return Ok(); - } + /// + /// Hides the notification. + /// + [Route("hideNotification")] + public async Task HideNotification(string id) + { + _notifications.Hide(id); + return Ok(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthHandler.cs b/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthHandler.cs index 13ba81c6..d75500a9 100644 --- a/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthHandler.cs +++ b/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthHandler.cs @@ -2,24 +2,23 @@ using Bonsai.Data.Models; using Microsoft.AspNetCore.Authorization; -namespace Bonsai.Areas.Admin.Logic.Auth +namespace Bonsai.Areas.Admin.Logic.Auth; + +/// +/// Authorization handler for requiring login depending on the config. +/// +public class AdminAuthHandler: AuthorizationHandler { /// - /// Authorization handler for requiring login depending on the config. + /// Checks the authorization if the config requires it. /// - public class AdminAuthHandler: AuthorizationHandler + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminAuthRequirement requirement) { - /// - /// Checks the authorization if the config requires it. - /// - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminAuthRequirement requirement) - { - var user = context.User; - if (user == null) - return; + var user = context.User; + if (user == null) + return; - if(user.IsInRole(nameof(UserRole.Admin)) || user.IsInRole(nameof(UserRole.Editor))) - context.Succeed(requirement); - } + if(user.IsInRole(nameof(UserRole.Admin)) || user.IsInRole(nameof(UserRole.Editor))) + context.Succeed(requirement); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthRequirement.cs b/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthRequirement.cs index 6ec7bc03..40421414 100644 --- a/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthRequirement.cs +++ b/src/Bonsai/Areas/Admin/Logic/Auth/AdminAuthRequirement.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Authorization; -namespace Bonsai.Areas.Admin.Logic.Auth +namespace Bonsai.Areas.Admin.Logic.Auth; + +/// +/// Requirement for administrator access. +/// +public class AdminAuthRequirement: IAuthorizationRequirement { - /// - /// Requirement for administrator access. - /// - public class AdminAuthRequirement: IAuthorizationRequirement - { - public const string Name = "AdminAuthRequirement"; - } -} + public const string Name = "AdminAuthRequirement"; +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangePropertyValue.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangePropertyValue.cs index 6d3c0086..3722b01d 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangePropertyValue.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangePropertyValue.cs @@ -1,30 +1,9 @@ -namespace Bonsai.Areas.Admin.Logic.Changesets -{ - /// - /// Property name to value binding. - /// - public class ChangePropertyValue - { - public ChangePropertyValue(string propertyName, string title, string value) - { - PropertyName = propertyName; - Title = title; - Value = value; - } +namespace Bonsai.Areas.Admin.Logic.Changesets; - /// - /// Original name of the property (as in source code). - /// - public string PropertyName { get; } - - /// - /// Readable of the property. - /// - public string Title { get; } - - /// - /// Rendered value. - /// - public string Value { get; } - } -} +/// +/// Property name to value binding. +/// Original name of the property (as in source code). +/// Readable of the property. +/// Rendered value. +/// +public record ChangePropertyValue(string PropertyName, string Title, string Value); \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetHelper.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetHelper.cs index 63d713d4..758ae428 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetHelper.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetHelper.cs @@ -1,28 +1,27 @@ using System; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// Helper changeset-related methods used in multiple places. +/// +public static class ChangesetHelper { /// - /// Helper changeset-related methods used in multiple places. + /// Returns the changeset type. /// - public static class ChangesetHelper + public static ChangesetType GetChangeType(object prev, object next, Guid? revertedId) { - /// - /// Returns the changeset type. - /// - public static ChangesetType GetChangeType(object prev, object next, Guid? revertedId) - { - if (revertedId != null) - return ChangesetType.Restored; + if (revertedId != null) + return ChangesetType.Restored; - if (prev == null) - return ChangesetType.Created; + if (prev == null) + return ChangesetType.Created; - if (next == null) - return ChangesetType.Removed; + if (next == null) + return ChangesetType.Removed; - return ChangesetType.Updated; - } + return ChangesetType.Updated; } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetsManagerService.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetsManagerService.cs index 71256ad8..26c61983 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetsManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/ChangesetsManagerService.cs @@ -22,423 +22,422 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// The service for searching and displaying changesets. +/// +public class ChangesetsManagerService { + public ChangesetsManagerService( + IEnumerable renderers, + IWebHostEnvironment env, + MediaManagerService media, + PagesManagerService pages, + RelationsManagerService rels, + ISearchEngine search, + AppDbContext db + ) + { + _db = db; + _env = env; + _media = media; + _pages = pages; + _rels = rels; + _search = search; + _renderers = renderers.ToDictionary(x => x.EntityType, x => x); + } + + private readonly AppDbContext _db; + private readonly IWebHostEnvironment _env; + private readonly MediaManagerService _media; + private readonly PagesManagerService _pages; + private readonly RelationsManagerService _rels; + private readonly ISearchEngine _search; + private readonly IReadOnlyDictionary _renderers; + + #region Public methods + /// - /// The service for searching and displaying changesets. + /// Finds changesets. /// - public class ChangesetsManagerService + public async Task GetChangesetsAsync(ChangesetsListRequestVM request) { - public ChangesetsManagerService( - IEnumerable renderers, - IWebHostEnvironment env, - MediaManagerService media, - PagesManagerService pages, - RelationsManagerService rels, - ISearchEngine search, - AppDbContext db - ) + const int PageSize = 20; + + request = NormalizeListRequest(request); + + var result = new ChangesetsListVM { Request = request }; + await FillAdditionalDataAsync(request, result); + + var query = _db.Changes + .AsNoTracking() + .Include(x => x.Author) + .Include(x => x.EditedPage) + .ThenInclude(x => x.MainPhoto) + .Include(x => x.EditedMedia) + .Include(x => x.EditedRelation) + .ThenInclude(x => x.Source) + .Include(x => x.EditedRelation) + .ThenInclude(x => x.Destination) + .AsQueryable(); + + if (!string.IsNullOrEmpty(request.SearchQuery)) { - _db = db; - _env = env; - _media = media; - _pages = pages; - _rels = rels; - _search = search; - _renderers = renderers.ToDictionary(x => x.EntityType, x => x); + var search = PageHelper.NormalizeTitle(request.SearchQuery); + query = query.Where(x => (x.EditedPage != null && x.EditedPage.NormalizedTitle.Contains(search)) + || x.EditedMedia != null && x.EditedMedia.NormalizedTitle.Contains(search)); } - private readonly AppDbContext _db; - private readonly IWebHostEnvironment _env; - private readonly MediaManagerService _media; - private readonly PagesManagerService _pages; - private readonly RelationsManagerService _rels; - private readonly ISearchEngine _search; - private readonly IReadOnlyDictionary _renderers; - - #region Public methods + if (request.EntityTypes?.Length > 0) + query = query.Where(x => request.EntityTypes.Contains(x.EntityType)); + + if (request.ChangesetTypes?.Length > 0) + query = query.Where(x => request.ChangesetTypes.Contains(x.ChangeType)); + + if (request.EntityId != null) + query = query.Where(x => x.EditedPageId == request.EntityId + || x.EditedMediaId == request.EntityId + || x.EditedRelationId == request.EntityId); + + if (!string.IsNullOrEmpty(request.UserId)) + query = query.Where(x => x.Author.Id == request.UserId); + + var totalCount = await query.CountAsync(); + result.PageCount = (int)Math.Ceiling((double)totalCount / PageSize); + + var dir = request.OrderDescending.Value; + if (request.OrderBy == nameof(Changeset.Author)) + query = query.OrderBy(x => x.Author.UserName, dir); + else + query = query.OrderBy(x => x.Date, dir); + + var changesets = await query.Skip(PageSize * request.Page) + .Take(PageSize) + .ToListAsync(); + + result.Items = changesets.Select(x => new ChangesetTitleVM + { + Id = x.Id, + Date = x.Date, + ChangeType = x.ChangeType, + Author = x.Author.FirstName + " " + x.Author.LastName, + EntityId = x.EditedPageId ?? x.EditedMediaId ?? x.EditedRelationId ?? Guid.Empty, + EntityType = x.EntityType, + EntityTitle = GetEntityTitle(x), + EntityThumbnailUrl = GetEntityThumbnailUrl(x), + EntityExists = x.EditedMedia?.IsDeleted == false + || x.EditedPage?.IsDeleted == false + || x.EditedRelation?.IsDeleted == false, + EntityKey = x.EditedPage?.Key ?? x.EditedMedia?.Key, + PageType = GetPageType(x), + CanRevert = CanRevert(x) + }) + .ToList(); + + return result; + } - /// - /// Finds changesets. - /// - public async Task GetChangesetsAsync(ChangesetsListRequestVM request) + /// + /// Returns the details for a changeset. + /// + public async Task GetChangesetDetailsAsync(Guid id) + { + var (chg, prev) = await GetChangesetPairAsync( + id, + q => q.AsNoTracking() + .Include(x => x.Author) + .Include(x => x.EditedMedia) + .Include(x => x.EditedPage) + .Include(x => x.EditedRelation) + ); + + var renderer = _renderers[chg.EntityType]; + var prevData = await renderer.RenderValuesAsync(prev?.UpdatedState); + var nextData = await renderer.RenderValuesAsync(chg.UpdatedState); + + return new ChangesetDetailsVM { - const int PageSize = 20; - - request = NormalizeListRequest(request); - - var result = new ChangesetsListVM { Request = request }; - await FillAdditionalDataAsync(request, result); - - var query = _db.Changes - .AsNoTracking() - .Include(x => x.Author) - .Include(x => x.EditedPage) - .ThenInclude(x => x.MainPhoto) - .Include(x => x.EditedMedia) - .Include(x => x.EditedRelation) - .ThenInclude(x => x.Source) - .Include(x => x.EditedRelation) - .ThenInclude(x => x.Destination) - .AsQueryable(); - - if (!string.IsNullOrEmpty(request.SearchQuery)) - { - var search = PageHelper.NormalizeTitle(request.SearchQuery); - query = query.Where(x => (x.EditedPage != null && x.EditedPage.NormalizedTitle.Contains(search)) - || x.EditedMedia != null && x.EditedMedia.NormalizedTitle.Contains(search)); - } - - if (request.EntityTypes?.Length > 0) - query = query.Where(x => request.EntityTypes.Contains(x.EntityType)); - - if (request.ChangesetTypes?.Length > 0) - query = query.Where(x => request.ChangesetTypes.Contains(x.ChangeType)); - - if (request.EntityId != null) - query = query.Where(x => x.EditedPageId == request.EntityId - || x.EditedMediaId == request.EntityId - || x.EditedRelationId == request.EntityId); - - if (!string.IsNullOrEmpty(request.UserId)) - query = query.Where(x => x.Author.Id == request.UserId); - - var totalCount = await query.CountAsync(); - result.PageCount = (int)Math.Ceiling((double)totalCount / PageSize); - - var dir = request.OrderDescending.Value; - if (request.OrderBy == nameof(Changeset.Author)) - query = query.OrderBy(x => x.Author.UserName, dir); - else - query = query.OrderBy(x => x.Date, dir); - - var changesets = await query.Skip(PageSize * request.Page) - .Take(PageSize) - .ToListAsync(); - - result.Items = changesets.Select(x => new ChangesetTitleVM - { - Id = x.Id, - Date = x.Date, - ChangeType = x.ChangeType, - Author = x.Author.FirstName + " " + x.Author.LastName, - EntityId = x.EditedPageId ?? x.EditedMediaId ?? x.EditedRelationId ?? Guid.Empty, - EntityType = x.EntityType, - EntityTitle = GetEntityTitle(x), - EntityThumbnailUrl = GetEntityThumbnailUrl(x), - EntityExists = x.EditedMedia?.IsDeleted == false - || x.EditedPage?.IsDeleted == false - || x.EditedRelation?.IsDeleted == false, - EntityKey = x.EditedPage?.Key ?? x.EditedMedia?.Key, - PageType = GetPageType(x), - CanRevert = CanRevert(x) - }) - .ToList(); - - return result; - } + Id = chg.Id, + Author = string.Format(Texts.Admin_Changesets_AuthorNameFormat, chg.Author.FirstName, chg.Author.LastName), + Date = chg.Date, + ChangeType = chg.ChangeType, + EntityType = chg.EntityType, + EntityId = chg.EditedPageId ?? chg.EditedMediaId ?? chg.EditedRelationId ?? Guid.Empty, + EntityExists = chg.EditedMedia?.IsDeleted == false + || chg.EditedPage?.IsDeleted == false + || chg.EditedRelation?.IsDeleted == false, + EntityKey = chg.EditedPage?.Key ?? chg.EditedMedia?.Key, + ThumbnailUrl = chg.EditedMedia != null + ? MediaPresenterService.GetSizedMediaPath(chg.EditedMedia.FilePath, MediaSize.Small) + : null, + Changes = GetDiff(prevData, nextData, renderer), + CanRevert = CanRevert(chg) + }; + } - /// - /// Returns the details for a changeset. - /// - public async Task GetChangesetDetailsAsync(Guid id) + /// + /// Reverts a change. + /// + public async Task RevertChangeAsync(Guid id, ClaimsPrincipal user) + { + var (chg, prev) = await GetChangesetPairAsync(id, q => q.AsNoTracking()); + var isRemoving = string.IsNullOrEmpty(prev?.UpdatedState); + + switch (chg.EntityType) { - var (chg, prev) = await GetChangesetPairAsync( - id, - q => q.AsNoTracking() - .Include(x => x.Author) - .Include(x => x.EditedMedia) - .Include(x => x.EditedPage) - .Include(x => x.EditedRelation) - ); - - var renderer = _renderers[chg.EntityType]; - var prevData = await renderer.RenderValuesAsync(prev?.UpdatedState); - var nextData = await renderer.RenderValuesAsync(chg.UpdatedState); - - return new ChangesetDetailsVM + case ChangesetEntityType.Media: { - Id = chg.Id, - Author = string.Format(Texts.Admin_Changesets_AuthorNameFormat, chg.Author.FirstName, chg.Author.LastName), - Date = chg.Date, - ChangeType = chg.ChangeType, - EntityType = chg.EntityType, - EntityId = chg.EditedPageId ?? chg.EditedMediaId ?? chg.EditedRelationId ?? Guid.Empty, - EntityExists = chg.EditedMedia?.IsDeleted == false - || chg.EditedPage?.IsDeleted == false - || chg.EditedRelation?.IsDeleted == false, - EntityKey = chg.EditedPage?.Key ?? chg.EditedMedia?.Key, - ThumbnailUrl = chg.EditedMedia != null - ? MediaPresenterService.GetSizedMediaPath(chg.EditedMedia.FilePath, MediaSize.Small) - : null, - Changes = GetDiff(prevData, nextData, renderer), - CanRevert = CanRevert(chg) - }; - } + if (isRemoving) + { + await _media.RemoveAsync(chg.EditedMediaId.Value, user); + } + else + { + var vm = JsonConvert.DeserializeObject(prev.UpdatedState); + await _media.UpdateAsync(vm, user, id); + } - /// - /// Reverts a change. - /// - public async Task RevertChangeAsync(Guid id, ClaimsPrincipal user) - { - var (chg, prev) = await GetChangesetPairAsync(id, q => q.AsNoTracking()); - var isRemoving = string.IsNullOrEmpty(prev?.UpdatedState); - - switch (chg.EntityType) + return; + } + + case ChangesetEntityType.Page: { - case ChangesetEntityType.Media: + if (isRemoving) { - if (isRemoving) - { - await _media.RemoveAsync(chg.EditedMediaId.Value, user); - } - else - { - var vm = JsonConvert.DeserializeObject(prev.UpdatedState); - await _media.UpdateAsync(vm, user, id); - } - - return; + var pageId = chg.EditedPageId.Value; + await _pages.RemoveAsync(pageId, user); + await _search.RemovePageAsync(pageId); } - - case ChangesetEntityType.Page: + else { - if (isRemoving) - { - var pageId = chg.EditedPageId.Value; - await _pages.RemoveAsync(pageId, user); - await _search.RemovePageAsync(pageId); - } - else - { - var vm = JsonConvert.DeserializeObject(prev.UpdatedState); - var pageId = vm.Id == Guid.Empty ? chg.EditedPageId.Value : vm.Id; // workaround for a legacy bug - var page = await _pages.UpdateAsync(vm, user, id, pageId); - await _search.AddPageAsync(page); - } - return; + var vm = JsonConvert.DeserializeObject(prev.UpdatedState); + var pageId = vm.Id == Guid.Empty ? chg.EditedPageId.Value : vm.Id; // workaround for a legacy bug + var page = await _pages.UpdateAsync(vm, user, id, pageId); + await _search.AddPageAsync(page); } + return; + } - case ChangesetEntityType.Relation: + case ChangesetEntityType.Relation: + { + if (isRemoving) { - if (isRemoving) - { - await _rels.RemoveAsync(chg.EditedRelationId.Value, user); - } - else - { - var vm = JsonConvert.DeserializeObject(prev.UpdatedState); - await _rels.UpdateAsync(vm, user, id); - } - - return; + await _rels.RemoveAsync(chg.EditedRelationId.Value, user); + } + else + { + var vm = JsonConvert.DeserializeObject(prev.UpdatedState); + await _rels.UpdateAsync(vm, user, id); } - default: - throw new ArgumentException(string.Format(Texts.Admin_Changesets_UnknownEntityMessage, chg.EntityType)); + return; } - } - #endregion + default: + throw new ArgumentException(string.Format(Texts.Admin_Changesets_UnknownEntityMessage, chg.EntityType)); + } + } - #region Private helpers + #endregion - /// - /// Completes and\or corrects the search request. - /// - private ChangesetsListRequestVM NormalizeListRequest(ChangesetsListRequestVM vm) - { - vm ??= new ChangesetsListRequestVM(); + #region Private helpers - var orderableFields = new[] { nameof(Changeset.Date), nameof(Changeset.Author) }; - if (!orderableFields.Contains(vm.OrderBy)) - vm.OrderBy = orderableFields[0]; + /// + /// Completes and\or corrects the search request. + /// + private ChangesetsListRequestVM NormalizeListRequest(ChangesetsListRequestVM vm) + { + vm ??= new ChangesetsListRequestVM(); - if (vm.Page < 0) - vm.Page = 0; + var orderableFields = new[] { nameof(Changeset.Date), nameof(Changeset.Author) }; + if (!orderableFields.Contains(vm.OrderBy)) + vm.OrderBy = orderableFields[0]; - vm.OrderDescending ??= true; + if (vm.Page < 0) + vm.Page = 0; - return vm; - } + vm.OrderDescending ??= true; - /// - /// Returns the descriptive title for the changeset. - /// - private string GetEntityTitle(Changeset chg) - { - if (chg.EditedPage is { } p) - return p.Title; + return vm; + } - if (chg.EditedMedia is { } m) - return StringHelper.Coalesce(m.Title, MediaHelper.GetMediaFallbackTitle(m.Type, m.UploadDate)); + /// + /// Returns the descriptive title for the changeset. + /// + private string GetEntityTitle(Changeset chg) + { + if (chg.EditedPage is { } p) + return p.Title; - var rel = chg.EditedRelation; - var relType = rel.Type.GetLocaleEnumDescription(); - return string.Format(Texts.Admin_Changesets_RelationTitleFormat, relType, rel.Source.Title, rel.Destination.Title); - } + if (chg.EditedMedia is { } m) + return StringHelper.Coalesce(m.Title, MediaHelper.GetMediaFallbackTitle(m.Type, m.UploadDate)); - /// - /// Returns the thumbnail URL for the changeset. - /// - private string GetEntityThumbnailUrl(Changeset chg) - { - var file = chg.EditedPage?.MainPhoto?.FilePath ?? chg.EditedMedia?.FilePath; - if (file != null) - return MediaPresenterService.GetSizedMediaPath(file, MediaSize.Small); + var rel = chg.EditedRelation; + var relType = rel.Type.GetLocaleEnumDescription(); + return string.Format(Texts.Admin_Changesets_RelationTitleFormat, relType, rel.Source.Title, rel.Destination.Title); + } - return null; - } + /// + /// Returns the thumbnail URL for the changeset. + /// + private string GetEntityThumbnailUrl(Changeset chg) + { + var file = chg.EditedPage?.MainPhoto?.FilePath ?? chg.EditedMedia?.FilePath; + if (file != null) + return MediaPresenterService.GetSizedMediaPath(file, MediaSize.Small); - /// - /// Returns the page type (if any). - /// - private PageType? GetPageType(Changeset chg) - { - return chg.EditedPage?.Type; - } + return null; + } - /// - /// Returns the list of diffed values. - /// - private IReadOnlyList GetDiff(IReadOnlyList prevData, IReadOnlyList nextData, IChangesetRenderer renderer) - { - if (prevData.Count != nextData.Count) - throw new InvalidOperationException("Internal error: rendered changeset values mismatch!"); + /// + /// Returns the page type (if any). + /// + private PageType? GetPageType(Changeset chg) + { + return chg.EditedPage?.Type; + } - var result = new List(); + /// + /// Returns the list of diffed values. + /// + private IReadOnlyList GetDiff(IReadOnlyList prevData, IReadOnlyList nextData, IChangesetRenderer renderer) + { + if (prevData.Count != nextData.Count) + throw new InvalidOperationException("Internal error: rendered changeset values mismatch!"); - for (var idx = 0; idx < prevData.Count; idx++) - { - var prevValue = prevData[idx].Value; - var nextValue = nextData[idx].Value; + var result = new List(); - if (prevValue == nextValue) - continue; + for (var idx = 0; idx < prevData.Count; idx++) + { + var prevValue = prevData[idx].Value; + var nextValue = nextData[idx].Value; - var diff = renderer.GetCustomDiff(prevData[idx].PropertyName, prevValue, nextValue) - ?? new HtmlDiff.HtmlDiff(prevValue ?? "", nextValue ?? "").Build(); + if (prevValue == nextValue) + continue; - result.Add(new ChangeVM - { - Title = prevData[idx].Title, - Diff = diff - }); - } + var diff = renderer.GetCustomDiff(prevData[idx].PropertyName, prevValue, nextValue) + ?? new HtmlDiff.HtmlDiff(prevValue ?? "", nextValue ?? "").Build(); - return result; + result.Add(new ChangeVM + { + Title = prevData[idx].Title, + Diff = diff + }); } - /// - /// Returns the additional filter data. - /// - private async Task FillAdditionalDataAsync(ChangesetsListRequestVM request, ChangesetsListVM data) - { - if (!string.IsNullOrEmpty(request.UserId)) - { - var user = await _db.Users - .Where(x => x.Id == request.UserId) - .Select(x => new { x.FirstName, x.LastName }) - .FirstOrDefaultAsync(); + return result; + } - if (user != null) - data.UserTitle = string.Format(Texts.Admin_Changesets_AuthorNameFormat, user.FirstName, user.LastName); - else - request.UserId = null; - } + /// + /// Returns the additional filter data. + /// + private async Task FillAdditionalDataAsync(ChangesetsListRequestVM request, ChangesetsListVM data) + { + if (!string.IsNullOrEmpty(request.UserId)) + { + var user = await _db.Users + .Where(x => x.Id == request.UserId) + .Select(x => new { x.FirstName, x.LastName }) + .FirstOrDefaultAsync(); - if (request.EntityId != null) - { - var title = await GetPageTitleAsync() - ?? await GetMediaTitleAsync() - ?? await GetRelationTitleAsync(); + if (user != null) + data.UserTitle = string.Format(Texts.Admin_Changesets_AuthorNameFormat, user.FirstName, user.LastName); + else + request.UserId = null; + } - if (title != null) - data.EntityTitle = title; - else - request.EntityId = null; - } + if (request.EntityId != null) + { + var title = await GetPageTitleAsync() + ?? await GetMediaTitleAsync() + ?? await GetRelationTitleAsync(); - async Task GetPageTitleAsync() - { - return await _db.Pages - .Where(x => x.Id == request.EntityId) - .Select(x => x.Title) - .FirstOrDefaultAsync(); - } + if (title != null) + data.EntityTitle = title; + else + request.EntityId = null; + } - async Task GetMediaTitleAsync() - { - var media = await _db.Media - .Where(x => x.Id == request.EntityId) - .Select(x => new { x.Title }) - .FirstOrDefaultAsync(); - - return media == null - ? null - : StringHelper.Coalesce(media.Title, Texts.Admin_Changesets_MediaFallback); - } + async Task GetPageTitleAsync() + { + return await _db.Pages + .Where(x => x.Id == request.EntityId) + .Select(x => x.Title) + .FirstOrDefaultAsync(); + } - async Task GetRelationTitleAsync() - { - var rel = await _db.Relations + async Task GetMediaTitleAsync() + { + var media = await _db.Media .Where(x => x.Id == request.EntityId) - .Select(x => new { x.Type }) + .Select(x => new { x.Title }) .FirstOrDefaultAsync(); - return rel?.Type.GetLocaleEnumDescription(); - } + return media == null + ? null + : StringHelper.Coalesce(media.Title, Texts.Admin_Changesets_MediaFallback); } - /// - /// Checks if the changeset can be reverted. - /// - private bool CanRevert(Changeset chg) + async Task GetRelationTitleAsync() { - if (chg.ChangeType == ChangesetType.Restored) - return false; + var rel = await _db.Relations + .Where(x => x.Id == request.EntityId) + .Select(x => new { x.Type }) + .FirstOrDefaultAsync(); - if (chg.EditedMedia != null) - { - // if the file has been removed completely, revert is impossible - var file = _env.GetMediaPath(chg.EditedMedia); - return File.Exists(file); - } + return rel?.Type.GetLocaleEnumDescription(); + } + } + + /// + /// Checks if the changeset can be reverted. + /// + private bool CanRevert(Changeset chg) + { + if (chg.ChangeType == ChangesetType.Restored) + return false; - return true; + if (chg.EditedMedia != null) + { + // if the file has been removed completely, revert is impossible + var file = _env.GetMediaPath(chg.EditedMedia); + return File.Exists(file); } + + return true; + } - /// - /// Returns the current changeset by ID and the previous one related to current entity, if one exists. - /// - private async Task GetChangesetPairAsync(Guid id, Func, IQueryable> config = null) - { - config ??= x => x; + /// + /// Returns the current changeset by ID and the previous one related to current entity, if one exists. + /// + private async Task GetChangesetPairAsync(Guid id, Func, IQueryable> config = null) + { + config ??= x => x; - var chg = await config(_db.Changes).GetAsync(x => x.Id == id, Texts.Admin_Changesets_NotFound); + var chg = await config(_db.Changes).GetAsync(x => x.Id == id, Texts.Admin_Changesets_NotFound); - var prevQuery = config(_db.Changes); - if (chg.EditedMediaId != null) - prevQuery = prevQuery.Where(x => x.EditedMediaId == chg.EditedMediaId); - else if (chg.EditedPageId != null) - prevQuery = prevQuery.Where(x => x.EditedPageId == chg.EditedPageId); - else if (chg.EditedRelationId != null) - prevQuery = prevQuery.Where(x => x.EditedRelationId == chg.EditedRelationId); - - var prev = await prevQuery.OrderByDescending(x => x.Date) - .Where(x => x.Date < chg.Date) - .FirstOrDefaultAsync(); - - return new ChangesetPair(chg, prev); - } - - /// - /// Current and previous changesets for the same entity. - /// - /// Current changeset - /// The previous changeset related to current entity - private record ChangesetPair(Changeset current, Changeset previous); - - #endregion + var prevQuery = config(_db.Changes); + if (chg.EditedMediaId != null) + prevQuery = prevQuery.Where(x => x.EditedMediaId == chg.EditedMediaId); + else if (chg.EditedPageId != null) + prevQuery = prevQuery.Where(x => x.EditedPageId == chg.EditedPageId); + else if (chg.EditedRelationId != null) + prevQuery = prevQuery.Where(x => x.EditedRelationId == chg.EditedRelationId); + + var prev = await prevQuery.OrderByDescending(x => x.Date) + .Where(x => x.Date < chg.Date) + .FirstOrDefaultAsync(); + + return new ChangesetPair(chg, prev); } -} + + /// + /// Current and previous changesets for the same entity. + /// + /// Current changeset + /// The previous changeset related to current entity + private record ChangesetPair(Changeset current, Changeset previous); + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/IChangesetRenderer.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/IChangesetRenderer.cs index 148797dd..07d61926 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/IChangesetRenderer.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/IChangesetRenderer.cs @@ -2,23 +2,22 @@ using System.Threading.Tasks; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +public interface IChangesetRenderer { - public interface IChangesetRenderer - { - /// - /// Type of the entity this renderer can handle. - /// - ChangesetEntityType EntityType { get; } + /// + /// Type of the entity this renderer can handle. + /// + ChangesetEntityType EntityType { get; } - /// - /// Renders JSON representation of an editor into a set of property values. - /// - Task> RenderValuesAsync(string json); + /// + /// Renders JSON representation of an editor into a set of property values. + /// + Task> RenderValuesAsync(string json); - /// - /// Returns a customized diff for fields that are not supported by standard diff. - /// - string GetCustomDiff(string propName, string oldValue, string newValue); - } -} + /// + /// Returns a customized diff for fields that are not supported by standard diff. + /// + string GetCustomDiff(string propName, string oldValue, string newValue); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/IEditorVM.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/IEditorVM.cs index 3efdbf48..5c49edf2 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/IEditorVM.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/IEditorVM.cs @@ -1,15 +1,14 @@ using System; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// Common interface for viewmodels that can be stored as changesets. +/// +public interface IVersionable { /// - /// Common interface for viewmodels that can be stored as changesets. + /// ID of the entity. /// - public interface IVersionable - { - /// - /// ID of the entity. - /// - Guid Id { get; } - } -} + Guid Id { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/MediaChangesetRenderer.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/MediaChangesetRenderer.cs index 6bb3c1d1..ac0c69a6 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/MediaChangesetRenderer.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/MediaChangesetRenderer.cs @@ -14,77 +14,76 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// Renderer for media-related changesets. +/// +public class MediaChangesetRenderer: IChangesetRenderer { - /// - /// Renderer for media-related changesets. - /// - public class MediaChangesetRenderer: IChangesetRenderer + public MediaChangesetRenderer(IHtmlHelper html, AppDbContext db) { - public MediaChangesetRenderer(IHtmlHelper html, AppDbContext db) - { - _html = html; - _db = db; - } - - private readonly IHtmlHelper _html; - private readonly AppDbContext _db; + _html = html; + _db = db; + } - #region IChangesetRenderer implementation + private readonly IHtmlHelper _html; + private readonly AppDbContext _db; - /// - /// Supported type of changed entity. - /// - public ChangesetEntityType EntityType => ChangesetEntityType.Media; + #region IChangesetRenderer implementation - /// - /// Renders the properties. - /// - public async Task> RenderValuesAsync(string json) - { - var result = new List(); - var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); - var depicted = JsonConvert.DeserializeObject(StringHelper.Coalesce(data.DepictedEntities, "[]")); + /// + /// Supported type of changed entity. + /// + public ChangesetEntityType EntityType => ChangesetEntityType.Media; - var pageIds = depicted.Where(x => x.PageId != null) - .Select(x => x.PageId.Value) - .ToList(); + /// + /// Renders the properties. + /// + public async Task> RenderValuesAsync(string json) + { + var result = new List(); + var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); + var depicted = JsonConvert.DeserializeObject(StringHelper.Coalesce(data.DepictedEntities, "[]")); - var locId = data.Location.TryParse(); - var eventId = data.Event.TryParse(); + var pageIds = depicted.Where(x => x.PageId != null) + .Select(x => x.PageId.Value) + .ToList(); - if (locId != Guid.Empty) pageIds.Add(locId); - if (eventId != Guid.Empty) pageIds.Add(eventId); + var locId = data.Location.TryParse(); + var eventId = data.Event.TryParse(); - var namesLookup = await _db.Pages - .Where(x => pageIds.Contains(x.Id)) - .ToDictionaryAsync(x => x.Id, x => x.Title); + if (locId != Guid.Empty) pageIds.Add(locId); + if (eventId != Guid.Empty) pageIds.Add(eventId); - var deps = depicted.Select(x => string.Format("{0} ({1})", namesLookup[x.PageId ?? Guid.Empty] ?? x.ObjectTitle, x.Coordinates)); + var namesLookup = await _db.Pages + .Where(x => pageIds.Contains(x.Id)) + .ToDictionaryAsync(x => x.Id, x => x.Title); - Add(nameof(MediaEditorVM.Title), Texts.Admin_Changesets_Media_Title, data.Title); - Add(nameof(MediaEditorVM.Date), Texts.Admin_Changesets_Media_Date, data.Date != null ? FuzzyDate.Parse(data.Date).ReadableDate : null); - Add(nameof(MediaEditorVM.Description), Texts.Admin_Changesets_Media_Description, data.Description); - Add(nameof(MediaEditorVM.Location), Texts.Admin_Changesets_Media_Location, namesLookup.TryGetValue(locId) ?? data.Location); - Add(nameof(MediaEditorVM.Event), Texts.Admin_Changesets_Media_Event, namesLookup.TryGetValue(eventId) ?? data.Event); - Add(nameof(MediaEditorVM.DepictedEntities), Texts.Admin_Changesets_Media_DepictedEntities, depicted.Length == 0 ? null : ViewHelper.RenderBulletList(_html, deps)); + var deps = depicted.Select(x => string.Format("{0} ({1})", namesLookup[x.PageId ?? Guid.Empty] ?? x.ObjectTitle, x.Coordinates)); - return result; + Add(nameof(MediaEditorVM.Title), Texts.Admin_Changesets_Media_Title, data.Title); + Add(nameof(MediaEditorVM.Date), Texts.Admin_Changesets_Media_Date, data.Date != null ? FuzzyDate.Parse(data.Date).ReadableDate : null); + Add(nameof(MediaEditorVM.Description), Texts.Admin_Changesets_Media_Description, data.Description); + Add(nameof(MediaEditorVM.Location), Texts.Admin_Changesets_Media_Location, namesLookup.TryGetValue(locId) ?? data.Location); + Add(nameof(MediaEditorVM.Event), Texts.Admin_Changesets_Media_Event, namesLookup.TryGetValue(eventId) ?? data.Event); + Add(nameof(MediaEditorVM.DepictedEntities), Texts.Admin_Changesets_Media_DepictedEntities, depicted.Length == 0 ? null : ViewHelper.RenderBulletList(_html, deps)); - void Add(string prop, string name, string value) - { - result.Add(new ChangePropertyValue(prop, name, value)); - } - } + return result; - /// - /// Returns custom diffs. - /// - public string GetCustomDiff(string propName, string oldValue, string newValue) + void Add(string prop, string name, string value) { - return null; + result.Add(new ChangePropertyValue(prop, name, value)); } + } - #endregion + /// + /// Returns custom diffs. + /// + public string GetCustomDiff(string propName, string oldValue, string newValue) + { + return null; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/PageChangesetRenderer.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/PageChangesetRenderer.cs index 89282ef4..ea9857f6 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/PageChangesetRenderer.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/PageChangesetRenderer.cs @@ -18,127 +18,126 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// Renderer for page-related changesets. +/// +public class PageChangesetRenderer: IChangesetRenderer { - /// - /// Renderer for page-related changesets. - /// - public class PageChangesetRenderer: IChangesetRenderer + public PageChangesetRenderer(IHtmlHelper html, ViewRenderService viewRenderer, IUrlHelper url) { - public PageChangesetRenderer(IHtmlHelper html, ViewRenderService viewRenderer, IUrlHelper url) - { - _html = html; - _viewRenderer = viewRenderer; - _url = url; - } + _html = html; + _viewRenderer = viewRenderer; + _url = url; + } - private readonly IHtmlHelper _html; - private readonly IUrlHelper _url; - private readonly ViewRenderService _viewRenderer; + private readonly IHtmlHelper _html; + private readonly IUrlHelper _url; + private readonly ViewRenderService _viewRenderer; - #region IChangesetRenderer implementation + #region IChangesetRenderer implementation - /// - /// Supported type of changed entity. - /// - public ChangesetEntityType EntityType => ChangesetEntityType.Page; + /// + /// Supported type of changed entity. + /// + public ChangesetEntityType EntityType => ChangesetEntityType.Page; - /// - /// Renders the properties. - /// - public async Task> RenderValuesAsync(string json) + /// + /// Renders the properties. + /// + public async Task> RenderValuesAsync(string json) + { + var result = new List(); + var isEmpty = string.IsNullOrEmpty(json); + var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); + var aliases = JsonConvert.DeserializeObject(data.Aliases ?? "[]"); + var photoUrl = GetMediaThumbnailPath(data.MainPhotoKey); + var facts = await RenderFactsAsync(data.Type, data.Facts); + + Add(nameof(PageEditorVM.Title), Texts.Admin_Changesets_Page_Title, data.Title); + Add(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Changesets_Page_Photo, photoUrl == null ? null : ViewHelper.RenderMediaThumbnail(photoUrl)); + Add(nameof(PageEditorVM.Type), Texts.Admin_Changesets_Page_Type, isEmpty ? null : data.Type.GetLocaleEnumDescription()); + Add(nameof(PageEditorVM.Description), Texts.Admin_Changesets_Page_Text, data.Description); + Add(nameof(PageEditorVM.Aliases), Texts.Admin_Changesets_Page_Aliases, data.Aliases == null ? null : ViewHelper.RenderBulletList(_html, aliases)); + Add(nameof(PageEditorVM.Facts), Texts.Admin_Changesets_Page_Facts, facts); + + return result; + + void Add(string prop, string name, string value) { - var result = new List(); - var isEmpty = string.IsNullOrEmpty(json); - var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); - var aliases = JsonConvert.DeserializeObject(data.Aliases ?? "[]"); - var photoUrl = GetMediaThumbnailPath(data.MainPhotoKey); - var facts = await RenderFactsAsync(data.Type, data.Facts); - - Add(nameof(PageEditorVM.Title), Texts.Admin_Changesets_Page_Title, data.Title); - Add(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Changesets_Page_Photo, photoUrl == null ? null : ViewHelper.RenderMediaThumbnail(photoUrl)); - Add(nameof(PageEditorVM.Type), Texts.Admin_Changesets_Page_Type, isEmpty ? null : data.Type.GetLocaleEnumDescription()); - Add(nameof(PageEditorVM.Description), Texts.Admin_Changesets_Page_Text, data.Description); - Add(nameof(PageEditorVM.Aliases), Texts.Admin_Changesets_Page_Aliases, data.Aliases == null ? null : ViewHelper.RenderBulletList(_html, aliases)); - Add(nameof(PageEditorVM.Facts), Texts.Admin_Changesets_Page_Facts, facts); - - return result; - - void Add(string prop, string name, string value) - { - result.Add(new ChangePropertyValue(prop, name, value)); - } + result.Add(new ChangePropertyValue(prop, name, value)); } + } - /// - /// Returns custom diffs. - /// - public string GetCustomDiff(string propName, string oldValue, string newValue) + /// + /// Returns custom diffs. + /// + public string GetCustomDiff(string propName, string oldValue, string newValue) + { + if (propName == nameof(PageEditorVM.MainPhotoKey)) { - if (propName == nameof(PageEditorVM.MainPhotoKey)) - { - var sb = new StringBuilder(); - - if (oldValue != null) - sb.Append($@"{oldValue}"); + var sb = new StringBuilder(); - if (newValue != null) - sb.Append($@"{newValue}"); + if (oldValue != null) + sb.Append($@"{oldValue}"); - return sb.ToString(); - } + if (newValue != null) + sb.Append($@"{newValue}"); - return null; + return sb.ToString(); } - /// - /// Returns the full path for a photo. - /// - private string GetMediaThumbnailPath(string key) - { - if (key == null) - return null; + return null; + } - // hack: using original path with fake extension to avoid a DB query because thumbnails are always JPG - var path = MediaPresenterService.GetSizedMediaPath($"~/media/{key}.fake", MediaSize.Small); - return _url.Content(path); - } + /// + /// Returns the full path for a photo. + /// + private string GetMediaThumbnailPath(string key) + { + if (key == null) + return null; - /// - /// Renders the facts for display in changeset viewer. - /// - private async Task RenderFactsAsync(PageType pageType, string rawFacts) - { - if (string.IsNullOrEmpty(rawFacts)) - return null; + // hack: using original path with fake extension to avoid a DB query because thumbnails are always JPG + var path = MediaPresenterService.GetSizedMediaPath($"~/media/{key}.fake", MediaSize.Small); + return _url.Content(path); + } - var facts = JObject.Parse(rawFacts); - var vms = new List(); + /// + /// Renders the facts for display in changeset viewer. + /// + private async Task RenderFactsAsync(PageType pageType, string rawFacts) + { + if (string.IsNullOrEmpty(rawFacts)) + return null; - foreach (var group in FactDefinitions.Groups[pageType]) - { - var groupVm = new FactGroupVM { Definition = group, Facts = new List() }; - foreach (var fact in group.Defs) - { - var key = group.Id + "." + fact.Id; - var factInfo = facts[key]?.ToString(); + var facts = JObject.Parse(rawFacts); + var vms = new List(); - if (string.IsNullOrEmpty(factInfo)) - continue; + foreach (var group in FactDefinitions.Groups[pageType]) + { + var groupVm = new FactGroupVM { Definition = group, Facts = new List() }; + foreach (var fact in group.Defs) + { + var key = group.Id + "." + fact.Id; + var factInfo = facts[key]?.ToString(); - var factVm = (FactModelBase) JsonConvert.DeserializeObject(factInfo, fact.Kind); - factVm.Definition = fact; + if (string.IsNullOrEmpty(factInfo)) + continue; - groupVm.Facts.Add(factVm); - } + var factVm = (FactModelBase) JsonConvert.DeserializeObject(factInfo, fact.Kind); + factVm.Definition = fact; - if (groupVm.Facts.Any()) - vms.Add(groupVm); + groupVm.Facts.Add(factVm); } - return await _viewRenderer.RenderToStringAsync("~/Areas/Admin/Views/Changesets/Facts/FactsList.cshtml", vms); + if (groupVm.Facts.Any()) + vms.Add(groupVm); } - #endregion + return await _viewRenderer.RenderToStringAsync("~/Areas/Admin/Views/Changesets/Facts/FactsList.cshtml", vms); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Changesets/RelationChangesetRenderer.cs b/src/Bonsai/Areas/Admin/Logic/Changesets/RelationChangesetRenderer.cs index a93d9044..dba4739c 100644 --- a/src/Bonsai/Areas/Admin/Logic/Changesets/RelationChangesetRenderer.cs +++ b/src/Bonsai/Areas/Admin/Logic/Changesets/RelationChangesetRenderer.cs @@ -15,89 +15,88 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic.Changesets +namespace Bonsai.Areas.Admin.Logic.Changesets; + +/// +/// Renderer for relation changesets. +/// +public class RelationChangesetRenderer: IChangesetRenderer { + public RelationChangesetRenderer(IHtmlHelper html, AppDbContext db) + { + _html = html; + _db = db; + } + + private readonly IHtmlHelper _html; + private readonly AppDbContext _db; + + #region IChangesetRenderer implementation + /// - /// Renderer for relation changesets. + /// Supported relation type. /// - public class RelationChangesetRenderer: IChangesetRenderer + public ChangesetEntityType EntityType => ChangesetEntityType.Relation; + + /// + /// Renders the property values. + /// + public async Task> RenderValuesAsync(string json) { - public RelationChangesetRenderer(IHtmlHelper html, AppDbContext db) - { - _html = html; - _db = db; - } + var result = new List(); + var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); + + if (data.SourceIds == null) + data.SourceIds = Array.Empty(); - private readonly IHtmlHelper _html; - private readonly AppDbContext _db; + var pageIds = data.SourceIds + .Concat(new[] {data.DestinationId ?? Guid.Empty, data.EventId ?? Guid.Empty}) + .ToList(); - #region IChangesetRenderer implementation + var namesLookup = await _db.Pages + .Where(x => pageIds.Contains(x.Id)) + .ToDictionaryAsync(x => x.Id, x => x.Title); - /// - /// Supported relation type. - /// - public ChangesetEntityType EntityType => ChangesetEntityType.Relation; + Add(nameof(RelationEditorVM.DestinationId), Texts.Admin_Changesets_Relation_Destination, namesLookup.TryGetValue(data.DestinationId ?? Guid.Empty)); + Add(nameof(RelationEditorVM.Type), Texts.Admin_Changesets_Relation_Type, string.IsNullOrEmpty(json) ? null : data.Type.GetLocaleEnumDescription()); - /// - /// Renders the property values. - /// - public async Task> RenderValuesAsync(string json) + if (data.SourceIds.Length == 0) { - var result = new List(); - var data = JsonConvert.DeserializeObject(StringHelper.Coalesce(json, "{}")); - - if (data.SourceIds == null) - data.SourceIds = Array.Empty(); - - var pageIds = data.SourceIds - .Concat(new[] {data.DestinationId ?? Guid.Empty, data.EventId ?? Guid.Empty}) - .ToList(); - - var namesLookup = await _db.Pages - .Where(x => pageIds.Contains(x.Id)) - .ToDictionaryAsync(x => x.Id, x => x.Title); - - Add(nameof(RelationEditorVM.DestinationId), Texts.Admin_Changesets_Relation_Destination, namesLookup.TryGetValue(data.DestinationId ?? Guid.Empty)); - Add(nameof(RelationEditorVM.Type), Texts.Admin_Changesets_Relation_Type, string.IsNullOrEmpty(json) ? null : data.Type.GetLocaleEnumDescription()); - - if (data.SourceIds.Length == 0) - { - Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_Source, null); - } - else if (data.SourceIds.Length == 1) - { - var name = namesLookup.TryGetValue(data.SourceIds[0]); - Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_Source, name); - } - else - { - var pageNames = data.SourceIds - .Select(x => namesLookup.TryGetValue(x)) - .Where(x => !string.IsNullOrEmpty(x)); - - Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_SourceM, ViewHelper.RenderBulletList(_html, pageNames)); - } - - Add(nameof(RelationEditorVM.EventId), Texts.Admin_Changesets_Relation_Event, namesLookup.TryGetValue(data.EventId ?? Guid.Empty)); - Add(nameof(RelationEditorVM.DurationStart), Texts.Admin_Changesets_Relation_Start, FuzzyDate.TryParse(data.DurationStart)?.ReadableDate); - Add(nameof(RelationEditorVM.DurationEnd), Texts.Admin_Changesets_Relation_End, FuzzyDate.TryParse(data.DurationEnd)?.ReadableDate); - - return result; + Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_Source, null); + } + else if (data.SourceIds.Length == 1) + { + var name = namesLookup.TryGetValue(data.SourceIds[0]); + Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_Source, name); + } + else + { + var pageNames = data.SourceIds + .Select(x => namesLookup.TryGetValue(x)) + .Where(x => !string.IsNullOrEmpty(x)); - void Add(string prop, string name, string value) - { - result.Add(new ChangePropertyValue(prop, name, value)); - } + Add(nameof(RelationEditorVM.SourceIds), Texts.Admin_Changesets_Relation_SourceM, ViewHelper.RenderBulletList(_html, pageNames)); } - /// - /// Returns custom diffs. - /// - public string GetCustomDiff(string propName, string oldValue, string newValue) + Add(nameof(RelationEditorVM.EventId), Texts.Admin_Changesets_Relation_Event, namesLookup.TryGetValue(data.EventId ?? Guid.Empty)); + Add(nameof(RelationEditorVM.DurationStart), Texts.Admin_Changesets_Relation_Start, FuzzyDate.TryParse(data.DurationStart)?.ReadableDate); + Add(nameof(RelationEditorVM.DurationEnd), Texts.Admin_Changesets_Relation_End, FuzzyDate.TryParse(data.DurationEnd)?.ReadableDate); + + return result; + + void Add(string prop, string name, string value) { - return null; + result.Add(new ChangePropertyValue(prop, name, value)); } + } - #endregion + /// + /// Returns custom diffs. + /// + public string GetCustomDiff(string propName, string oldValue, string newValue) + { + return null; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/DashboardPresenterService.cs b/src/Bonsai/Areas/Admin/Logic/DashboardPresenterService.cs index 1a860fb6..e65b9506 100644 --- a/src/Bonsai/Areas/Admin/Logic/DashboardPresenterService.cs +++ b/src/Bonsai/Areas/Admin/Logic/DashboardPresenterService.cs @@ -19,128 +19,127 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// Service for displaying dashboard info. +/// +public class DashboardPresenterService { + public DashboardPresenterService(AppDbContext db, IWebHostEnvironment env, IUrlHelper url, IMapper mapper) + { + _db = db; + _env = env; + _url = url; + _mapper = mapper; + } + + private readonly AppDbContext _db; + private readonly IWebHostEnvironment _env; + private readonly IUrlHelper _url; + private readonly IMapper _mapper; + /// - /// Service for displaying dashboard info. + /// Returns the dashboard data. /// - public class DashboardPresenterService + public async Task GetDashboardAsync() { - public DashboardPresenterService(AppDbContext db, IWebHostEnvironment env, IUrlHelper url, IMapper mapper) + return new DashboardVM { - _db = db; - _env = env; - _url = url; - _mapper = mapper; - } + Events = await GetEventsAsync().ToListAsync(), + PagesCount = await _db.Pages.CountAsync(x => x.IsDeleted == false), + PagesToImproveCount = await _db.PagesScored.CountAsync(x => x.IsDeleted == false && x.CompletenessScore <= 50), + MediaCount = await _db.Media.CountAsync(x => x.IsDeleted == false), + MediaToTagCount = await _db.Media.CountAsync(x => x.IsDeleted == false && !x.Tags.Any()), + RelationsCount = await _db.Relations.CountAsync(x => x.IsComplementary == false && x.IsDeleted == false), + UsersCount = await _db.Users.CountAsync(x => x.IsValidated), + UsersPendingValidationCount = await _db.Users.CountAsync(x => x.IsValidated == false), + }; + } - private readonly AppDbContext _db; - private readonly IWebHostEnvironment _env; - private readonly IUrlHelper _url; - private readonly IMapper _mapper; + /// + /// Returns the list of changeset groups at given offset. + /// + public async IAsyncEnumerable GetEventsAsync(int page = 0) + { + const int PAGE_SIZE = 20; - /// - /// Returns the dashboard data. - /// - public async Task GetDashboardAsync() - { - return new DashboardVM - { - Events = await GetEventsAsync().ToListAsync(), - PagesCount = await _db.Pages.CountAsync(x => x.IsDeleted == false), - PagesToImproveCount = await _db.PagesScored.CountAsync(x => x.IsDeleted == false && x.CompletenessScore <= 50), - MediaCount = await _db.Media.CountAsync(x => x.IsDeleted == false), - MediaToTagCount = await _db.Media.CountAsync(x => x.IsDeleted == false && !x.Tags.Any()), - RelationsCount = await _db.Relations.CountAsync(x => x.IsComplementary == false && x.IsDeleted == false), - UsersCount = await _db.Users.CountAsync(x => x.IsValidated), - UsersPendingValidationCount = await _db.Users.CountAsync(x => x.IsValidated == false), - }; - } + var groups = await _db.ChangeEvents + .OrderByDescending(x => x.Date) + .Skip(PAGE_SIZE * page) + .Take(PAGE_SIZE) + .ToListAsync(); - /// - /// Returns the list of changeset groups at given offset. - /// - public async IAsyncEnumerable GetEventsAsync(int page = 0) - { - const int PAGE_SIZE = 20; + var parsedGroups = groups.Select(x => new {x.GroupKey, Ids = x.Ids.Split(',').Select(y => y.Parse()).ToList()}) + .ToList(); - var groups = await _db.ChangeEvents - .OrderByDescending(x => x.Date) - .Skip(PAGE_SIZE * page) - .Take(PAGE_SIZE) - .ToListAsync(); + var changeIds = parsedGroups.SelectMany(x => x.Ids).ToList(); + var changes = await _db.Changes + .AsNoTracking() + .Include(x => x.EditedMedia) + .Include(x => x.EditedPage) + .Include(x => x.EditedRelation.Destination) + .Include(x => x.EditedRelation.Source) + .Include(x => x.Author) + .Where(x => changeIds.Contains(x.Id)) + .ToDictionaryAsync(x => x.Id, x => x); - var parsedGroups = groups.Select(x => new {x.GroupKey, Ids = x.Ids.Split(',').Select(y => y.Parse()).ToList()}) - .ToList(); + // todo: cache genders in the user table to avoid loading the entire context + var ctx = await RelationContext.LoadContextAsync(_db, new RelationContextOptions {PagesOnly = true, PeopleOnly = true}); - var changeIds = parsedGroups.SelectMany(x => x.Ids).ToList(); - var changes = await _db.Changes - .AsNoTracking() - .Include(x => x.EditedMedia) - .Include(x => x.EditedPage) - .Include(x => x.EditedRelation.Destination) - .Include(x => x.EditedRelation.Source) - .Include(x => x.Author) - .Where(x => changeIds.Contains(x.Id)) - .ToDictionaryAsync(x => x.Id, x => x); + foreach (var group in parsedGroups) + { + var chg = changes[group.Ids.First()]; + var vm = _mapper.Map(chg); - // todo: cache genders in the user table to avoid loading the entire context - var ctx = await RelationContext.LoadContextAsync(_db, new RelationContextOptions {PagesOnly = true, PeopleOnly = true}); + if (vm.User.PageId != null) + vm.User.IsMale = ctx.Pages[vm.User.PageId.Value].Gender; - foreach (var group in parsedGroups) + if (chg.EntityType == ChangesetEntityType.Page) { - var chg = changes[group.Ids.First()]; - var vm = _mapper.Map(chg); - - if (vm.User.PageId != null) - vm.User.IsMale = ctx.Pages[vm.User.PageId.Value].Gender; - - if (chg.EntityType == ChangesetEntityType.Page) - { - vm.MainLink = GetLinkToPage(chg.EditedPage); - vm.ElementCount = 1; - } - else if (chg.EntityType == ChangesetEntityType.Relation) - { - var rel = chg.EditedRelation; - vm.MainLink = new LinkVM - { - Title = chg.EditedRelation.Type.GetLocaleEnumDescription(), - Url = _url.Action("Update", "Relations", new { area = "Admin", id = rel.Id }) - }; - vm.ExtraLinks = new[] {GetLinkToPage(rel.Destination), GetLinkToPage(rel.Source)}; - vm.ElementCount = 1; - } - else if (chg.EntityType == ChangesetEntityType.Media) + vm.MainLink = GetLinkToPage(chg.EditedPage); + vm.ElementCount = 1; + } + else if (chg.EntityType == ChangesetEntityType.Relation) + { + var rel = chg.EditedRelation; + vm.MainLink = new LinkVM { - vm.ElementCount = group.Ids.Count; - vm.MediaThumbnails = group.Ids - .Take(50) - .Select(x => changes[x].EditedMedia) - .Where(x => File.Exists(_env.GetMediaPath(x))) - .Select(x => new MediaThumbnailVM - { - Key = x.Key, - Type = x.Type, - ThumbnailUrl = MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small), - }) - .ToList(); - } - - yield return vm; + Title = chg.EditedRelation.Type.GetLocaleEnumDescription(), + Url = _url.Action("Update", "Relations", new { area = "Admin", id = rel.Id }) + }; + vm.ExtraLinks = new[] {GetLinkToPage(rel.Destination), GetLinkToPage(rel.Source)}; + vm.ElementCount = 1; + } + else if (chg.EntityType == ChangesetEntityType.Media) + { + vm.ElementCount = group.Ids.Count; + vm.MediaThumbnails = group.Ids + .Take(50) + .Select(x => changes[x].EditedMedia) + .Where(x => File.Exists(_env.GetMediaPath(x))) + .Select(x => new MediaThumbnailVM + { + Key = x.Key, + Type = x.Type, + ThumbnailUrl = MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small), + }) + .ToList(); } + + yield return vm; } + } - /// - /// Builds a link to the page. - /// - private LinkVM GetLinkToPage(Page page) + /// + /// Builds a link to the page. + /// + private LinkVM GetLinkToPage(Page page) + { + return new LinkVM { - return new LinkVM - { - Title = page.Title, - Url = page.IsDeleted ? null : _url.Action("Description", "Page", new {area = "Front", key = page.Key}) - }; - } + Title = page.Title, + Url = page.IsDeleted ? null : _url.Action("Description", "Page", new {area = "Front", key = page.Key}) + }; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/DynamicConfigManagerService.cs b/src/Bonsai/Areas/Admin/Logic/DynamicConfigManagerService.cs index 2817448d..16404775 100644 --- a/src/Bonsai/Areas/Admin/Logic/DynamicConfigManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/DynamicConfigManagerService.cs @@ -6,41 +6,40 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// Service for managing the global app configuration. +/// +public class DynamicConfigManagerService { - /// - /// Service for managing the global app configuration. - /// - public class DynamicConfigManagerService + public DynamicConfigManagerService(AppDbContext db, BonsaiConfigService cfgService, IMapper mapper) { - public DynamicConfigManagerService(AppDbContext db, BonsaiConfigService cfgService, IMapper mapper) - { - _db = db; - _cfgService = cfgService; - _mapper = mapper; - } + _db = db; + _cfgService = cfgService; + _mapper = mapper; + } - private readonly AppDbContext _db; - private readonly BonsaiConfigService _cfgService; - private readonly IMapper _mapper; + private readonly AppDbContext _db; + private readonly BonsaiConfigService _cfgService; + private readonly IMapper _mapper; - /// - /// Gets the update form default values. - /// - public async Task RequestUpdateAsync() - { - var config = _cfgService.GetDynamicConfig(); - return _mapper.Map(config); - } + /// + /// Gets the update form default values. + /// + public async Task RequestUpdateAsync() + { + var config = _cfgService.GetDynamicConfig(); + return _mapper.Map(config); + } - /// - /// Updates the current configuration. - /// - public async Task UpdateAsync(UpdateDynamicConfigVM request) - { - var wrapper = await _db.DynamicConfig.FirstAsync(); - var config = _mapper.Map(request); - wrapper.Value = JsonConvert.SerializeObject(config); - } + /// + /// Updates the current configuration. + /// + public async Task UpdateAsync(UpdateDynamicConfigVM request) + { + var wrapper = await _db.DynamicConfig.FirstAsync(); + var config = _mapper.Map(request); + wrapper.Value = JsonConvert.SerializeObject(config); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/IMediaHandler.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/IMediaHandler.cs index b15c2bc1..daee4f7c 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/IMediaHandler.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/IMediaHandler.cs @@ -2,36 +2,35 @@ using Bonsai.Data.Models; using SixLabors.ImageSharp; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// Common interface for handling an uploaded media file. +/// +public interface IMediaHandler { /// - /// Common interface for handling an uploaded media file. + /// Flag indicating that the media is immediately available (does not need to wait for encoding). /// - public interface IMediaHandler - { - /// - /// Flag indicating that the media is immediately available (does not need to wait for encoding). - /// - bool IsImmediate { get; } + bool IsImmediate { get; } - /// - /// List of mime types this handler accepts. - /// - string[] SupportedMimeTypes { get; } + /// + /// List of mime types this handler accepts. + /// + string[] SupportedMimeTypes { get; } - /// - /// Media file classification. - /// - MediaType MediaType { get; } + /// + /// Media file classification. + /// + MediaType MediaType { get; } - /// - /// Creates thumbnail files for this media file. - /// - Task ExtractThumbnailAsync(string path, string mime); + /// + /// Creates thumbnail files for this media file. + /// + Task ExtractThumbnailAsync(string path, string mime); - /// - /// Extracts additional data from the media. - /// - Task ExtractMetadataAsync(string path, string mime); - } -} + /// + /// Extracts additional data from the media. + /// + Task ExtractMetadataAsync(string path, string mime); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaHandlerHelper.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaHandlerHelper.cs index 2822b765..41eed53e 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaHandlerHelper.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaHandlerHelper.cs @@ -9,78 +9,77 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// Helper methods for creating thumbnails. +/// +public class MediaHandlerHelper { + #region Constants + /// - /// Helper methods for creating thumbnails. + /// Absolute sizes in pixels for the media files. /// - public class MediaHandlerHelper + private static Dictionary Sizes = new Dictionary { - #region Constants - - /// - /// Absolute sizes in pixels for the media files. - /// - private static Dictionary Sizes = new Dictionary - { - [MediaSize.Small] = new Size(200, 200), - [MediaSize.Medium] = new Size(640, 480), - [MediaSize.Large] = new Size(1280, 768), - }; + [MediaSize.Small] = new Size(200, 200), + [MediaSize.Medium] = new Size(640, 480), + [MediaSize.Large] = new Size(1280, 768), + }; - /// - /// Encoder for JPEG thumbnails. - /// - private static IImageEncoder JpegEncoder = new JpegEncoder { Quality = 90 }; + /// + /// Encoder for JPEG thumbnails. + /// + private static IImageEncoder JpegEncoder = new JpegEncoder { Quality = 90 }; - #endregion + #endregion - #region Public methods + #region Public methods - /// - /// Creates thumbnails. - /// - public static async Task CreateThumbnailsAsync(string path, Image frame, CancellationToken token = default) + /// + /// Creates thumbnails. + /// + public static async Task CreateThumbnailsAsync(string path, Image frame, CancellationToken token = default) + { + foreach (var size in Sizes) { - foreach (var size in Sizes) - { - var thumbPath = MediaPresenterService.GetSizedMediaPath(path, size.Key); - using var image = await Task.Run(() => ResizeToFit(frame, size.Value), token); - await image.SaveAsync(thumbPath, JpegEncoder, token); - } + var thumbPath = MediaPresenterService.GetSizedMediaPath(path, size.Key); + using var image = await Task.Run(() => ResizeToFit(frame, size.Value), token); + await image.SaveAsync(thumbPath, JpegEncoder, token); } + } - #endregion - - #region Private helpers + #endregion - /// - /// Generates the thumbnail version of the specified image. - /// - private static Image ResizeToFit(Image source, Size maxSize) - { - var propSize = GetProportionalSize(source.Size, maxSize); - return source.Clone(x => - { - x.Resize(propSize, KnownResamplers.Lanczos3, true); - }); - } + #region Private helpers - /// - /// Calculates the rectangle into which the image thumbnail will be inscribed. - /// - private static Size GetProportionalSize(Size size, Size maxSize) + /// + /// Generates the thumbnail version of the specified image. + /// + private static Image ResizeToFit(Image source, Size maxSize) + { + var propSize = GetProportionalSize(source.Size, maxSize); + return source.Clone(x => { - // do not upscale small images - if(size.Width < maxSize.Width && size.Height < maxSize.Height) - return size; + x.Resize(propSize, KnownResamplers.Lanczos3, true); + }); + } - var xRatio = (double)maxSize.Width / size.Width; - var yRatio = (double)maxSize.Height / size.Height; - var ratio = Math.Min(yRatio, xRatio); - return new Size((int)(size.Width * ratio), (int)(size.Height * ratio)); - } + /// + /// Calculates the rectangle into which the image thumbnail will be inscribed. + /// + private static Size GetProportionalSize(Size size, Size maxSize) + { + // do not upscale small images + if(size.Width < maxSize.Width && size.Height < maxSize.Height) + return size; - #endregion + var xRatio = (double)maxSize.Width / size.Width; + var yRatio = (double)maxSize.Height / size.Height; + var ratio = Math.Min(yRatio, xRatio); + return new Size((int)(size.Width * ratio), (int)(size.Height * ratio)); } + + #endregion } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaMetadata.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaMetadata.cs index 2924096c..eee6b162 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaMetadata.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/MediaMetadata.cs @@ -1,15 +1,14 @@ using System; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// Additional data about a photo. +/// +public class MediaMetadata { /// - /// Additional data about a photo. + /// Date of the photo or video's origin. /// - public class MediaMetadata - { - /// - /// Date of the photo or video's origin. - /// - public DateTime? Date { get; set; } - } -} + public DateTime? Date { get; init; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PdfMediaHandler.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PdfMediaHandler.cs index c13c6fa8..139a7361 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PdfMediaHandler.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PdfMediaHandler.cs @@ -4,39 +4,38 @@ using Microsoft.AspNetCore.Hosting; using SixLabors.ImageSharp; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// MediaHandler for creating thumbnails from PDF documents. +/// +public class PdfMediaHandler: IMediaHandler { - /// - /// MediaHandler for creating thumbnails from PDF documents. - /// - public class PdfMediaHandler: IMediaHandler + public PdfMediaHandler(IWebHostEnvironment env) { - public PdfMediaHandler(IWebHostEnvironment env) - { - _env = env; - } + _env = env; + } - private readonly IWebHostEnvironment _env; + private readonly IWebHostEnvironment _env; - public bool IsImmediate => true; - public string[] SupportedMimeTypes => new[] {"application/pdf"}; - public MediaType MediaType => MediaType.Document; + public bool IsImmediate => true; + public string[] SupportedMimeTypes => ["application/pdf"]; + public MediaType MediaType => MediaType.Document; - /// - /// Extracts the first page as a thumbnail. - /// - public async Task ExtractThumbnailAsync(string path, string mime) - { - var thumbPath = Path.Combine(_env.WebRootPath, "assets", "img", "pdf-thumb.png"); - return await Image.LoadAsync(thumbPath); - } + /// + /// Extracts the first page as a thumbnail. + /// + public async Task ExtractThumbnailAsync(string path, string mime) + { + var thumbPath = Path.Combine(_env.WebRootPath, "assets", "img", "pdf-thumb.png"); + return await Image.LoadAsync(thumbPath); + } - /// - /// No metadata extraction is supported. - /// - public Task ExtractMetadataAsync(string path, string mime) - { - return Task.FromResult(null); - } + /// + /// No metadata extraction is supported. + /// + public Task ExtractMetadataAsync(string path, string mime) + { + return Task.FromResult(null); } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PhotoMediaHandler.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PhotoMediaHandler.cs index 560a09f3..cd5f047c 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PhotoMediaHandler.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/PhotoMediaHandler.cs @@ -8,77 +8,76 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// MediaHandler for creating thumbnails for photos. +/// +public class PhotoMediaHandler: IMediaHandler { - /// - /// MediaHandler for creating thumbnails for photos. - /// - public class PhotoMediaHandler: IMediaHandler + public PhotoMediaHandler(ILogger logger) { - public PhotoMediaHandler(ILogger logger) - { - _logger = logger; - } + _logger = logger; + } - private readonly ILogger _logger; + private readonly ILogger _logger; - public bool IsImmediate => true; - public MediaType MediaType => MediaType.Photo; - public string[] SupportedMimeTypes => new [] - { - "image/jpeg", - "image/png" - }; + public bool IsImmediate => true; + public MediaType MediaType => MediaType.Photo; + public string[] SupportedMimeTypes => + [ + "image/jpeg", + "image/png" + ]; - /// - /// Returns the image for thumbnail creation. - /// - public async Task ExtractThumbnailAsync(string path, string mime) - { - return await Image.LoadAsync(path); - } + /// + /// Returns the image for thumbnail creation. + /// + public async Task ExtractThumbnailAsync(string path, string mime) + { + return await Image.LoadAsync(path); + } - /// - /// Extracts additional data from the media. - /// - public async Task ExtractMetadataAsync(string path, string mime) + /// + /// Extracts additional data from the media. + /// + public async Task ExtractMetadataAsync(string path, string mime) + { + try { - try - { - using var image = await Image.LoadAsync(path); - var exif = image.Metadata.ExifProfile; - var dateRaw = exif.Values?.FirstOrDefault(x => x.Tag == ExifTag.DateTimeOriginal)?.GetValue(); + using var image = await Image.LoadAsync(path); + var exif = image.Metadata.ExifProfile; + var dateRaw = exif.Values?.FirstOrDefault(x => x.Tag == ExifTag.DateTimeOriginal)?.GetValue(); - return new MediaMetadata - { - Date = ParseDate(dateRaw?.ToString()) - }; - } - catch (Exception ex) + return new MediaMetadata { - _logger.Error(ex, "Failed to get photo metadata!"); - return null; - } + Date = ParseDate(dateRaw?.ToString()) + }; } - - #region Private helpers - - /// - /// Parses the date from an EXIF raw value. - /// - private DateTime? ParseDate(string dateRaw) + catch (Exception ex) { - if (string.IsNullOrEmpty(dateRaw)) - return null; - - var dateFixed = Regex.Replace(dateRaw, @"^(?\d{4}):(?\d{2}):(?\d{2})", "${year}/${month}/${day}"); + _logger.Error(ex, "Failed to get photo metadata!"); + return null; + } + } - if (DateTime.TryParse(dateFixed, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) - return date; + #region Private helpers + /// + /// Parses the date from an EXIF raw value. + /// + private DateTime? ParseDate(string dateRaw) + { + if (string.IsNullOrEmpty(dateRaw)) return null; - } - #endregion + var dateFixed = Regex.Replace(dateRaw, @"^(?\d{4}):(?\d{2}):(?\d{2})", "${year}/${month}/${day}"); + + if (DateTime.TryParse(dateFixed, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) + return date; + + return null; } + + #endregion } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/VideoMediaHandler.cs b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/VideoMediaHandler.cs index 99fd7ee5..3196a4c1 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaHandlers/VideoMediaHandler.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaHandlers/VideoMediaHandler.cs @@ -4,47 +4,46 @@ using Microsoft.AspNetCore.Hosting; using SixLabors.ImageSharp; -namespace Bonsai.Areas.Admin.Logic.MediaHandlers +namespace Bonsai.Areas.Admin.Logic.MediaHandlers; + +/// +/// MediaHandler for creating thumbnails for photos. +/// +public class VideoMediaHandler: IMediaHandler { - /// - /// MediaHandler for creating thumbnails for photos. - /// - public class VideoMediaHandler: IMediaHandler + public VideoMediaHandler(IWebHostEnvironment env) { - public VideoMediaHandler(IWebHostEnvironment env) - { - _env = env; - } + _env = env; + } - private readonly IWebHostEnvironment _env; + private readonly IWebHostEnvironment _env; - public bool IsImmediate => false; - public MediaType MediaType => MediaType.Video; - public string[] SupportedMimeTypes => new[] - { - "video/mp4", - "video/x-flv", - "video/ogg", - "video/quicktime", - "video/x-msvideo", - "video/x-matroska" - }; + public bool IsImmediate => false; + public MediaType MediaType => MediaType.Video; + public string[] SupportedMimeTypes => + [ + "video/mp4", + "video/x-flv", + "video/ogg", + "video/quicktime", + "video/x-msvideo", + "video/x-matroska" + ]; - /// - /// Returns the image for thumbnail creation. - /// - public async Task ExtractThumbnailAsync(string path, string mime) - { - var thumbPath = Path.Combine(_env.WebRootPath, "assets", "img", "video-thumb.png"); - return await Image.LoadAsync(thumbPath); - } + /// + /// Returns the image for thumbnail creation. + /// + public async Task ExtractThumbnailAsync(string path, string mime) + { + var thumbPath = Path.Combine(_env.WebRootPath, "assets", "img", "video-thumb.png"); + return await Image.LoadAsync(thumbPath); + } - /// - /// Extracts additional data from the media. - /// - public Task ExtractMetadataAsync(string path, string mime) - { - return Task.FromResult(null); - } + /// + /// Extracts additional data from the media. + /// + public Task ExtractMetadataAsync(string path, string mime) + { + return Task.FromResult(null); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/MediaManagerService.cs b/src/Bonsai/Areas/Admin/Logic/MediaManagerService.cs index 07c4daa6..e2a00a75 100644 --- a/src/Bonsai/Areas/Admin/Logic/MediaManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/MediaManagerService.cs @@ -37,535 +37,534 @@ using Newtonsoft.Json; using MediaTagVM = Bonsai.Areas.Admin.ViewModels.Media.MediaTagVM; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// The manager service for handling media items. +/// +public class MediaManagerService { - /// - /// The manager service for handling media items. - /// - public class MediaManagerService + public MediaManagerService( + AppDbContext db, + UserManager userMgr, + IMapper mapper, + IWebHostEnvironment env, + IEnumerable mediaHandlers, + IBackgroundJobService jobs, + CacheService cache + ) { - public MediaManagerService( - AppDbContext db, - UserManager userMgr, - IMapper mapper, - IWebHostEnvironment env, - IEnumerable mediaHandlers, - IBackgroundJobService jobs, - CacheService cache - ) - { - _db = db; - _mapper = mapper; - _userMgr = userMgr; - _env = env; - _jobs = jobs; - _mediaHandlers = mediaHandlers.ToList(); - _cache = cache; - } - - private readonly AppDbContext _db; - private readonly IMapper _mapper; - private readonly UserManager _userMgr; - private readonly IWebHostEnvironment _env; - private readonly IBackgroundJobService _jobs; - private readonly IReadOnlyList _mediaHandlers; - private readonly CacheService _cache; + _db = db; + _mapper = mapper; + _userMgr = userMgr; + _env = env; + _jobs = jobs; + _mediaHandlers = mediaHandlers.ToList(); + _cache = cache; + } - #region Public methods + private readonly AppDbContext _db; + private readonly IMapper _mapper; + private readonly UserManager _userMgr; + private readonly IWebHostEnvironment _env; + private readonly IBackgroundJobService _jobs; + private readonly IReadOnlyList _mediaHandlers; + private readonly CacheService _cache; - /// - /// Finds media files. - /// - public async Task GetMediaAsync(MediaListRequestVM request) - { - const int PageSize = 20; + #region Public methods - request = NormalizeListRequest(request); + /// + /// Finds media files. + /// + public async Task GetMediaAsync(MediaListRequestVM request) + { + const int PageSize = 20; - var result = new MediaListVM { Request = request }; - await FillAdditionalDataAsync(request, result); + request = NormalizeListRequest(request); - var query = _db.Media - .Include(x => x.Tags) - .Where(x => x.IsDeleted == false); + var result = new MediaListVM { Request = request }; + await FillAdditionalDataAsync(request, result); - if(!string.IsNullOrEmpty(request.SearchQuery)) - query = query.Where(x => x.NormalizedTitle.Contains(PageHelper.NormalizeTitle(request.SearchQuery))); + var query = _db.Media + .Include(x => x.Tags) + .Where(x => x.IsDeleted == false); - if (request.EntityId != null) - query = query.Where(x => x.Tags.Any(y => y.ObjectId == request.EntityId)); + if(!string.IsNullOrEmpty(request.SearchQuery)) + query = query.Where(x => x.NormalizedTitle.Contains(PageHelper.NormalizeTitle(request.SearchQuery))); - if(request.Types?.Length > 0) - query = query.Where(x => request.Types.Contains(x.Type)); + if (request.EntityId != null) + query = query.Where(x => x.Tags.Any(y => y.ObjectId == request.EntityId)); - var totalCount = await query.CountAsync(); - result.PageCount = (int) Math.Ceiling((double) totalCount / PageSize); + if(request.Types?.Length > 0) + query = query.Where(x => request.Types.Contains(x.Type)); - var isDesc = request.OrderDescending ?? false; - if (request.OrderBy == nameof(Media.Tags)) - query = query.OrderBy(x => x.Tags.Count(y => y.Type == MediaTagType.DepictedEntity), isDesc); - else if (request.OrderBy == nameof(Media.Title)) - query = query.OrderBy(x => x.Title, isDesc).ThenBy(x => x.UploadDate, isDesc); - else - query = query.OrderBy(request.OrderBy, isDesc); + var totalCount = await query.CountAsync(); + result.PageCount = (int) Math.Ceiling((double) totalCount / PageSize); - result.Items = await query.ProjectToType(_mapper.Config) - .Skip(PageSize * request.Page) - .Take(PageSize) - .ToListAsync(); - return result; - } + var isDesc = request.OrderDescending ?? false; + if (request.OrderBy == nameof(Media.Tags)) + query = query.OrderBy(x => x.Tags.Count(y => y.Type == MediaTagType.DepictedEntity), isDesc); + else if (request.OrderBy == nameof(Media.Title)) + query = query.OrderBy(x => x.Title, isDesc).ThenBy(x => x.UploadDate, isDesc); + else + query = query.OrderBy(request.OrderBy, isDesc); - /// - /// Uploads a new media file. - /// - public async Task UploadAsync(MediaUploadRequestVM vm, IFormFile file, ClaimsPrincipal principal) - { - var id = Guid.NewGuid(); - var key = PageHelper.GetMediaKey(id); + result.Items = await query.ProjectToType(_mapper.Config) + .Skip(PageSize * request.Page) + .Take(PageSize) + .ToListAsync(); + return result; + } - var handler = _mediaHandlers.FirstOrDefault(x => x.SupportedMimeTypes.Contains(file.ContentType)); - if(handler == null) - throw new UploadException(Texts.Admin_Media_UnknownFileType); + /// + /// Uploads a new media file. + /// + public async Task UploadAsync(MediaUploadRequestVM vm, IFormFile file, ClaimsPrincipal principal) + { + var id = Guid.NewGuid(); + var key = PageHelper.GetMediaKey(id); - var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); - var paths = await SaveUploadAsync(file, key, handler); - var tags = await GetTagsForUploadedMedia(vm); - var meta = await handler.ExtractMetadataAsync(paths.LocalPath, file.ContentType); + var handler = _mediaHandlers.FirstOrDefault(x => x.SupportedMimeTypes.Contains(file.ContentType)); + if(handler == null) + throw new UploadException(Texts.Admin_Media_UnknownFileType); - var title = vm.UseFileNameAsTitle ? SanitizeFileName(file.FileName) : vm.Title; - var media = new Media - { - Id = id, - Key = key, - Type = handler.MediaType, - MimeType = file.ContentType, - Title = title, - NormalizedTitle = PageHelper.NormalizeTitle(title), - FilePath = paths.UrlPath, - UploadDate = DateTimeOffset.Now, - Uploader = user, - IsProcessed = handler.IsImmediate, - Date = FuzzyDate.TryParse(vm.Date) != null - ? vm.Date - : meta?.Date?.Date.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture), - Tags = tags - }; - - _db.Media.Add(media); - - var changeset = await GetChangesetAsync(null, _mapper.Map(media), id, principal, null); - _db.Changes.Add(changeset); - - return _mapper.Map(media); - } + var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); + var paths = await SaveUploadAsync(file, key, handler); + var tags = await GetTagsForUploadedMedia(vm); + var meta = await handler.ExtractMetadataAsync(paths.LocalPath, file.ContentType); - /// - /// Returns the media file editor. - /// - public async Task RequestUpdateAsync(Guid id, bool includeDeleted = false) + var title = vm.UseFileNameAsTitle ? SanitizeFileName(file.FileName) : vm.Title; + var media = new Media { - var media = await _db.Media - .AsNoTracking() - .Include(x => x.Tags) - .GetAsync(x => x.Id == id && (x.IsDeleted == false || includeDeleted), - Texts.Admin_Media_NotFound); - - var taggedIds = media.Tags - .Where(x => x.ObjectId != null) - .Select(x => x.ObjectId.Value) - .ToList(); - - var tagNames = await _db.Pages - .Where(x => taggedIds.Contains(x.Id) && x.IsDeleted == false) - .ToDictionaryAsync(x => x.Id, x => x.Title); - - var vm = _mapper.Map(media); - vm.Location = GetTagValue(MediaTagType.Location); - vm.Event = GetTagValue(MediaTagType.Event); - vm.DepictedEntities = JsonConvert.SerializeObject( - media.Tags.Where(x => x.Type == MediaTagType.DepictedEntity) - .Select(x => new MediaTagVM - { - Coordinates = x.Coordinates, - PageId = x.ObjectId, - ObjectTitle = tagNames.TryGetValue(x.ObjectId ?? Guid.Empty) - }) - ); - - return vm; - - string GetTagValue(MediaTagType type) - { - var tag = media.Tags.FirstOrDefault(x => x.Type == type); - return tag?.ObjectId?.ToString() ?? tag?.ObjectTitle; - } - } + Id = id, + Key = key, + Type = handler.MediaType, + MimeType = file.ContentType, + Title = title, + NormalizedTitle = PageHelper.NormalizeTitle(title), + FilePath = paths.UrlPath, + UploadDate = DateTimeOffset.Now, + Uploader = user, + IsProcessed = handler.IsImmediate, + Date = FuzzyDate.TryParse(vm.Date) != null + ? vm.Date + : meta?.Date?.Date.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture), + Tags = tags + }; + + _db.Media.Add(media); + + var changeset = await GetChangesetAsync(null, _mapper.Map(media), id, principal, null); + _db.Changes.Add(changeset); + + return _mapper.Map(media); + } - /// - /// Updates the media data. - /// - public async Task UpdateAsync(MediaEditorVM vm, ClaimsPrincipal principal, Guid? revertedId = null) + /// + /// Returns the media file editor. + /// + public async Task RequestUpdateAsync(Guid id, bool includeDeleted = false) + { + var media = await _db.Media + .AsNoTracking() + .Include(x => x.Tags) + .GetAsync(x => x.Id == id && (x.IsDeleted == false || includeDeleted), + Texts.Admin_Media_NotFound); + + var taggedIds = media.Tags + .Where(x => x.ObjectId != null) + .Select(x => x.ObjectId.Value) + .ToList(); + + var tagNames = await _db.Pages + .Where(x => taggedIds.Contains(x.Id) && x.IsDeleted == false) + .ToDictionaryAsync(x => x.Id, x => x.Title); + + var vm = _mapper.Map(media); + vm.Location = GetTagValue(MediaTagType.Location); + vm.Event = GetTagValue(MediaTagType.Event); + vm.DepictedEntities = JsonConvert.SerializeObject( + media.Tags.Where(x => x.Type == MediaTagType.DepictedEntity) + .Select(x => new MediaTagVM + { + Coordinates = x.Coordinates, + PageId = x.ObjectId, + ObjectTitle = tagNames.TryGetValue(x.ObjectId ?? Guid.Empty) + }) + ); + + return vm; + + string GetTagValue(MediaTagType type) { - await ValidateRequestAsync(vm); + var tag = media.Tags.FirstOrDefault(x => x.Type == type); + return tag?.ObjectId?.ToString() ?? tag?.ObjectTitle; + } + } - var media = await _db.Media - .Include(x => x.Tags) - .GetAsync(x => x.Id == vm.Id && (x.IsDeleted == false || revertedId != null), - Texts.Admin_Media_NotFound); + /// + /// Updates the media data. + /// + public async Task UpdateAsync(MediaEditorVM vm, ClaimsPrincipal principal, Guid? revertedId = null) + { + await ValidateRequestAsync(vm); - var prevTags = media.Tags.ToList(); - var prevVm = media.IsDeleted ? null : await RequestUpdateAsync(vm.Id, revertedId != null); - var changeset = await GetChangesetAsync(prevVm, vm, vm.Id, principal, revertedId); - _db.Changes.Add(changeset); + var media = await _db.Media + .Include(x => x.Tags) + .GetAsync(x => x.Id == vm.Id && (x.IsDeleted == false || revertedId != null), + Texts.Admin_Media_NotFound); - _mapper.Map(vm, media); - media.NormalizedTitle = PageHelper.NormalizeTitle(vm.Title); + var prevTags = media.Tags.ToList(); + var prevVm = media.IsDeleted ? null : await RequestUpdateAsync(vm.Id, revertedId != null); + var changeset = await GetChangesetAsync(prevVm, vm, vm.Id, principal, revertedId); + _db.Changes.Add(changeset); - if(revertedId != null) - media.IsDeleted = false; + _mapper.Map(vm, media); + media.NormalizedTitle = PageHelper.NormalizeTitle(vm.Title); - _db.MediaTags.RemoveRange(media.Tags); - foreach (var tag in DeserializeTags(vm)) - { - tag.Media = media; - _db.MediaTags.Add(tag); - } + if(revertedId != null) + media.IsDeleted = false; - await ClearCacheAsync(media, prevTags); + _db.MediaTags.RemoveRange(media.Tags); + foreach (var tag in DeserializeTags(vm)) + { + tag.Media = media; + _db.MediaTags.Add(tag); } - /// - /// Returns the confirmation info for the media. - /// - public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) - { - var media = await _db.Media - .AsNoTracking() - .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Media_NotFound); + await ClearCacheAsync(media, prevTags); + } - var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); - - return new RemoveEntryInfoVM - { - Entry = MediaPresenterService.GetSizedMediaPath(media.FilePath, MediaSize.Small), - CanRemoveCompletely = isAdmin - }; - } + /// + /// Returns the confirmation info for the media. + /// + public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) + { + var media = await _db.Media + .AsNoTracking() + .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Media_NotFound); - /// - /// Removes the media file. - /// - public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); + + return new RemoveEntryInfoVM { - var media = await _db.Media - .Include(x => x.Tags) - .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Media_NotFound); + Entry = MediaPresenterService.GetSizedMediaPath(media.FilePath, MediaSize.Small), + CanRemoveCompletely = isAdmin + }; + } + + /// + /// Removes the media file. + /// + public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + { + var media = await _db.Media + .Include(x => x.Tags) + .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Media_NotFound); - var prevState = await RequestUpdateAsync(id); - var changeset = await GetChangesetAsync(prevState, null, id, principal, null); - _db.Changes.Add(changeset); + var prevState = await RequestUpdateAsync(id); + var changeset = await GetChangesetAsync(prevState, null, id, principal, null); + _db.Changes.Add(changeset); - media.IsDeleted = true; + media.IsDeleted = true; - await ClearCacheAsync(media); - } + await ClearCacheAsync(media); + } - /// - /// Removes the media file irreversibly, including the media on disk. - /// - public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) - { - if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) - throw new OperationException(Texts.Admin_Users_Forbidden); + /// + /// Removes the media file irreversibly, including the media on disk. + /// + public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) + { + if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) + throw new OperationException(Texts.Admin_Users_Forbidden); - var media = await _db.Media - .Include(x => x.Tags) - .GetAsync(x => x.Id == id, Texts.Admin_Media_NotFound); + var media = await _db.Media + .Include(x => x.Tags) + .GetAsync(x => x.Id == id, Texts.Admin_Media_NotFound); - _db.MediaTags.RemoveRange(media.Tags); + _db.MediaTags.RemoveRange(media.Tags); - await _db.Changes.RemoveWhereAsync(x => x.EditedMediaId == id); + await _db.Changes.RemoveWhereAsync(x => x.EditedMediaId == id); - await foreach (var page in _db.Pages.WhereAsync(x => x.MainPhotoId == id)) - page.MainPhotoId = null; + await foreach (var page in _db.Pages.WhereAsync(x => x.MainPhotoId == id)) + page.MainPhotoId = null; - foreach(var size in new [] { MediaSize.Small, MediaSize.Original, MediaSize.Medium, MediaSize.Large }) - File.Delete(_env.GetMediaPath(media, size)); + foreach(var size in new [] { MediaSize.Small, MediaSize.Original, MediaSize.Medium, MediaSize.Large }) + File.Delete(_env.GetMediaPath(media, size)); - _db.Media.Remove(media); + _db.Media.Remove(media); - _cache.Clear(); - } + _cache.Clear(); + } - /// - /// Returns the thumbnails for the media files. - /// - public async Task> GetThumbnailsAsync(IEnumerable ids) - { - return await _db.Media - .Where(x => ids.Contains(x.Id)) - .ProjectToType(_mapper.Config) - .ToListAsync(); - } + /// + /// Returns the thumbnails for the media files. + /// + public async Task> GetThumbnailsAsync(IEnumerable ids) + { + return await _db.Media + .Where(x => ids.Contains(x.Id)) + .ProjectToType(_mapper.Config) + .ToListAsync(); + } - /// - /// Returns the ID of the chronologically first media without tags in the database. - /// - public async Task GetNextUntaggedMediaAsync() - { - return await _db.Media - .Where(x => !x.Tags.Any(y => y.Type == MediaTagType.DepictedEntity)) - .Where(x => x.IsProcessed && !x.IsDeleted) - .OrderByDescending(x => x.UploadDate) - .Select(x => (Guid?) x.Id) - .FirstOrDefaultAsync(); - } + /// + /// Returns the ID of the chronologically first media without tags in the database. + /// + public async Task GetNextUntaggedMediaAsync() + { + return await _db.Media + .Where(x => !x.Tags.Any(y => y.Type == MediaTagType.DepictedEntity)) + .Where(x => x.IsProcessed && !x.IsDeleted) + .OrderByDescending(x => x.UploadDate) + .Select(x => (Guid?) x.Id) + .FirstOrDefaultAsync(); + } - #endregion + #endregion - #region Helpers + #region Helpers - /// - /// Completes and\or corrects the search request. - /// - private MediaListRequestVM NormalizeListRequest(MediaListRequestVM vm) - { - if(vm == null) - vm = new MediaListRequestVM { OrderDescending = true }; + /// + /// Completes and\or corrects the search request. + /// + private MediaListRequestVM NormalizeListRequest(MediaListRequestVM vm) + { + if(vm == null) + vm = new MediaListRequestVM { OrderDescending = true }; - var orderableFields = new[] { nameof(Media.UploadDate), nameof(Media.Date), nameof(Media.Title), nameof(Media.Tags) }; - if(!orderableFields.Contains(vm.OrderBy)) - vm.OrderBy = orderableFields[0]; + var orderableFields = new[] { nameof(Media.UploadDate), nameof(Media.Date), nameof(Media.Title), nameof(Media.Tags) }; + if(!orderableFields.Contains(vm.OrderBy)) + vm.OrderBy = orderableFields[0]; - if(vm.Page < 0) - vm.Page = 0; + if(vm.Page < 0) + vm.Page = 0; - if (vm.OrderDescending == null) - vm.OrderDescending = true; + if (vm.OrderDescending == null) + vm.OrderDescending = true; - return vm; - } + return vm; + } - /// - /// Creates tag elements. - /// - private ICollection DeserializeTags(MediaEditorVM vm) - { - var tags = JsonConvert.DeserializeObject>(vm.DepictedEntities ?? "[]") - .Select(x => new MediaTag - { - Id = Guid.NewGuid(), - Type = MediaTagType.DepictedEntity, - Coordinates = x.Coordinates, - ObjectId = x.PageId, - ObjectTitle = x.PageId == null ? x.ObjectTitle : null - }) - .ToList(); + /// + /// Creates tag elements. + /// + private ICollection DeserializeTags(MediaEditorVM vm) + { + var tags = JsonConvert.DeserializeObject>(vm.DepictedEntities ?? "[]") + .Select(x => new MediaTag + { + Id = Guid.NewGuid(), + Type = MediaTagType.DepictedEntity, + Coordinates = x.Coordinates, + ObjectId = x.PageId, + ObjectTitle = x.PageId == null ? x.ObjectTitle : null + }) + .ToList(); - TryParseTag(vm.Location, MediaTagType.Location); - TryParseTag(vm.Event, MediaTagType.Event); + TryParseTag(vm.Location, MediaTagType.Location); + TryParseTag(vm.Event, MediaTagType.Event); - return tags; + return tags; + + void TryParseTag(string source, MediaTagType type) + { + if (string.IsNullOrEmpty(source)) + return; - void TryParseTag(string source, MediaTagType type) + var id = source.TryParse(); + tags.Add(new MediaTag { - if (string.IsNullOrEmpty(source)) - return; - - var id = source.TryParse(); - tags.Add(new MediaTag - { - Id = Guid.NewGuid(), - Type = type, - ObjectId = id, - ObjectTitle = id == null ? source : null - }); - } + Id = Guid.NewGuid(), + Type = type, + ObjectId = id, + ObjectTitle = id == null ? source : null + }); } + } - /// - /// Checks if the update request contains valid data. - /// - private async Task ValidateRequestAsync(MediaEditorVM vm) - { - var val = new Validator(); - - if (!string.IsNullOrEmpty(vm.Date) && FuzzyDate.TryParse(vm.Date) == null) - val.Add(nameof(vm.Date), Texts.Admin_Validation_IncorrectDate); + /// + /// Checks if the update request contains valid data. + /// + private async Task ValidateRequestAsync(MediaEditorVM vm) + { + var val = new Validator(); - var depictedIds = JsonConvert.DeserializeObject>(vm.DepictedEntities ?? "[]") - .Select(x => x.PageId) - .ToList(); + if (!string.IsNullOrEmpty(vm.Date) && FuzzyDate.TryParse(vm.Date) == null) + val.Add(nameof(vm.Date), Texts.Admin_Validation_IncorrectDate); - var locId = vm.Location.TryParse(); - var evtId = vm.Event.TryParse(); - var tagIds = depictedIds.Concat(new[] {locId, evtId}) - .Where(x => x != null) - .Select(x => x.Value) - .ToList(); + var depictedIds = JsonConvert.DeserializeObject>(vm.DepictedEntities ?? "[]") + .Select(x => x.PageId) + .ToList(); - if (tagIds.Any()) - { - var existing = await _db.Pages - .Where(x => tagIds.Contains(x.Id) && !x.IsDeleted) - .ToHashSetAsync(x => x.Id); + var locId = vm.Location.TryParse(); + var evtId = vm.Event.TryParse(); + var tagIds = depictedIds.Concat(new[] {locId, evtId}) + .Where(x => x != null) + .Select(x => x.Value) + .ToList(); - if (depictedIds.Any(x => x != null && !existing.Contains(x.Value))) - val.Add(nameof(vm.DepictedEntities), Texts.Admin_Pages_NotFound); + if (tagIds.Any()) + { + var existing = await _db.Pages + .Where(x => tagIds.Contains(x.Id) && !x.IsDeleted) + .ToHashSetAsync(x => x.Id); - if (locId != null && !existing.Contains(locId.Value)) - val.Add(nameof(vm.Location), Texts.Admin_Pages_NotFound); + if (depictedIds.Any(x => x != null && !existing.Contains(x.Value))) + val.Add(nameof(vm.DepictedEntities), Texts.Admin_Pages_NotFound); - if (evtId != null && !existing.Contains(evtId.Value)) - val.Add(nameof(vm.Event), Texts.Admin_Pages_NotFound); - } + if (locId != null && !existing.Contains(locId.Value)) + val.Add(nameof(vm.Location), Texts.Admin_Pages_NotFound); - val.ThrowIfInvalid(); + if (evtId != null && !existing.Contains(evtId.Value)) + val.Add(nameof(vm.Event), Texts.Admin_Pages_NotFound); } - /// - /// Saves an uploaded file to disk. - /// - private async Task<(string LocalPath, string UrlPath)> SaveUploadAsync(IFormFile file, string key, IMediaHandler handler) - { - var ext = Path.GetExtension(file.FileName); - var fileName = key + ext; - var filePath = Path.Combine(_env.WebRootPath, "media", fileName); + val.ThrowIfInvalid(); + } - await using (var localStream = new FileStream(filePath, FileMode.CreateNew)) - await using (var sourceStream = file.OpenReadStream()) - await sourceStream.CopyToAsync(localStream); + /// + /// Saves an uploaded file to disk. + /// + private async Task<(string LocalPath, string UrlPath)> SaveUploadAsync(IFormFile file, string key, IMediaHandler handler) + { + var ext = Path.GetExtension(file.FileName); + var fileName = key + ext; + var filePath = Path.Combine(_env.WebRootPath, "media", fileName); - using(var frame = await handler.ExtractThumbnailAsync(filePath, file.ContentType)) - await MediaHandlerHelper.CreateThumbnailsAsync(filePath, frame); + await using (var localStream = new FileStream(filePath, FileMode.CreateNew)) + await using (var sourceStream = file.OpenReadStream()) + await sourceStream.CopyToAsync(localStream); - return (filePath, $"~/media/{fileName}"); - } + using(var frame = await handler.ExtractThumbnailAsync(filePath, file.ContentType)) + await MediaHandlerHelper.CreateThumbnailsAsync(filePath, frame); - /// - /// Gets the changeset for updates. - /// - private async Task GetChangesetAsync(MediaEditorVM prev, MediaEditorVM next, Guid id, ClaimsPrincipal principal, Guid? revertedId) - { - if(prev == null && next == null) - throw new ArgumentNullException(); + return (filePath, $"~/media/{fileName}"); + } - var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); + /// + /// Gets the changeset for updates. + /// + private async Task GetChangesetAsync(MediaEditorVM prev, MediaEditorVM next, Guid id, ClaimsPrincipal principal, Guid? revertedId) + { + if(prev == null && next == null) + throw new ArgumentNullException(); - return new Changeset - { - Id = Guid.NewGuid(), - RevertedChangesetId = revertedId, - ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), - EntityType = ChangesetEntityType.Media, - Date = DateTime.Now, - EditedMediaId = id, - Author = user, - UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), - }; - } + var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); - /// - /// Clears the related pages from cache. - /// - private async Task ClearCacheAsync(Media media, IEnumerable prevTags = null) + return new Changeset { - _cache.Remove(media.Key); + Id = Guid.NewGuid(), + RevertedChangesetId = revertedId, + ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), + EntityType = ChangesetEntityType.Media, + Date = DateTime.Now, + EditedMediaId = id, + Author = user, + UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), + }; + } - var tags = prevTags == null ? media.Tags : media.Tags.Concat(prevTags); - var tagIds = tags.Select(x => x.ObjectId).ToList(); - var tagKeys = await _db.Pages - .Where(x => tagIds.Contains(x.Id)) - .Select(x => x.Key) - .ToListAsync(); + /// + /// Clears the related pages from cache. + /// + private async Task ClearCacheAsync(Media media, IEnumerable prevTags = null) + { + _cache.Remove(media.Key); - foreach (var key in tagKeys) - _cache.Remove(key); + var tags = prevTags == null ? media.Tags : media.Tags.Concat(prevTags); + var tagIds = tags.Select(x => x.ObjectId).ToList(); + var tagKeys = await _db.Pages + .Where(x => tagIds.Contains(x.Id)) + .Select(x => x.Key) + .ToListAsync(); - var pagesWithMain = await _db.Pages - .Where(x => x.MainPhotoId == media.Id) - .Select(x => x.Key) - .ToListAsync(); + foreach (var key in tagKeys) + _cache.Remove(key); - foreach (var key in pagesWithMain) - { - _cache.Remove(key); - _cache.Remove(key); - _cache.Remove(key); - } + var pagesWithMain = await _db.Pages + .Where(x => x.MainPhotoId == media.Id) + .Select(x => x.Key) + .ToListAsync(); + + foreach (var key in pagesWithMain) + { + _cache.Remove(key); + _cache.Remove(key); + _cache.Remove(key); } + } - /// - /// Loads extra data for the filter. - /// - private async Task FillAdditionalDataAsync(MediaListRequestVM request, MediaListVM data) + /// + /// Loads extra data for the filter. + /// + private async Task FillAdditionalDataAsync(MediaListRequestVM request, MediaListVM data) + { + if (request.EntityId != null) { - if (request.EntityId != null) - { - var title = await _db.Pages - .Where(x => x.Id == request.EntityId) - .Select(x => x.Title) - .FirstOrDefaultAsync(); - - if (title != null) - data.EntityTitle = title; - else - request.EntityId = null; - } - } + var title = await _db.Pages + .Where(x => x.Id == request.EntityId) + .Select(x => x.Title) + .FirstOrDefaultAsync(); - /// - /// Creates event \ location tags for the uploaded media. - /// - private async Task> GetTagsForUploadedMedia(MediaUploadRequestVM vm) - { - var result = new List(); + if (title != null) + data.EntityTitle = title; + else + request.EntityId = null; + } + } - var locId = vm.Location.TryParse(); - var evtId = vm.Event.TryParse(); + /// + /// Creates event \ location tags for the uploaded media. + /// + private async Task> GetTagsForUploadedMedia(MediaUploadRequestVM vm) + { + var result = new List(); - var tagIds = new[] {locId, evtId} - .Where(x => x != Guid.Empty) - .ToList(); + var locId = vm.Location.TryParse(); + var evtId = vm.Event.TryParse(); - var existing = tagIds.Any() - ? await _db.Pages - .Where(x => tagIds.Contains(x.Id) && !x.IsDeleted) - .ToHashSetAsync(x => x.Id) - : null; + var tagIds = new[] {locId, evtId} + .Where(x => x != Guid.Empty) + .ToList(); - TryAddTag(vm.Location, locId, MediaTagType.Location); - TryAddTag(vm.Event, evtId, MediaTagType.Event); + var existing = tagIds.Any() + ? await _db.Pages + .Where(x => tagIds.Contains(x.Id) && !x.IsDeleted) + .ToHashSetAsync(x => x.Id) + : null; - return result; + TryAddTag(vm.Location, locId, MediaTagType.Location); + TryAddTag(vm.Event, evtId, MediaTagType.Event); - void TryAddTag(string title, Guid id, MediaTagType type) - { - if (string.IsNullOrEmpty(title)) - return; + return result; - var tag = new MediaTag { Type = type }; + void TryAddTag(string title, Guid id, MediaTagType type) + { + if (string.IsNullOrEmpty(title)) + return; - if (existing?.Contains(id) == true) - tag.ObjectId = id; - else - tag.ObjectTitle = title; + var tag = new MediaTag { Type = type }; - result.Add(tag); - } - } + if (existing?.Contains(id) == true) + tag.ObjectId = id; + else + tag.ObjectTitle = title; - /// - /// Returns the "cleaned-up" file name. - /// - private string SanitizeFileName(string fileName) - { - var extless = Path.GetFileNameWithoutExtension(fileName); - return extless.ReplaceRegex("[\\W_]", " ") - .ReplaceRegex("\\s{2,}", " "); + result.Add(tag); } + } - #endregion + /// + /// Returns the "cleaned-up" file name. + /// + private string SanitizeFileName(string fileName) + { + var extless = Path.GetFileNameWithoutExtension(fileName); + return extless.ReplaceRegex("[\\W_]", " ") + .ReplaceRegex("\\s{2,}", " "); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/NotificationsService.cs b/src/Bonsai/Areas/Admin/Logic/NotificationsService.cs index c2619577..5f57701d 100644 --- a/src/Bonsai/Areas/Admin/Logic/NotificationsService.cs +++ b/src/Bonsai/Areas/Admin/Logic/NotificationsService.cs @@ -1,64 +1,63 @@ using Microsoft.AspNetCore.Http; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// A service to keep track of user-dismissable notifications. +/// +public class NotificationsService { - /// - /// A service to keep track of user-dismissable notifications. - /// - public class NotificationsService + public NotificationsService(IHttpContextAccessor httpAccessor) { - public NotificationsService(IHttpContextAccessor httpAccessor) - { - _httpAccessor = httpAccessor; - } + _httpAccessor = httpAccessor; + } - private readonly IHttpContextAccessor _httpAccessor; + private readonly IHttpContextAccessor _httpAccessor; - private HttpRequest Request => _httpAccessor.HttpContext.Request; - private HttpResponse Response => _httpAccessor.HttpContext.Response; + private HttpRequest Request => _httpAccessor.HttpContext.Request; + private HttpResponse Response => _httpAccessor.HttpContext.Response; - /// - /// Prefix for all notification cookies. - /// - private static readonly string PREFIX = "Notifications."; + /// + /// Prefix for all notification cookies. + /// + private static readonly string PREFIX = "Notifications."; - /// - /// Unique ID for the notification urging the user to see guidelines before editing a page. - /// - public static readonly string NOTE_USER_GUIDELINES = "UserGuidelines"; + /// + /// Unique ID for the notification urging the user to see guidelines before editing a page. + /// + public static readonly string NOTE_USER_GUIDELINES = "UserGuidelines"; - /// - /// Unique ID for the notification about password auth profiles. - /// - public static readonly string NOTE_PASSWORD_AUTH = "PasswordAuth"; + /// + /// Unique ID for the notification about password auth profiles. + /// + public static readonly string NOTE_PASSWORD_AUTH = "PasswordAuth"; - /// - /// Notification ID: user attempted to create a new page, but a draft of a different type was already present. - /// - public static readonly string NOTE_PAGETYPE_RESET_FROM_DRAFT = "PageTypeResetFromDraft"; + /// + /// Notification ID: user attempted to create a new page, but a draft of a different type was already present. + /// + public static readonly string NOTE_PAGETYPE_RESET_FROM_DRAFT = "PageTypeResetFromDraft"; - /// - /// Checks if the notification must be shown. - /// - public bool IsShown(string id) - { - return !Request.Cookies.ContainsKey(PREFIX + id); - } + /// + /// Checks if the notification must be shown. + /// + public bool IsShown(string id) + { + return !Request.Cookies.ContainsKey(PREFIX + id); + } - /// - /// Dismisses the notification by setting a cookie. - /// - public void Hide(string id) - { - Response.Cookies.Append(PREFIX + id, "true", new CookieOptions { IsEssential = true }); - } + /// + /// Dismisses the notification by setting a cookie. + /// + public void Hide(string id) + { + Response.Cookies.Append(PREFIX + id, "true", new CookieOptions { IsEssential = true }); + } - /// - /// Clears the dismissed state. - /// - public void Show(string id) - { - Response.Cookies.Delete(PREFIX + id, new CookieOptions { IsEssential = true }); - } + /// + /// Clears the dismissed state. + /// + public void Show(string id) + { + Response.Cookies.Delete(PREFIX + id, new CookieOptions { IsEssential = true }); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/PagesManagerService.cs b/src/Bonsai/Areas/Admin/Logic/PagesManagerService.cs index 2598106e..a43dea16 100644 --- a/src/Bonsai/Areas/Admin/Logic/PagesManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/PagesManagerService.cs @@ -26,647 +26,645 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// The manager service for handling pages. +/// +public class PagesManagerService { - /// - /// The manager service for handling pages. - /// - public class PagesManagerService + public PagesManagerService(AppDbContext db, IMapper mapper, UserManager userMgr, PageValidator validator, CacheService cache) { - public PagesManagerService(AppDbContext db, IMapper mapper, UserManager userMgr, PageValidator validator, CacheService cache) - { - _db = db; - _mapper = mapper; - _userMgr = userMgr; - _validator = validator; - _cache = cache; - } - - private readonly AppDbContext _db; - private readonly IMapper _mapper; - private readonly UserManager _userMgr; - private readonly PageValidator _validator; - private readonly CacheService _cache; + _db = db; + _mapper = mapper; + _userMgr = userMgr; + _validator = validator; + _cache = cache; + } - #region Public methods + private readonly AppDbContext _db; + private readonly IMapper _mapper; + private readonly UserManager _userMgr; + private readonly PageValidator _validator; + private readonly CacheService _cache; - /// - /// Finds pages. - /// - public async Task GetPagesAsync(PagesListRequestVM request) - { - const int PageSize = 20; + #region Public methods - request = NormalizeListRequest(request); + /// + /// Finds pages. + /// + public async Task GetPagesAsync(PagesListRequestVM request) + { + const int PageSize = 20; - var query = _db.PagesScored - .Include(x => x.MainPhoto) - .Where(x => x.IsDeleted == false); + request = NormalizeListRequest(request); - if (request.Types?.Length > 0) - query = query.Where(x => request.Types.Contains(x.Type)); + var query = _db.PagesScored + .Include(x => x.MainPhoto) + .Where(x => x.IsDeleted == false); - if (!string.IsNullOrEmpty(request.SearchQuery)) - query = query.Where(x => x.NormalizedTitle.Contains(PageHelper.NormalizeTitle(request.SearchQuery))); + if (request.Types?.Length > 0) + query = query.Where(x => request.Types.Contains(x.Type)); - var totalCount = await query.CountAsync(); + if (!string.IsNullOrEmpty(request.SearchQuery)) + query = query.Where(x => x.NormalizedTitle.Contains(PageHelper.NormalizeTitle(request.SearchQuery))); - var items = await query.OrderBy(request.OrderBy, request.OrderDescending ?? false) - .ProjectToType(_mapper.Config) - .Skip(PageSize * request.Page) - .Take(PageSize) - .ToListAsync(); + var totalCount = await query.CountAsync(); - return new PagesListVM - { - Items = items, - PageCount = (int) Math.Ceiling((double) totalCount / PageSize), - Request = request - }; - } + var items = await query.OrderBy(request.OrderBy, request.OrderDescending ?? false) + .ProjectToType(_mapper.Config) + .Skip(PageSize * request.Page) + .Take(PageSize) + .ToListAsync(); - /// - /// Returns the lookup of page titles. - /// - public async Task> FindPagesByIdsAsync(IReadOnlyList pages) + return new PagesListVM { - return await _db.Pages - .Include(x => x.MainPhoto) - .Where(x => pages.Any(y => x.Id == y) && x.IsDeleted == false) - .ProjectToType(_mapper.Config) - .ToDictionaryAsync(x => x.Id, x => x); - } + Items = items, + PageCount = (int) Math.Ceiling((double) totalCount / PageSize), + Request = request + }; + } - /// - /// Returns the editor's new state (blank or restored from a draft). - /// - public async Task RequestCreateAsync(PageType type, ClaimsPrincipal principal) - { - var draft = await GetPageDraftAsync(null, principal); - if (draft != null) - return JsonConvert.DeserializeObject(draft.Content); + /// + /// Returns the lookup of page titles. + /// + public async Task> FindPagesByIdsAsync(IReadOnlyList pages) + { + return await _db.Pages + .Include(x => x.MainPhoto) + .Where(x => pages.Any(y => x.Id == y) && x.IsDeleted == false) + .ProjectToType(_mapper.Config) + .ToDictionaryAsync(x => x.Id, x => x); + } - return new PageEditorVM {Type = type}; - } + /// + /// Returns the editor's new state (blank or restored from a draft). + /// + public async Task RequestCreateAsync(PageType type, ClaimsPrincipal principal) + { + var draft = await GetPageDraftAsync(null, principal); + if (draft != null) + return JsonConvert.DeserializeObject(draft.Content); + + return new PageEditorVM {Type = type}; + } - /// - /// Creates the new page. - /// - public async Task CreateAsync(PageEditorVM vm, ClaimsPrincipal principal) - { - await ValidateRequestAsync(vm); - - var key = PageHelper.EncodeTitle(vm.Title); - var existingRemoved = await _db.Pages - .Where(x => x.Key == key && x.IsDeleted == true) - .Select(x => new { x.Id }) - .FirstOrDefaultAsync(); - if (existingRemoved != null) - return await UpdateAsync(vm, principal, pageId: existingRemoved.Id); - - var page = _mapper.Map(vm); - page.Id = Guid.NewGuid(); - page.CreationDate = DateTimeOffset.Now; - page.MainPhoto = await FindMainPhotoAsync(vm.MainPhotoKey); - page.LivingBeingOverview = MapLivingBeingOverview(vm); - page.NormalizedTitle = PageHelper.NormalizeTitle(page.Title); - - await _validator.ValidateAsync(page, vm.Facts); - - var changeset = await GetChangesetAsync(null, _mapper.Map(page), page.Id, principal, null); - _db.Changes.Add(changeset); + /// + /// Creates the new page. + /// + public async Task CreateAsync(PageEditorVM vm, ClaimsPrincipal principal) + { + await ValidateRequestAsync(vm); + + var key = PageHelper.EncodeTitle(vm.Title); + var existingRemoved = await _db.Pages + .Where(x => x.Key == key && x.IsDeleted == true) + .Select(x => new { x.Id }) + .FirstOrDefaultAsync(); + if (existingRemoved != null) + return await UpdateAsync(vm, principal, pageId: existingRemoved.Id); + + var page = _mapper.Map(vm); + page.Id = Guid.NewGuid(); + page.CreationDate = DateTimeOffset.Now; + page.MainPhoto = await FindMainPhotoAsync(vm.MainPhotoKey); + page.LivingBeingOverview = MapLivingBeingOverview(vm); + page.NormalizedTitle = PageHelper.NormalizeTitle(page.Title); + + await _validator.ValidateAsync(page, vm.Facts); + + var changeset = await GetChangesetAsync(null, _mapper.Map(page), page.Id, principal, null); + _db.Changes.Add(changeset); - _db.Pages.Add(page); - _db.PageAliases.AddRange(GetPageAliases(vm.Aliases, vm.Title, page)); - _db.PageReferences.AddRange(await GetPageReferencesAsync(vm.Description, page)); + _db.Pages.Add(page); + _db.PageAliases.AddRange(GetPageAliases(vm.Aliases, vm.Title, page)); + _db.PageReferences.AddRange(await GetPageReferencesAsync(vm.Description, page)); - await DiscardPageDraftAsync(null, principal); + await DiscardPageDraftAsync(null, principal); - return page; - } + return page; + } - /// - /// Creates a default page for the user. - /// - public async Task CreateDefaultUserPageAsync(RegisterUserVM vm, ClaimsPrincipal principal) - { - var name = new[] {vm.LastName, vm.FirstName, vm.MiddleName} - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Trim()) - .JoinString(" "); + /// + /// Creates a default page for the user. + /// + public async Task CreateDefaultUserPageAsync(RegisterUserVM vm, ClaimsPrincipal principal) + { + var name = new[] {vm.LastName, vm.FirstName, vm.MiddleName} + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .JoinString(" "); - var createVm = new PageEditorVM + var createVm = new PageEditorVM + { + Title = name, + Description = name, + Type = PageType.Person, + Facts = new JObject { - Title = name, - Description = name, - Type = PageType.Person, - Facts = new JObject + ["Birth.Date"] = new JObject { ["Value"] = vm.Birthday }, + ["Main.Name"] = new JObject { - ["Birth.Date"] = new JObject { ["Value"] = vm.Birthday }, - ["Main.Name"] = new JObject + ["Values"] = new JArray { - ["Values"] = new JArray + new JObject { - new JObject - { - ["FirstName"] = vm.FirstName, - ["MiddleName"] = vm.MiddleName, - ["LastName"] = vm.LastName - } + ["FirstName"] = vm.FirstName, + ["MiddleName"] = vm.MiddleName, + ["LastName"] = vm.LastName } } + } - }.ToString() - }; + }.ToString() + }; - return await CreateAsync(createVm, principal); - } + return await CreateAsync(createVm, principal); + } - /// - /// Returns the original data for the editor form. - /// - public async Task RequestUpdateAsync(Guid id, ClaimsPrincipal principal, bool force = false) + /// + /// Returns the original data for the editor form. + /// + public async Task RequestUpdateAsync(Guid id, ClaimsPrincipal principal, bool force = false) + { + if (!force) { - if (!force) - { - var draft = await GetPageDraftAsync(id, principal); - if (draft != null) - return JsonConvert.DeserializeObject(draft.Content); - } - - var page = await _db.Pages - .AsNoTracking() - .Include(x => x.MainPhoto) - .Include(x => x.Aliases) - .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); - - return _mapper.Map(page); + var draft = await GetPageDraftAsync(id, principal); + if (draft != null) + return JsonConvert.DeserializeObject(draft.Content); } - /// - /// Updates the changes to a page. - /// - public async Task UpdateAsync(PageEditorVM vm, ClaimsPrincipal principal, Guid? revertedChangeId = null, Guid? pageId = null) - { - await ValidateRequestAsync(vm); + var page = await _db.Pages + .AsNoTracking() + .Include(x => x.MainPhoto) + .Include(x => x.Aliases) + .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); - if (pageId == null) - pageId = vm.Id; + return _mapper.Map(page); + } - var page = await _db.Pages - .Include(x => x.Aliases) - .Include(x => x.MainPhoto) - .Include(x => x.LivingBeingOverview) - .GetAsync(x => x.Id == pageId, Texts.Admin_Pages_NotFound); + /// + /// Updates the changes to a page. + /// + public async Task UpdateAsync(PageEditorVM vm, ClaimsPrincipal principal, Guid? revertedChangeId = null, Guid? pageId = null) + { + await ValidateRequestAsync(vm); - await _validator.ValidateAsync(page, vm.Facts); + if (pageId == null) + pageId = vm.Id; - var prevVm = page.IsDeleted ? null : _mapper.Map(page); - var changeset = await GetChangesetAsync(prevVm, vm, pageId.Value, principal, revertedChangeId); - _db.Changes.Add(changeset); + var page = await _db.Pages + .Include(x => x.Aliases) + .Include(x => x.MainPhoto) + .Include(x => x.LivingBeingOverview) + .GetAsync(x => x.Id == pageId, Texts.Admin_Pages_NotFound); - _mapper.Map(vm, page); - page.MainPhotoId = (await FindMainPhotoAsync(vm.MainPhotoKey))?.Id; - page.LivingBeingOverview = MapLivingBeingOverview(vm, page.LivingBeingOverview); - page.NormalizedTitle = PageHelper.NormalizeTitle(page.Title); + await _validator.ValidateAsync(page, vm.Facts); - page.IsDeleted = false; + var prevVm = page.IsDeleted ? null : _mapper.Map(page); + var changeset = await GetChangesetAsync(prevVm, vm, pageId.Value, principal, revertedChangeId); + _db.Changes.Add(changeset); - await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == pageId); - _db.PageAliases.AddRange(GetPageAliases(vm.Aliases, vm.Title, page)); + _mapper.Map(vm, page); + page.MainPhotoId = (await FindMainPhotoAsync(vm.MainPhotoKey))?.Id; + page.LivingBeingOverview = MapLivingBeingOverview(vm, page.LivingBeingOverview); + page.NormalizedTitle = PageHelper.NormalizeTitle(page.Title); - await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == pageId); - _db.PageReferences.AddRange(await GetPageReferencesAsync(vm.Description, page)); + page.IsDeleted = false; - if (prevVm?.Title != vm.Title || prevVm?.Facts != vm.Facts) - { - _cache.Clear(); - } - else - { - _cache.Remove(page.Key); - _cache.Remove(page.Key); - } + await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == pageId); + _db.PageAliases.AddRange(GetPageAliases(vm.Aliases, vm.Title, page)); - if(revertedChangeId == null) - await DiscardPageDraftAsync(vm.Id, principal); + await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == pageId); + _db.PageReferences.AddRange(await GetPageReferencesAsync(vm.Description, page)); - return page; + if (prevVm?.Title != vm.Title || prevVm?.Facts != vm.Facts) + { + _cache.Clear(); } - - /// - /// Displays the remove confirmation form. - /// - public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) + else { - // todo: figure out why ProjectToType<> does not work in this particular case - // https://github.com/impworks/bonsai/issues/252 - var page = await _db.Pages - .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); + _cache.Remove(page.Key); + _cache.Remove(page.Key); + } - var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); + if(revertedChangeId == null) + await DiscardPageDraftAsync(vm.Id, principal); - return new RemoveEntryInfoVM - { - Entry = _mapper.Map(page), - CanRemoveCompletely = isAdmin - }; - } + return page; + } + + /// + /// Displays the remove confirmation form. + /// + public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) + { + // todo: figure out why ProjectToType<> does not work in this particular case + // https://github.com/impworks/bonsai/issues/252 + var page = await _db.Pages + .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); + + var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); - /// - /// Removes the page. - /// - public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + return new RemoveEntryInfoVM { - var page = await _db.Pages - .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); + Entry = _mapper.Map(page), + CanRemoveCompletely = isAdmin + }; + } - var prev = await RequestUpdateAsync(id, principal, force: true); - var changeset = await GetChangesetAsync(prev, null, id, principal, null); - _db.Changes.Add(changeset); + /// + /// Removes the page. + /// + public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + { + var page = await _db.Pages + .GetAsync(x => x.Id == id && x.IsDeleted == false, Texts.Admin_Pages_NotFound); - page.IsDeleted = true; + var prev = await RequestUpdateAsync(id, principal, force: true); + var changeset = await GetChangesetAsync(prev, null, id, principal, null); + _db.Changes.Add(changeset); - await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == id); - await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); + page.IsDeleted = true; - _cache.Clear(); + await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == id); + await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); - return page; - } + _cache.Clear(); - /// - /// Removes the page completely with all related entities (tags, relations, changesets). - /// - public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) - { - if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) - throw new OperationException(Texts.Admin_Users_Forbidden); + return page; + } + + /// + /// Removes the page completely with all related entities (tags, relations, changesets). + /// + public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) + { + if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) + throw new OperationException(Texts.Admin_Users_Forbidden); - var page = await _db.Pages - .GetAsync(x => x.Id == id, Texts.Admin_Pages_NotFound); + var page = await _db.Pages + .GetAsync(x => x.Id == id, Texts.Admin_Pages_NotFound); - // changesets - await _db.Changes.RemoveWhereAsync(x => x.EditedPageId == id); - await _db.Changes.RemoveWhereAsync(x => x.EditedRelation.SourceId == id || x.EditedRelation.DestinationId == id); + // changesets + await _db.Changes.RemoveWhereAsync(x => x.EditedPageId == id); + await _db.Changes.RemoveWhereAsync(x => x.EditedRelation.SourceId == id || x.EditedRelation.DestinationId == id); - // relations - await _db.Relations.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); - await foreach (var rel in _db.Relations.WhereAsync(x => x.EventId == id)) - rel.EventId = null; + // relations + await _db.Relations.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); + await foreach (var rel in _db.Relations.WhereAsync(x => x.EventId == id)) + rel.EventId = null; - // media tags - await _db.MediaTags.RemoveWhereAsync(x => x.ObjectId == id); - await CleanupMediaChangesetsAsync(); - - // page-related stuff - await _db.PageDrafts.RemoveWhereAsync(x => x.PageId == id); - await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == id); - await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); + // media tags + await _db.MediaTags.RemoveWhereAsync(x => x.ObjectId == id); + await CleanupMediaChangesetsAsync(); + + // page-related stuff + await _db.PageDrafts.RemoveWhereAsync(x => x.PageId == id); + await _db.PageAliases.RemoveWhereAsync(x => x.Page.Id == id); + await _db.PageReferences.RemoveWhereAsync(x => x.SourceId == id || x.DestinationId == id); - // users - await foreach (var user in _db.Users.WhereAsync(x => x.PageId == id)) - user.PageId = null; + // users + await foreach (var user in _db.Users.WhereAsync(x => x.PageId == id)) + user.PageId = null; - // page itself - _db.Pages.Remove(page); - - _cache.Clear(); - - async Task CleanupMediaChangesetsAsync() - { - var changesets = await _db.Changes.Where(x => x.EditedMediaId != null) - .ToListAsync(); + // page itself + _db.Pages.Remove(page); - foreach (var changeset in changesets) - { - var updState = RemoveMediaTagReferences(changeset.UpdatedState); - if (updState != null) - changeset.UpdatedState = updState; - } - } + _cache.Clear(); - string RemoveMediaTagReferences(string raw) - { - var editor = TryParse(raw); - if (editor == null) - return null; - - var tags = TryParse(editor.DepictedEntities); - if (tags == null) - return null; - - var remainingTags = tags.Where(x => x.PageId != id).ToList(); - if (remainingTags.Count == tags.Length) - return null; - - editor.DepictedEntities = JsonConvert.SerializeObject(remainingTags); - return JsonConvert.SerializeObject(editor); - } + async Task CleanupMediaChangesetsAsync() + { + var changesets = await _db.Changes.Where(x => x.EditedMediaId != null) + .ToListAsync(); - T TryParse(string raw) where T: class + foreach (var changeset in changesets) { - if (string.IsNullOrWhiteSpace(raw)) - return null; - - try - { - return JsonConvert.DeserializeObject(raw); - } - catch - { - return null; - } + var updState = RemoveMediaTagReferences(changeset.UpdatedState); + if (updState != null) + changeset.UpdatedState = updState; } } - /// - /// Returns the editor state from the current draft. - /// - public async Task GetPageDraftAsync(Guid? page, ClaimsPrincipal principal) + string RemoveMediaTagReferences(string raw) { - var userId = _userMgr.GetUserId(principal); - var pageId = page == Guid.Empty ? null : page; - return await _db.PageDrafts.FirstOrDefaultAsync(x => x.PageId == pageId && x.UserId == userId); + var editor = TryParse(raw); + if (editor == null) + return null; + + var tags = TryParse(editor.DepictedEntities); + if (tags == null) + return null; + + var remainingTags = tags.Where(x => x.PageId != id).ToList(); + if (remainingTags.Count == tags.Length) + return null; + + editor.DepictedEntities = JsonConvert.SerializeObject(remainingTags); + return JsonConvert.SerializeObject(editor); } - /// - /// Updates the existing draft state. - /// - public async Task UpdatePageDraftAsync(PageEditorVM vm, ClaimsPrincipal principal) + T TryParse(string raw) where T: class { - var userId = _userMgr.GetUserId(principal); - var pageId = vm.Id == Guid.Empty ? null : (Guid?) vm.Id; - - var draft = await _db.PageDrafts - .FirstOrDefaultAsync(x => x.PageId == pageId && x.UserId == userId); + if (string.IsNullOrWhiteSpace(raw)) + return null; - if (draft == null) + try { - draft = new PageDraft - { - Id = Guid.NewGuid(), - PageId = pageId, - UserId = userId - }; - _db.PageDrafts.Add(draft); + return JsonConvert.DeserializeObject(raw); } + catch + { + return null; + } + } + } - draft.Content = JsonConvert.SerializeObject(vm); - draft.LastUpdateDate = DateTimeOffset.Now; + /// + /// Returns the editor state from the current draft. + /// + public async Task GetPageDraftAsync(Guid? page, ClaimsPrincipal principal) + { + var userId = _userMgr.GetUserId(principal); + var pageId = page == Guid.Empty ? null : page; + return await _db.PageDrafts.FirstOrDefaultAsync(x => x.PageId == pageId && x.UserId == userId); + } - return new PageDraftInfoVM + /// + /// Updates the existing draft state. + /// + public async Task UpdatePageDraftAsync(PageEditorVM vm, ClaimsPrincipal principal) + { + var userId = _userMgr.GetUserId(principal); + var pageId = vm.Id == Guid.Empty ? null : (Guid?) vm.Id; + + var draft = await _db.PageDrafts + .FirstOrDefaultAsync(x => x.PageId == pageId && x.UserId == userId); + + if (draft == null) + { + draft = new PageDraft { - LastUpdateDate = draft.LastUpdateDate + Id = Guid.NewGuid(), + PageId = pageId, + UserId = userId }; + _db.PageDrafts.Add(draft); } - /// - /// Discards the existing draft for a page. - /// - public async Task DiscardPageDraftAsync(Guid? pageId, ClaimsPrincipal principal) + draft.Content = JsonConvert.SerializeObject(vm); + draft.LastUpdateDate = DateTimeOffset.Now; + + return new PageDraftInfoVM { - var draft = await GetPageDraftAsync(pageId, principal); + LastUpdateDate = draft.LastUpdateDate + }; + } - if (draft != null) - _db.PageDrafts.Remove(draft); - } + /// + /// Discards the existing draft for a page. + /// + public async Task DiscardPageDraftAsync(Guid? pageId, ClaimsPrincipal principal) + { + var draft = await GetPageDraftAsync(pageId, principal); - /// - /// Returns the preview-friendly version of the page. - /// - public async Task GetPageDraftPreviewAsync(Guid? pageId, ClaimsPrincipal principal) - { - var draft = await GetPageDraftAsync(pageId, principal); - if (draft == null) - return null; + if (draft != null) + _db.PageDrafts.Remove(draft); + } - var editor = JsonConvert.DeserializeObject(draft.Content); - var page = _mapper.Map(editor); - page.MainPhoto = await FindMainPhotoAsync(editor.MainPhotoKey); + /// + /// Returns the preview-friendly version of the page. + /// + public async Task GetPageDraftPreviewAsync(Guid? pageId, ClaimsPrincipal principal) + { + var draft = await GetPageDraftAsync(pageId, principal); + if (draft == null) + return null; - return page; - } + var editor = JsonConvert.DeserializeObject(draft.Content); + var page = _mapper.Map(editor); + page.MainPhoto = await FindMainPhotoAsync(editor.MainPhotoKey); - #endregion + return page; + } - #region Helpers + #endregion - /// - /// Completes and\or corrects the search request. - /// - private PagesListRequestVM NormalizeListRequest(PagesListRequestVM vm) - { - if (vm == null) - vm = new PagesListRequestVM(); + #region Helpers - var orderableFields = new[] {nameof(PageScoredVM.Title), nameof(PageScoredVM.LastUpdateDate), nameof(PageScoredVM.CreationDate), nameof(PageScoredVM.CompletenessScore)}; - if (!orderableFields.Contains(vm.OrderBy)) - vm.OrderBy = orderableFields[0]; + /// + /// Completes and\or corrects the search request. + /// + private PagesListRequestVM NormalizeListRequest(PagesListRequestVM vm) + { + if (vm == null) + vm = new PagesListRequestVM(); - if (vm.Page < 0) - vm.Page = 0; + var orderableFields = new[] {nameof(PageScoredVM.Title), nameof(PageScoredVM.LastUpdateDate), nameof(PageScoredVM.CreationDate), nameof(PageScoredVM.CompletenessScore)}; + if (!orderableFields.Contains(vm.OrderBy)) + vm.OrderBy = orderableFields[0]; - if (vm.OrderDescending == null) - vm.OrderDescending = false; + if (vm.Page < 0) + vm.Page = 0; - return vm; - } + if (vm.OrderDescending == null) + vm.OrderDescending = false; - /// - /// Checks if the create/update request contains valid data. - /// - private async Task ValidateRequestAsync(PageEditorVM vm) - { - var val = new Validator(); + return vm; + } - if (vm.Description == null) - vm.Description = string.Empty; + /// + /// Checks if the create/update request contains valid data. + /// + private async Task ValidateRequestAsync(PageEditorVM vm) + { + var val = new Validator(); + + if (vm.Description == null) + vm.Description = string.Empty; - var key = PageHelper.EncodeTitle(vm.Title).ToLowerInvariant(); - var otherPage = await _db.PageAliases - .AnyAsync(x => x.Key == key - && x.Page.Id != vm.Id); + var key = PageHelper.EncodeTitle(vm.Title).ToLowerInvariant(); + var otherPage = await _db.PageAliases + .AnyAsync(x => x.Key == key + && x.Page.Id != vm.Id); - if (otherPage) - val.Add(nameof(PageEditorVM.Title), Texts.Admin_Validation_Page_TitleAlreadyExists); + if (otherPage) + val.Add(nameof(PageEditorVM.Title), Texts.Admin_Validation_Page_TitleAlreadyExists); - if (!string.IsNullOrEmpty(vm.Aliases)) + if (!string.IsNullOrEmpty(vm.Aliases)) + { + var aliases = TryDeserialize>(vm.Aliases)?.Select(x => x.ToLowerInvariant()); + if (aliases == null) { - var aliases = TryDeserialize>(vm.Aliases)?.Select(x => x.ToLowerInvariant()); - if (aliases == null) - { - val.Add(nameof(PageEditorVM.Aliases), Texts.Admin_Validation_Page_InvalidAliases); - } - else - { - var otherAliases = await _db.PageAliases - .Where(x => aliases.Contains(x.Key) && x.Page.Id != vm.Id) - .Select(x => x.Title) - .ToListAsync(); + val.Add(nameof(PageEditorVM.Aliases), Texts.Admin_Validation_Page_InvalidAliases); + } + else + { + var otherAliases = await _db.PageAliases + .Where(x => aliases.Contains(x.Key) && x.Page.Id != vm.Id) + .Select(x => x.Title) + .ToListAsync(); - if (otherAliases.Any()) - val.Add(nameof(PageEditorVM.Aliases), string.Format(Texts.Admin_Validation_Page_AliasesAlreadyExist, otherAliases.JoinString(", "))); - } + if (otherAliases.Any()) + val.Add(nameof(PageEditorVM.Aliases), string.Format(Texts.Admin_Validation_Page_AliasesAlreadyExist, otherAliases.JoinString(", "))); } + } - val.ThrowIfInvalid(); - } + val.ThrowIfInvalid(); + } - /// - /// Gets the changeset for updates. - /// - private async Task GetChangesetAsync(PageEditorVM prev, PageEditorVM next, Guid id, ClaimsPrincipal principal, Guid? revertedId) - { - if(prev == null && next == null) - throw new ArgumentNullException(nameof(next), "Either prev or next must be provided."); + /// + /// Gets the changeset for updates. + /// + private async Task GetChangesetAsync(PageEditorVM prev, PageEditorVM next, Guid id, ClaimsPrincipal principal, Guid? revertedId) + { + if(prev == null && next == null) + throw new ArgumentNullException(nameof(next), "Either prev or next must be provided."); - var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); + var user = await _userMgr.GetUserAsync(principal, Texts.Admin_Users_NotFound); - return new Changeset - { - Id = Guid.NewGuid(), - RevertedChangesetId = revertedId, - ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), - EntityType = ChangesetEntityType.Page, - Date = DateTime.Now, - EditedPageId = id, - Author = user, - UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), - }; - } - - /// - /// Finds the image to use for the page. - /// - private async Task FindMainPhotoAsync(string key) + return new Changeset { - if (string.IsNullOrEmpty(key)) - return null; + Id = Guid.NewGuid(), + RevertedChangesetId = revertedId, + ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), + EntityType = ChangesetEntityType.Page, + Date = DateTime.Now, + EditedPageId = id, + Author = user, + UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), + }; + } - var media = await _db.Media - .FirstOrDefaultAsync(x => x.Key == key && x.IsDeleted == false); + /// + /// Finds the image to use for the page. + /// + private async Task FindMainPhotoAsync(string key) + { + if (string.IsNullOrEmpty(key)) + return null; - if(media == null) - throw new ValidationException(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Validation_Page_PhotoNotFound); + var media = await _db.Media + .FirstOrDefaultAsync(x => x.Key == key && x.IsDeleted == false); - if(media.Type != MediaType.Photo) - throw new ValidationException(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Validation_Page_InvalidPhoto); + if(media == null) + throw new ValidationException(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Validation_Page_PhotoNotFound); - return media; - } + if(media.Type != MediaType.Photo) + throw new ValidationException(nameof(PageEditorVM.MainPhotoKey), Texts.Admin_Validation_Page_InvalidPhoto); - /// - /// Checks if the serialized field contains valid data. - /// - private T TryDeserialize(string value) where T: class + return media; + } + + /// + /// Checks if the serialized field contains valid data. + /// + private T TryDeserialize(string value) where T: class + { + try { - try - { - return JsonConvert.DeserializeObject(value); - } - catch - { - return null; - } + return JsonConvert.DeserializeObject(value); } - - /// - /// Returns the list of aliases for current page. - /// - private IEnumerable GetPageAliases(string aliases, string title, Page page) + catch { - var aliasValues = JsonConvert.DeserializeObject>(aliases ?? "[]"); - if(!aliasValues.Contains(title)) - aliasValues.Add(title); - - return aliasValues.Select((x, idx) => - new PageAlias - { - Id = Guid.NewGuid(), - Page = page, - Key = PageHelper.EncodeTitle(x).ToLowerInvariant(), - Title = x, - NormalizedTitle = PageHelper.NormalizeTitle(x), - Order = idx - } - ); + return null; } + } - /// - /// Returns the list of pages references by the contents of current page. - /// - private async Task> GetPageReferencesAsync(string body, Page page) - { - var refs = MarkdownService.GetPageReferences(body); - var pages = await _db.Pages - .Where(x => refs.Contains(x.Key)) - .Select(x => new { x.Id, x.Key }) - .ToListAsync(); - - foreach(var p in pages) - _cache.Remove(p.Key); + /// + /// Returns the list of aliases for current page. + /// + private IEnumerable GetPageAliases(string aliases, string title, Page page) + { + var aliasValues = JsonConvert.DeserializeObject>(aliases ?? "[]"); + if(!aliasValues.Contains(title)) + aliasValues.Add(title); - return pages.Select(x => new PageReference + return aliasValues.Select((x, idx) => + new PageAlias { Id = Guid.NewGuid(), - DestinationId = x.Id, - Source = page - }); - } + Page = page, + Key = PageHelper.EncodeTitle(x).ToLowerInvariant(), + Title = x, + NormalizedTitle = PageHelper.NormalizeTitle(x), + Order = idx + } + ); + } + + /// + /// Returns the list of pages references by the contents of current page. + /// + private async Task> GetPageReferencesAsync(string body, Page page) + { + var refs = MarkdownService.GetPageReferences(body); + var pages = await _db.Pages + .Where(x => refs.Contains(x.Key)) + .Select(x => new { x.Id, x.Key }) + .ToListAsync(); + + foreach(var p in pages) + _cache.Remove(p.Key); - /// - /// Updates the cached values for living being overview from page facts. - /// - public static LivingBeingOverview MapLivingBeingOverview(PageEditorVM vm, LivingBeingOverview overview = null) + return pages.Select(x => new PageReference { - if (vm.Type is not PageType.Person and not PageType.Pet) - return null; + Id = Guid.NewGuid(), + DestinationId = x.Id, + Source = page + }); + } - if (string.IsNullOrEmpty(vm.Facts)) - return null; + /// + /// Updates the cached values for living being overview from page facts. + /// + public static LivingBeingOverview MapLivingBeingOverview(PageEditorVM vm, LivingBeingOverview overview = null) + { + if (vm.Type is not PageType.Person and not PageType.Pet) + return null; - overview ??= new LivingBeingOverview {PageId = vm.Id}; + if (string.IsNullOrEmpty(vm.Facts)) + return null; - var json = JObject.Parse(vm.Facts); - overview.BirthDate = json["Birth.Date"]?["Value"]?.Value(); - overview.DeathDate = json["Death.Date"]?["Value"]?.Value(); - overview.IsDead = json["Death.Date"] is not null; - overview.Gender = json["Bio.Gender"]?["IsMale"]?.Value() == true; + overview ??= new LivingBeingOverview {PageId = vm.Id}; - if (vm.Type is PageType.Person) - { - var names = json["Main.Name"]?["Values"]; + var json = JObject.Parse(vm.Facts); + overview.BirthDate = json["Birth.Date"]?["Value"]?.Value(); + overview.DeathDate = json["Death.Date"]?["Value"]?.Value(); + overview.IsDead = json["Death.Date"] is not null; + overview.Gender = json["Bio.Gender"]?["IsMale"]?.Value() == true; - overview.MaidenName = names?.Count() > 1 && names.FirstOrDefault() is { } oldestName - ? oldestName["LastName"]?.Value() - : null; + if (vm.Type is PageType.Person) + { + var names = json["Main.Name"]?["Values"]; - overview.ShortName = names?.LastOrDefault() is { } newestName - ? GetShortName(newestName) - : null; - } - else - { - overview.ShortName = json["Main.Name"]?["Value"]?.Value(); - } + overview.MaidenName = names?.Count() > 1 && names.FirstOrDefault() is { } oldestName + ? oldestName["LastName"]?.Value() + : null; + + overview.ShortName = names?.LastOrDefault() is { } newestName + ? GetShortName(newestName) + : null; + } + else + { + overview.ShortName = json["Main.Name"]?["Value"]?.Value(); + } - return overview; + return overview; - string GetShortName(JToken newestName) - { - var firstName = newestName["FirstName"]?.Value(); - var lastName = newestName["LastName"]?.Value(); + string GetShortName(JToken newestName) + { + var firstName = newestName["FirstName"]?.Value(); + var lastName = newestName["LastName"]?.Value(); - if (string.IsNullOrEmpty(firstName) || string.IsNullOrEmpty(lastName)) - return null; + if (string.IsNullOrEmpty(firstName) || string.IsNullOrEmpty(lastName)) + return null; - return $"{firstName} {lastName}"; - } + return $"{firstName} {lastName}"; } - - #endregion } -} + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/RelationsManagerService.cs b/src/Bonsai/Areas/Admin/Logic/RelationsManagerService.cs index 2b4e457d..f6d8c833 100644 --- a/src/Bonsai/Areas/Admin/Logic/RelationsManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/RelationsManagerService.cs @@ -24,440 +24,439 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// Service for managing relations. +/// +public class RelationsManagerService { - /// - /// Service for managing relations. - /// - public class RelationsManagerService + public RelationsManagerService(AppDbContext db, IMapper mapper, UserManager userMgr, RelationValidator validator, CacheService cache) { - public RelationsManagerService(AppDbContext db, IMapper mapper, UserManager userMgr, RelationValidator validator, CacheService cache) - { - _db = db; - _mapper = mapper; - _userMgr = userMgr; - _validator = validator; - _cache = cache; - } + _db = db; + _mapper = mapper; + _userMgr = userMgr; + _validator = validator; + _cache = cache; + } - private readonly AppDbContext _db; - private readonly IMapper _mapper; - private readonly UserManager _userMgr; - private readonly RelationValidator _validator; - private readonly CacheService _cache; + private readonly AppDbContext _db; + private readonly IMapper _mapper; + private readonly UserManager _userMgr; + private readonly RelationValidator _validator; + private readonly CacheService _cache; - /// - /// Returns the found relations. - /// - public async Task GetRelationsAsync(RelationsListRequestVM request) - { - const int PageSize = 50; + /// + /// Returns the found relations. + /// + public async Task GetRelationsAsync(RelationsListRequestVM request) + { + const int PageSize = 50; - request = NormalizeListRequest(request); + request = NormalizeListRequest(request); - var result = new RelationsListVM {Request = request}; - await FillAdditionalDataAsync(request, result); + var result = new RelationsListVM {Request = request}; + await FillAdditionalDataAsync(request, result); - var query = _db.Relations.Where(x => x.IsComplementary == false && x.IsDeleted == false); + var query = _db.Relations.Where(x => x.IsComplementary == false && x.IsDeleted == false); - if (request.EntityId != null) - query = query.Where(x => x.DestinationId == request.EntityId - || x.SourceId == request.EntityId - || x.EventId == request.EntityId); + if (request.EntityId != null) + query = query.Where(x => x.DestinationId == request.EntityId + || x.SourceId == request.EntityId + || x.EventId == request.EntityId); - if (!string.IsNullOrEmpty(request.SearchQuery)) - { - var req = PageHelper.NormalizeTitle(request.SearchQuery); - query = query.Where(x => x.Destination.NormalizedTitle.Contains(req) - || x.Source.NormalizedTitle.Contains(req) - || x.Event.NormalizedTitle.Contains(req)); - } + if (!string.IsNullOrEmpty(request.SearchQuery)) + { + var req = PageHelper.NormalizeTitle(request.SearchQuery); + query = query.Where(x => x.Destination.NormalizedTitle.Contains(req) + || x.Source.NormalizedTitle.Contains(req) + || x.Event.NormalizedTitle.Contains(req)); + } - if (request.Types?.Length > 0) - query = query.Where(x => request.Types.Contains(x.Type)); + if (request.Types?.Length > 0) + query = query.Where(x => request.Types.Contains(x.Type)); - var totalCount = await query.CountAsync(); - result.PageCount = (int) Math.Ceiling((double) totalCount / PageSize); + var totalCount = await query.CountAsync(); + result.PageCount = (int) Math.Ceiling((double) totalCount / PageSize); - var dir = request.OrderDescending ?? false; - if (request.OrderBy == nameof(RelationTitleVM.Destination)) - query = query.OrderBy(x => x.Destination.Title, dir); - else if (request.OrderBy == nameof(RelationTitleVM.Source)) - query = query.OrderBy(x => x.Source.Title, dir); - else - query = query.OrderBy(x => x.Type, dir); + var dir = request.OrderDescending ?? false; + if (request.OrderBy == nameof(RelationTitleVM.Destination)) + query = query.OrderBy(x => x.Destination.Title, dir); + else if (request.OrderBy == nameof(RelationTitleVM.Source)) + query = query.OrderBy(x => x.Source.Title, dir); + else + query = query.OrderBy(x => x.Type, dir); - result.Items = await query.ProjectToType(_mapper.Config) - .Skip(PageSize * request.Page) - .Take(PageSize) - .ToListAsync(); + result.Items = await query.ProjectToType(_mapper.Config) + .Skip(PageSize * request.Page) + .Take(PageSize) + .ToListAsync(); - return result; - } + return result; + } - /// - /// Creates a new relation. - /// - public async Task CreateAsync(RelationEditorVM vm, ClaimsPrincipal principal) + /// + /// Creates a new relation. + /// + public async Task CreateAsync(RelationEditorVM vm, ClaimsPrincipal principal) + { + await ValidateRequestAsync(vm, isNew: true); + + var newRels = new List(); + var updatedRels = new List(); + var groupId = vm.SourceIds.Length > 1 ? Guid.NewGuid() : (Guid?) null; + + var removedRelations = await _db.Relations + .Where(x => vm.SourceIds.Contains(x.SourceId) + && x.DestinationId == vm.DestinationId + && x.Type == vm.Type + && x.Id != vm.Id + && x.IsDeleted == true) + .ToDictionaryAsync(x => x.SourceId, x => x); + + var user = await GetUserAsync(principal); + foreach (var srcId in vm.SourceIds) { - await ValidateRequestAsync(vm, isNew: true); - - var newRels = new List(); - var updatedRels = new List(); - var groupId = vm.SourceIds.Length > 1 ? Guid.NewGuid() : (Guid?) null; - - var removedRelations = await _db.Relations - .Where(x => vm.SourceIds.Contains(x.SourceId) - && x.DestinationId == vm.DestinationId - && x.Type == vm.Type - && x.Id != vm.Id - && x.IsDeleted == true) - .ToDictionaryAsync(x => x.SourceId, x => x); - - var user = await GetUserAsync(principal); - foreach (var srcId in vm.SourceIds) - { - Relation rel; + Relation rel; - if (removedRelations.TryGetValue(srcId, out rel)) - { - rel.IsDeleted = false; + if (removedRelations.TryGetValue(srcId, out rel)) + { + rel.IsDeleted = false; - updatedRels.Add(rel); - } - else - { - rel = _mapper.Map(vm); - rel.Id = Guid.NewGuid(); - rel.SourceId = srcId; + updatedRels.Add(rel); + } + else + { + rel = _mapper.Map(vm); + rel.Id = Guid.NewGuid(); + rel.SourceId = srcId; - newRels.Add(rel); - } + newRels.Add(rel); + } - var compRel = new Relation {Id = Guid.NewGuid()}; - MapComplementaryRelation(rel, compRel); - newRels.Add(compRel); + var compRel = new Relation {Id = Guid.NewGuid()}; + MapComplementaryRelation(rel, compRel); + newRels.Add(compRel); - _db.Changes.Add(GetChangeset(null, _mapper.Map(rel), rel.Id, user, null, groupId)); - } + _db.Changes.Add(GetChangeset(null, _mapper.Map(rel), rel.Id, user, null, groupId)); + } - await _validator.ValidateAsync(newRels.Concat(updatedRels).ToList()); + await _validator.ValidateAsync(newRels.Concat(updatedRels).ToList()); - _db.Relations.AddRange(newRels); + _db.Relations.AddRange(newRels); - _cache.Clear(); - } + _cache.Clear(); + } - /// - /// Returns the form information for updating a relation. - /// - public async Task RequestUpdateAsync(Guid id) - { - var rel = await _db.Relations - .GetAsync(x => x.Id == id - && x.IsComplementary == false - && x.IsDeleted == false, Texts.Admin_Relations_NotFound); + /// + /// Returns the form information for updating a relation. + /// + public async Task RequestUpdateAsync(Guid id) + { + var rel = await _db.Relations + .GetAsync(x => x.Id == id + && x.IsComplementary == false + && x.IsDeleted == false, Texts.Admin_Relations_NotFound); - return _mapper.Map(rel); - } + return _mapper.Map(rel); + } - /// - /// Updates the relation. - /// - public async Task UpdateAsync(RelationEditorVM vm, ClaimsPrincipal principal, Guid? revertedId = null) - { - await ValidateRequestAsync(vm, isNew: false); + /// + /// Updates the relation. + /// + public async Task UpdateAsync(RelationEditorVM vm, ClaimsPrincipal principal, Guid? revertedId = null) + { + await ValidateRequestAsync(vm, isNew: false); - var rel = await _db.Relations - .GetAsync(x => x.Id == vm.Id - && x.IsComplementary == false - && (x.IsDeleted == false || revertedId != null), - Texts.Admin_Relations_NotFound); + var rel = await _db.Relations + .GetAsync(x => x.Id == vm.Id + && x.IsComplementary == false + && (x.IsDeleted == false || revertedId != null), + Texts.Admin_Relations_NotFound); - var compRel = await FindComplementaryRelationAsync(rel, revertedId != null); + var compRel = await FindComplementaryRelationAsync(rel, revertedId != null); - var user = await GetUserAsync(principal); - var prevVm = rel.IsDeleted ? null : _mapper.Map(rel); - var changeset = GetChangeset(prevVm, vm, rel.Id, user, revertedId); - _db.Changes.Add(changeset); + var user = await GetUserAsync(principal); + var prevVm = rel.IsDeleted ? null : _mapper.Map(rel); + var changeset = GetChangeset(prevVm, vm, rel.Id, user, revertedId); + _db.Changes.Add(changeset); - _mapper.Map(vm, rel); - MapComplementaryRelation(rel, compRel); + _mapper.Map(vm, rel); + MapComplementaryRelation(rel, compRel); - if(revertedId != null) - rel.IsDeleted = false; + if(revertedId != null) + rel.IsDeleted = false; - await _validator.ValidateAsync(new[] {rel, compRel}); + await _validator.ValidateAsync(new[] {rel, compRel}); - _cache.Clear(); - } + _cache.Clear(); + } - /// - /// Returns the brief information about a relation for reviewing before removal. - /// - public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) - { - var rel = await _db.Relations - .Where(x => x.IsDeleted == false && x.IsComplementary == false) - .ProjectToType(_mapper.Config) - .GetAsync(x => x.Id == id, Texts.Admin_Relations_NotFound); + /// + /// Returns the brief information about a relation for reviewing before removal. + /// + public async Task> RequestRemoveAsync(Guid id, ClaimsPrincipal principal) + { + var rel = await _db.Relations + .Where(x => x.IsDeleted == false && x.IsComplementary == false) + .ProjectToType(_mapper.Config) + .GetAsync(x => x.Id == id, Texts.Admin_Relations_NotFound); - var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); + var isAdmin = await _userMgr.IsInRoleAsync(principal, UserRole.Admin); - return new RemoveEntryInfoVM - { - Entry = rel, - CanRemoveCompletely = isAdmin - }; - } - - /// - /// Removes the relation. - /// - public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + return new RemoveEntryInfoVM { - var rel = await _db.Relations - .GetAsync(x => x.Id == id - && x.IsComplementary == false - && x.IsDeleted == false, Texts.Admin_Relations_NotFound); + Entry = rel, + CanRemoveCompletely = isAdmin + }; + } + + /// + /// Removes the relation. + /// + public async Task RemoveAsync(Guid id, ClaimsPrincipal principal) + { + var rel = await _db.Relations + .GetAsync(x => x.Id == id + && x.IsComplementary == false + && x.IsDeleted == false, Texts.Admin_Relations_NotFound); - var compRel = await FindComplementaryRelationAsync(rel); + var compRel = await FindComplementaryRelationAsync(rel); - var user = await GetUserAsync(principal); - var changeset = GetChangeset(_mapper.Map(rel), null, id, user, null); - _db.Changes.Add(changeset); + var user = await GetUserAsync(principal); + var changeset = GetChangeset(_mapper.Map(rel), null, id, user, null); + _db.Changes.Add(changeset); - rel.IsDeleted = true; - _db.Relations.Remove(compRel); + rel.IsDeleted = true; + _db.Relations.Remove(compRel); - _cache.Clear(); - } + _cache.Clear(); + } - /// - /// Removes the relation irreversibly. - /// - public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) - { - if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) - throw new OperationException(Texts.Admin_Users_Forbidden); + /// + /// Removes the relation irreversibly. + /// + public async Task RemoveCompletelyAsync(Guid id, ClaimsPrincipal principal) + { + if (await _userMgr.IsInRoleAsync(principal, UserRole.Admin) == false) + throw new OperationException(Texts.Admin_Users_Forbidden); - var rel = await _db.Relations - .GetAsync(x => x.Id == id - && x.IsComplementary == false - && x.IsDeleted == false, Texts.Admin_Relations_NotFound); + var rel = await _db.Relations + .GetAsync(x => x.Id == id + && x.IsComplementary == false + && x.IsDeleted == false, Texts.Admin_Relations_NotFound); - var compRel = await FindComplementaryRelationAsync(rel); - var compRelId = compRel?.Id ?? Guid.Empty; + var compRel = await FindComplementaryRelationAsync(rel); + var compRelId = compRel?.Id ?? Guid.Empty; - await _db.Changes.RemoveWhereAsync(x => x.EditedRelationId == id || x.EditedRelationId == compRelId); + await _db.Changes.RemoveWhereAsync(x => x.EditedRelationId == id || x.EditedRelationId == compRelId); - _db.Relations.Remove(rel); - if (compRel != null) - _db.Relations.Remove(compRel); + _db.Relations.Remove(rel); + if (compRel != null) + _db.Relations.Remove(compRel); - _cache.Clear(); - } + _cache.Clear(); + } - /// - /// Returns extended properties based on the relation type. - /// - public RelationEditorPropertiesVM GetPropertiesForRelationType(RelationType relType) + /// + /// Returns extended properties based on the relation type. + /// + public RelationEditorPropertiesVM GetPropertiesForRelationType(RelationType relType) + { + return new RelationEditorPropertiesVM { - return new RelationEditorPropertiesVM - { - SourceName = RelationHelper.ComplementaryRelations[relType].GetLocaleEnumDescription(), - DestinationName = relType.GetLocaleEnumDescription(), - SourceTypes = RelationHelper.SuggestSourcePageTypes(relType), - DestinationTypes = RelationHelper.SuggestDestinationPageTypes(relType), - ShowDuration = RelationHelper.IsRelationDurationAllowed(relType), - ShowEvent = RelationHelper.IsRelationEventReferenceAllowed(relType) - }; - } + SourceName = RelationHelper.ComplementaryRelations[relType].GetLocaleEnumDescription(), + DestinationName = relType.GetLocaleEnumDescription(), + SourceTypes = RelationHelper.SuggestSourcePageTypes(relType), + DestinationTypes = RelationHelper.SuggestDestinationPageTypes(relType), + ShowDuration = RelationHelper.IsRelationDurationAllowed(relType), + ShowEvent = RelationHelper.IsRelationEventReferenceAllowed(relType) + }; + } - #region Helpers + #region Helpers - /// - /// Completes and\or corrects the search request. - /// - private RelationsListRequestVM NormalizeListRequest(RelationsListRequestVM vm) - { - if (vm == null) - vm = new RelationsListRequestVM(); + /// + /// Completes and\or corrects the search request. + /// + private RelationsListRequestVM NormalizeListRequest(RelationsListRequestVM vm) + { + if (vm == null) + vm = new RelationsListRequestVM(); - var orderableFields = new[] {nameof(RelationTitleVM.Destination), nameof(RelationTitleVM.Source), nameof(RelationTitleVM.Type)}; - if (!orderableFields.Contains(vm.OrderBy)) - vm.OrderBy = orderableFields[0]; + var orderableFields = new[] {nameof(RelationTitleVM.Destination), nameof(RelationTitleVM.Source), nameof(RelationTitleVM.Type)}; + if (!orderableFields.Contains(vm.OrderBy)) + vm.OrderBy = orderableFields[0]; - if (vm.Page < 0) - vm.Page = 0; + if (vm.Page < 0) + vm.Page = 0; - if (vm.OrderDescending == null) - vm.OrderDescending = false; + if (vm.OrderDescending == null) + vm.OrderDescending = false; - return vm; - } + return vm; + } - /// - /// Checks if the create/update request contains valid data. - /// - private async Task ValidateRequestAsync(RelationEditorVM vm, bool isNew) - { - var val = new Validator(); + /// + /// Checks if the create/update request contains valid data. + /// + private async Task ValidateRequestAsync(RelationEditorVM vm, bool isNew) + { + var val = new Validator(); - vm.SourceIds = vm.SourceIds ?? new Guid[0]; + vm.SourceIds = vm.SourceIds ?? Array.Empty(); - var pageIds = vm.SourceIds - .Concat(new [] {vm.DestinationId ?? Guid.Empty, vm.EventId ?? Guid.Empty}) - .ToList(); + var pageIds = vm.SourceIds + .Concat(new [] {vm.DestinationId ?? Guid.Empty, vm.EventId ?? Guid.Empty}) + .ToList(); - var pages = await _db.Pages - .Where(x => pageIds.Contains(x.Id)) - .ToDictionaryAsync(x => x.Id, x => x.Type); + var pages = await _db.Pages + .Where(x => pageIds.Contains(x.Id)) + .ToDictionaryAsync(x => x.Id, x => x.Type); - var sourceTypes = vm.SourceIds.Select(x => pages.TryGetNullableValue(x)).ToList(); - var destType = pages.TryGetNullableValue(vm.DestinationId ?? Guid.Empty); - var eventType = pages.TryGetNullableValue(vm.EventId ?? Guid.Empty); + var sourceTypes = vm.SourceIds.Select(x => pages.TryGetNullableValue(x)).ToList(); + var destType = pages.TryGetNullableValue(vm.DestinationId ?? Guid.Empty); + var eventType = pages.TryGetNullableValue(vm.EventId ?? Guid.Empty); - if(vm.SourceIds == null || vm.SourceIds.Length == 0) - val.Add(nameof(vm.SourceIds), Texts.Admin_Validation_Relation_PageNotSelected); - else if (isNew == false && vm.SourceIds.Length > 1) - val.Add(nameof(vm.SourceIds), Texts.Admin_Validation_Relation_OnlyOneSource); - else if (sourceTypes.Any(x => x == null)) - val.Add(nameof(vm.SourceIds), Texts.Admin_Pages_NotFound); + if(vm.SourceIds == null || vm.SourceIds.Length == 0) + val.Add(nameof(vm.SourceIds), Texts.Admin_Validation_Relation_PageNotSelected); + else if (isNew == false && vm.SourceIds.Length > 1) + val.Add(nameof(vm.SourceIds), Texts.Admin_Validation_Relation_OnlyOneSource); + else if (sourceTypes.Any(x => x == null)) + val.Add(nameof(vm.SourceIds), Texts.Admin_Pages_NotFound); - if(vm.DestinationId == null) - val.Add(nameof(vm.DestinationId), Texts.Admin_Validation_Relation_PageNotSelected); - else if (destType == null) - val.Add(nameof(vm.DestinationId), Texts.Admin_Pages_NotFound); + if(vm.DestinationId == null) + val.Add(nameof(vm.DestinationId), Texts.Admin_Validation_Relation_PageNotSelected); + else if (destType == null) + val.Add(nameof(vm.DestinationId), Texts.Admin_Pages_NotFound); - if (destType != null && sourceTypes.Any(x => x != null && !RelationHelper.IsRelationAllowed(x.Value, destType.Value, vm.Type))) - val.Add(nameof(vm.Type), Texts.Admin_Validation_Relation_RelationNotAllowedForPageTypes); + if (destType != null && sourceTypes.Any(x => x != null && !RelationHelper.IsRelationAllowed(x.Value, destType.Value, vm.Type))) + val.Add(nameof(vm.Type), Texts.Admin_Validation_Relation_RelationNotAllowedForPageTypes); - if (vm.EventId != null) + if (vm.EventId != null) + { + if(eventType == null) + val.Add(nameof(vm.EventId), Texts.Admin_Pages_NotFound); + else if(eventType != PageType.Event) + val.Add(nameof(vm.EventId), Texts.Admin_Validation_Relation_EventPageRequired); + else if(!RelationHelper.IsRelationEventReferenceAllowed(vm.Type)) + val.Add(nameof(vm.EventId), Texts.Admin_Validation_Relation_EventPageNotAllowedForType); + } + + if (!string.IsNullOrEmpty(vm.DurationStart) || !string.IsNullOrEmpty(vm.DurationEnd)) + { + if (!RelationHelper.IsRelationDurationAllowed(vm.Type)) { - if(eventType == null) - val.Add(nameof(vm.EventId), Texts.Admin_Pages_NotFound); - else if(eventType != PageType.Event) - val.Add(nameof(vm.EventId), Texts.Admin_Validation_Relation_EventPageRequired); - else if(!RelationHelper.IsRelationEventReferenceAllowed(vm.Type)) - val.Add(nameof(vm.EventId), Texts.Admin_Validation_Relation_EventPageNotAllowedForType); + val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_Relation_DateNotAllowedForType); } - - if (!string.IsNullOrEmpty(vm.DurationStart) || !string.IsNullOrEmpty(vm.DurationEnd)) + else { - if (!RelationHelper.IsRelationDurationAllowed(vm.Type)) - { - val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_Relation_DateNotAllowedForType); - } - else - { - var from = FuzzyDate.TryParse(vm.DurationStart); - var to = FuzzyDate.TryParse(vm.DurationEnd); - - if (from > to) - val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_Relation_StartAfterEnd); - else if (FuzzyRange.TryParse(FuzzyRange.TryCombine(vm.DurationStart, vm.DurationEnd)) == null) - val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_IncorrectDate); - - } + var from = FuzzyDate.TryParse(vm.DurationStart); + var to = FuzzyDate.TryParse(vm.DurationEnd); + + if (from > to) + val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_Relation_StartAfterEnd); + else if (FuzzyRange.TryParse(FuzzyRange.TryCombine(vm.DurationStart, vm.DurationEnd)) == null) + val.Add(nameof(vm.DurationStart), Texts.Admin_Validation_IncorrectDate); + } + } - var existingRelation = await _db.Relations - .AnyAsync(x => vm.SourceIds.Contains(x.SourceId) - && x.DestinationId == vm.DestinationId - && x.Type == vm.Type - && x.Id != vm.Id - && x.IsDeleted == false); + var existingRelation = await _db.Relations + .AnyAsync(x => vm.SourceIds.Contains(x.SourceId) + && x.DestinationId == vm.DestinationId + && x.Type == vm.Type + && x.Id != vm.Id + && x.IsDeleted == false); - if (existingRelation) - val.Add(nameof(vm.DestinationId), Texts.Admin_Validation_Relation_AlreadyExists); + if (existingRelation) + val.Add(nameof(vm.DestinationId), Texts.Admin_Validation_Relation_AlreadyExists); - val.ThrowIfInvalid(); - } + val.ThrowIfInvalid(); + } - /// - /// Gets the changeset for updates. - /// - private Changeset GetChangeset(RelationEditorVM prev, RelationEditorVM next, Guid id, AppUser user, Guid? revertedId, Guid? groupId = null) + /// + /// Gets the changeset for updates. + /// + private Changeset GetChangeset(RelationEditorVM prev, RelationEditorVM next, Guid id, AppUser user, Guid? revertedId, Guid? groupId = null) + { + if(prev == null && next == null) + throw new ArgumentNullException(); + + return new Changeset { - if(prev == null && next == null) - throw new ArgumentNullException(); + Id = Guid.NewGuid(), + RevertedChangesetId = revertedId, + GroupId = groupId, + ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), + EntityType = ChangesetEntityType.Relation, + Date = DateTime.Now, + EditedRelationId = id, + Author = user, + UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), + }; + } - return new Changeset - { - Id = Guid.NewGuid(), - RevertedChangesetId = revertedId, - GroupId = groupId, - ChangeType = ChangesetHelper.GetChangeType(prev, next, revertedId), - EntityType = ChangesetEntityType.Relation, - Date = DateTime.Now, - EditedRelationId = id, - Author = user, - UpdatedState = next == null ? null : JsonConvert.SerializeObject(next), - }; - } + /// + /// Creates a complimentary inverse relation. + /// + private void MapComplementaryRelation(Relation source, Relation target) + { + target.SourceId = source.DestinationId; + target.DestinationId = source.SourceId; + target.Type = RelationHelper.ComplementaryRelations[source.Type]; + target.EventId = source.EventId; + target.Duration = source.Duration; + target.IsComplementary = true; + } - /// - /// Creates a complimentary inverse relation. - /// - private void MapComplementaryRelation(Relation source, Relation target) + /// + /// Removes the complementary relation (it is always recreated). + /// + private async Task FindComplementaryRelationAsync(Relation rel, bool includeDeleted = false) + { + if (includeDeleted) { - target.SourceId = source.DestinationId; - target.DestinationId = source.SourceId; - target.Type = RelationHelper.ComplementaryRelations[source.Type]; - target.EventId = source.EventId; - target.Duration = source.Duration; - target.IsComplementary = true; + var compRel = new Relation {Id = Guid.NewGuid()}; + _db.Relations.Add(compRel); + return compRel; } - /// - /// Removes the complementary relation (it is always recreated). - /// - private async Task FindComplementaryRelationAsync(Relation rel, bool includeDeleted = false) - { - if (includeDeleted) - { - var compRel = new Relation {Id = Guid.NewGuid()}; - _db.Relations.Add(compRel); - return compRel; - } - - var compRelType = RelationHelper.ComplementaryRelations[rel.Type]; - return await _db.Relations - .FirstOrDefaultAsync(x => x.SourceId == rel.DestinationId - && x.DestinationId == rel.SourceId - && x.Type == compRelType - && x.IsComplementary); - } + var compRelType = RelationHelper.ComplementaryRelations[rel.Type]; + return await _db.Relations + .FirstOrDefaultAsync(x => x.SourceId == rel.DestinationId + && x.DestinationId == rel.SourceId + && x.Type == compRelType + && x.IsComplementary); + } - /// - /// Loads extra data for the filter. - /// - private async Task FillAdditionalDataAsync(RelationsListRequestVM request, RelationsListVM data) + /// + /// Loads extra data for the filter. + /// + private async Task FillAdditionalDataAsync(RelationsListRequestVM request, RelationsListVM data) + { + if (request.EntityId != null) { - if (request.EntityId != null) - { - var title = await _db.Pages - .Where(x => x.Id == request.EntityId) - .Select(x => x.Title) - .FirstOrDefaultAsync(); - - if (title != null) - data.EntityTitle = title; - else - request.EntityId = null; - } - } + var title = await _db.Pages + .Where(x => x.Id == request.EntityId) + .Select(x => x.Title) + .FirstOrDefaultAsync(); - /// - /// Returns the user corresponding to this principal. - /// - private async Task GetUserAsync(ClaimsPrincipal principal) - { - var userId = _userMgr.GetUserId(principal); - return await _db.Users.GetAsync(x => x.Id == userId, Texts.Admin_Users_NotFound); + if (title != null) + data.EntityTitle = title; + else + request.EntityId = null; } + } - #endregion + /// + /// Returns the user corresponding to this principal. + /// + private async Task GetUserAsync(ClaimsPrincipal principal) + { + var userId = _userMgr.GetUserId(principal); + return await _db.Users.GetAsync(x => x.Id == userId, Texts.Admin_Users_NotFound); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/SuggestService.cs b/src/Bonsai/Areas/Admin/Logic/SuggestService.cs index b59066d3..8e93e2ad 100644 --- a/src/Bonsai/Areas/Admin/Logic/SuggestService.cs +++ b/src/Bonsai/Areas/Admin/Logic/SuggestService.cs @@ -17,160 +17,158 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// Service for various lookups required by the interface. +/// +public class SuggestService { + public SuggestService(AppDbContext db, ISearchEngine search, IUrlHelper urlHelper, IMapper mapper) + { + _db = db; + _search = search; + _url = urlHelper; + _mapper = mapper; + } + + private readonly AppDbContext _db; + private readonly ISearchEngine _search; + private readonly IUrlHelper _url; + private readonly IMapper _mapper; + /// - /// Service for various lookups required by the interface. + /// Suggests pages of specified types. /// - public class SuggestService + public async Task> SuggestPagesAsync( + PickRequestVM request, + Func, IReadOnlyList> extraFilter = null + ) { - public SuggestService(AppDbContext db, ISearchEngine search, IUrlHelper urlHelper, IMapper mapper) - { - _db = db; - _search = search; - _url = urlHelper; - _mapper = mapper; - } + var search = await _search.SuggestAsync(request.Query, request.Types, 100); - private readonly AppDbContext _db; - private readonly ISearchEngine _search; - private readonly IUrlHelper _url; - private readonly IMapper _mapper; - - /// - /// Suggests pages of specified types. - /// - public async Task> SuggestPagesAsync( - PickRequestVM request, - Func, IReadOnlyList> extraFilter = null - ) - { - var search = await _search.SuggestAsync(request.Query, request.Types, 100); + var ids = (IReadOnlyList) search.Select(x => x.Id).ToList(); - var ids = (IReadOnlyList) search.Select(x => x.Id).ToList(); + if (extraFilter != null) + ids = extraFilter(ids); - if (extraFilter != null) - ids = extraFilter(ids); + var pages = await _db.Pages + .Where(x => ids.Contains(x.Id)) + .ProjectToType(_mapper.Config) + .ToDictionaryAsync(x => x.Id, x => x); - var pages = await _db.Pages - .Where(x => ids.Contains(x.Id)) - .ProjectToType(_mapper.Config) - .ToDictionaryAsync(x => x.Id, x => x); + // URL is global for easier usage in JSON + foreach (var page in pages.Values) + page.MainPhotoPath = GetFullThumbnailPath(page); - // URL is global for easier usage in JSON - foreach (var page in pages.Values) - page.MainPhotoPath = GetFullThumbnailPath(page); + return ids.Select(x => pages[x]).ToList(); + } - return ids.Select(x => pages[x]).ToList(); - } + /// + /// Suggests pages for the relations editor. + /// + public async Task> SuggestRelationPagesAsync(RelationSuggestQueryVM request) + { + if (request == null) + return null; - /// - /// Suggests pages for the relations editor. - /// - public async Task> SuggestRelationPagesAsync(RelationSuggestQueryVM request) - { - if (request == null) - return null; + var subRequest = new PickRequestVM {Query = request.Query, Types = request.Types}; - var subRequest = new PickRequestVM {Query = request.Query, Types = request.Types}; + if (request.DestinationId == null && request.SourceId == null) + return await SuggestPagesAsync(subRequest); - if (request.DestinationId == null && request.SourceId == null) - return await SuggestPagesAsync(subRequest); + var queryRoot = _db.Relations + .Where(x => x.IsDeleted == false); - var queryRoot = _db.Relations - .Where(x => x.IsDeleted == false); + var idQuery = request.DestinationId != null + ? queryRoot.Where(x => x.DestinationId == request.DestinationId) + .Select(x => x.SourceId) + : queryRoot.Where(x => x.SourceId == request.SourceId) + .Select(x => x.DestinationId); - var idQuery = request.DestinationId != null - ? queryRoot.Where(x => x.DestinationId == request.DestinationId) - .Select(x => x.SourceId) - : queryRoot.Where(x => x.SourceId == request.SourceId) - .Select(x => x.DestinationId); + var existingIds = await idQuery.ToHashSetAsync(); - var existingIds = await idQuery.ToHashSetAsync(); + var selfId = request.DestinationId ?? request.SourceId ?? Guid.Empty; + existingIds.Add(selfId); - var selfId = request.DestinationId ?? request.SourceId ?? Guid.Empty; - existingIds.Add(selfId); + return await SuggestPagesAsync( + new PickRequestVM { Query = request.Query, Types = request.Types }, + ids => ids.Where(id => !existingIds.Contains(id)).ToList() + ); + } - return await SuggestPagesAsync( - new PickRequestVM { Query = request.Query, Types = request.Types }, - ids => ids.Where(id => !existingIds.Contains(id)).ToList() - ); - } + /// + /// Returns the pickable pages. + /// + public async Task> GetPickablePagesAsync(PickRequestVM request) + { + var q = _db.Pages.AsQueryable(); - /// - /// Returns the pickable pages. - /// - public async Task> GetPickablePagesAsync(PickRequestVM request) + if (!string.IsNullOrEmpty(request.Query)) { - var q = _db.Pages.AsQueryable(); + var queryNormalized = PageHelper.NormalizeTitle(request.Query); + q = q.Where(x => x.Aliases.Any(y => y.NormalizedTitle.Contains(queryNormalized))); + } - if (!string.IsNullOrEmpty(request.Query)) - { - var queryNormalized = PageHelper.NormalizeTitle(request.Query); - q = q.Where(x => x.Aliases.Any(y => y.NormalizedTitle.Contains(queryNormalized))); - } + if (request.Types?.Length > 0) + q = q.Where(x => request.Types.Contains(x.Type)); - if (request.Types?.Length > 0) - q = q.Where(x => request.Types.Contains(x.Type)); + var count = Math.Clamp(request.Count ?? 100, 1, 100); + var offset = Math.Max(request.Offset ?? 0, 0); - var count = Math.Clamp(request.Count ?? 100, 1, 100); - var offset = Math.Max(request.Offset ?? 0, 0); + var vms = await q.OrderBy(x => x.Title) + .Skip(offset) + .Take(count) + .ProjectToType(_mapper.Config) + .ToListAsync(); - var vms = await q.OrderBy(x => x.Title) - .Skip(offset) - .Take(count) - .ProjectToType(_mapper.Config) - .ToListAsync(); + foreach (var vm in vms) + vm.MainPhotoPath = GetFullThumbnailPath(vm); - foreach (var vm in vms) - vm.MainPhotoPath = GetFullThumbnailPath(vm); + return vms; + } - return vms; - } + /// + /// Returns the pickable media. + /// + public async Task> GetPickableMediaAsync(PickRequestVM request) + { + var q = _db.Media.AsNoTracking(); - /// - /// Returns the pickable media. - /// - public async Task> GetPickableMediaAsync(PickRequestVM request) + if (!string.IsNullOrEmpty(request.Query)) { - var q = _db.Media.AsNoTracking(); - - if (!string.IsNullOrEmpty(request.Query)) - { - var queryNormalized = PageHelper.NormalizeTitle(request.Query); - q = q.Where(x => x.NormalizedTitle.Contains(queryNormalized)); - } + var queryNormalized = PageHelper.NormalizeTitle(request.Query); + q = q.Where(x => x.NormalizedTitle.Contains(queryNormalized)); + } - if (request.Types?.Length > 0) - q = q.Where(x => request.Types.Contains(x.Type)); + if (request.Types?.Length > 0) + q = q.Where(x => request.Types.Contains(x.Type)); - var count = Math.Clamp(request.Count ?? 100, 1, 100); - var offset = Math.Max(request.Offset ?? 0, 0); + var count = Math.Clamp(request.Count ?? 100, 1, 100); + var offset = Math.Max(request.Offset ?? 0, 0); - var media = await q.OrderByDescending(x => x.UploadDate) - .Skip(offset) - .Take(count) - .ToListAsync(); + var media = await q.OrderByDescending(x => x.UploadDate) + .Skip(offset) + .Take(count) + .ToListAsync(); - var vms = media.Select(x => MediaPresenterService.GetMediaThumbnail(x, MediaSize.Small)).ToList(); + var vms = media.Select(x => MediaPresenterService.GetMediaThumbnail(x, MediaSize.Small)).ToList(); - foreach (var vm in vms) - vm.ThumbnailUrl = _url.Content(vm.ThumbnailUrl); + foreach (var vm in vms) + vm.ThumbnailUrl = _url.Content(vm.ThumbnailUrl); - return vms; - } + return vms; + } - #region Helpers + #region Helpers - /// - /// Returns the full path for the page's main photo. - /// - private string GetFullThumbnailPath(PageTitleExtendedVM page) - { - return _url.Content(PageHelper.GetPageImageUrl(page.Type, page.MainPhotoPath)); - } - - #endregion + /// + /// Returns the full path for the page's main photo. + /// + private string GetFullThumbnailPath(PageTitleExtendedVM page) + { + return _url.Content(PageHelper.GetPageImageUrl(page.Type, page.MainPhotoPath)); } -} + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.FullTree.cs b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.FullTree.cs index 6cf653c6..1041d968 100644 --- a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.FullTree.cs +++ b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.FullTree.cs @@ -11,97 +11,96 @@ using Impworks.Utils.Linq; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Logic.Tree +namespace Bonsai.Areas.Admin.Logic.Tree; + +/// +/// Background job for recalculating the entire tree's layout. +/// +public partial class TreeLayoutJob { - /// - /// Background job for recalculating the entire tree's layout. - /// - public partial class TreeLayoutJob + protected async Task ProcessFullTreeAsync(RelationContext ctx, CancellationToken token) { - protected async Task ProcessFullTreeAsync(RelationContext ctx, CancellationToken token) - { - if (_config.TreeKinds.HasFlag(TreeKind.FullTree) == false) - return; + if (_config.TreeKinds.HasFlag(TreeKind.FullTree) == false) + return; - var trees = GetAllSubtrees(ctx); - var thoroughness = GetThoroughness(); + var trees = GetAllSubtrees(ctx); + var thoroughness = GetThoroughness(); - _logger.Information($"Full tree layout started: {ctx.Pages.Count} people, {ctx.Relations.Count} rels, {trees.Count} subtrees."); + _logger.Information($"Full tree layout started: {ctx.Pages.Count} people, {ctx.Relations.Count} rels, {trees.Count} subtrees."); - foreach (var tree in trees) + foreach (var tree in trees) + { + var rendered = await RenderTreeAsync(tree, thoroughness, token); + var layout = new TreeLayout { - var rendered = await RenderTreeAsync(tree, thoroughness, token); - var layout = new TreeLayout - { - Id = Guid.NewGuid(), - LayoutJson = rendered, - GenerationDate = DateTimeOffset.Now - }; - - await SaveLayoutAsync(_db, tree, layout); - } - - _logger.Information("Full tree layout completed."); + Id = Guid.NewGuid(), + LayoutJson = rendered, + GenerationDate = DateTimeOffset.Now + }; + + await SaveLayoutAsync(_db, tree, layout); } - #region Tree generation + _logger.Information("Full tree layout completed."); + } - /// - /// Loads all pages and groups them into subgraphs by relations. - /// - private IReadOnlyList GetAllSubtrees(RelationContext ctx) - { - var visited = new HashSet(); - var result = new List(); + #region Tree generation - foreach (var pageId in ctx.Pages.Keys) - { - if (visited.Contains(pageId.ToString())) - continue; + /// + /// Loads all pages and groups them into subgraphs by relations. + /// + private IReadOnlyList GetAllSubtrees(RelationContext ctx) + { + var visited = new HashSet(); + var result = new List(); - var subtree = GetSubtree(ctx, pageId, _ => TraverseMode.Normal); - result.Add(subtree); + foreach (var pageId in ctx.Pages.Keys) + { + if (visited.Contains(pageId.ToString())) + continue; - foreach (var person in subtree.Persons) - visited.Add(person.Id); - } + var subtree = GetSubtree(ctx, pageId, _ => TraverseMode.Normal); + result.Add(subtree); - return result; + foreach (var person in subtree.Persons) + visited.Add(person.Id); } - /// - /// Returns interpolated thoroughness. - /// - private int GetThoroughness() - { - return Interpolator.MapValue( - _config.TreeRenderThoroughness, - new IntervalMap(1, 10, 1, 10), - new IntervalMap(11, 50, 11, 600), - new IntervalMap(51, 100, 601, 15000) - ); - } + return result; + } - #endregion + /// + /// Returns interpolated thoroughness. + /// + private int GetThoroughness() + { + return Interpolator.MapValue( + _config.TreeRenderThoroughness, + new IntervalMap(1, 10, 1, 10), + new IntervalMap(11, 50, 11, 600), + new IntervalMap(51, 100, 601, 15000) + ); + } - #region Database processing + #endregion - /// - /// Updates the layout. - /// - private async Task SaveLayoutAsync(AppDbContext db, TreeLayoutVM tree, TreeLayout layout) - { - db.TreeLayouts.Add(layout); - await db.SaveChangesAsync(); + #region Database processing - foreach (var batch in tree.Persons.Select(x => Guid.Parse(x.Id)).PartitionBySize(100)) - { - await db.Pages - .Where(x => batch.Contains(x.Id)) - .ExecuteUpdateAsync(x => x.SetProperty(p => p.TreeLayoutId, layout.Id)); - } - } + /// + /// Updates the layout. + /// + private async Task SaveLayoutAsync(AppDbContext db, TreeLayoutVM tree, TreeLayout layout) + { + db.TreeLayouts.Add(layout); + await db.SaveChangesAsync(); - #endregion + foreach (var batch in tree.Persons.Select(x => Guid.Parse(x.Id)).PartitionBySize(100)) + { + await db.Pages + .Where(x => batch.Contains(x.Id)) + .ExecuteUpdateAsync(x => x.SetProperty(p => p.TreeLayoutId, layout.Id)); + } } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.PartialTrees.cs b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.PartialTrees.cs index eb21abbe..df33d9a6 100644 --- a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.PartialTrees.cs +++ b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.PartialTrees.cs @@ -10,115 +10,114 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.Logic.Tree +namespace Bonsai.Areas.Admin.Logic.Tree; + +/// +/// Background job for recalculating the partial trees. +/// +public partial class TreeLayoutJob { /// - /// Background job for recalculating the partial trees. + /// Processes the layouts for a particular tree kind. /// - public partial class TreeLayoutJob + private async Task ProcessPartialTreeAsync(TreeKind kind, Func treeGetter, RelationContext ctx, CancellationToken token) { - /// - /// Processes the layouts for a particular tree kind. - /// - private async Task ProcessPartialTreeAsync(TreeKind kind, Func treeGetter, RelationContext ctx, CancellationToken token) - { - if (_config.TreeKinds.HasFlag(kind) == false) - return; + if (_config.TreeKinds.HasFlag(kind) == false) + return; - _logger.Information($"Partial tree ({kind}) layouts started: {ctx.Pages.Count} subtrees."); + _logger.Information($"Partial tree ({kind}) layouts started: {ctx.Pages.Count} subtrees."); - var layouts = new List(); - foreach (var page in ctx.Pages.Values) - { - var tree = treeGetter(ctx, page.Id); + var layouts = new List(); + foreach (var page in ctx.Pages.Values) + { + var tree = treeGetter(ctx, page.Id); - try - { - var rendered = await RenderTreeAsync(tree, 1000, token); + try + { + var rendered = await RenderTreeAsync(tree, 1000, token); - layouts.Add(new TreeLayout - { - Id = Guid.NewGuid(), - LayoutJson = rendered, - GenerationDate = DateTimeOffset.Now, - PageId = tree.PageId, - Kind = kind - }); - } - catch (Exception ex) + layouts.Add(new TreeLayout { - var json = JsonConvert.SerializeObject(tree); - _logger.Error(ex.Demystify(), $"Failed to render partial tree ({kind}) for page {page.Id}:\n{json}"); - } + Id = Guid.NewGuid(), + LayoutJson = rendered, + GenerationDate = DateTimeOffset.Now, + PageId = tree.PageId, + Kind = kind + }); } + catch (Exception ex) + { + var json = JsonConvert.SerializeObject(tree); + _logger.Error(ex.Demystify(), $"Failed to render partial tree ({kind}) for page {page.Id}:\n{json}"); + } + } - await _db.TreeLayouts.Where(x => x.Kind == kind).ExecuteDeleteAsync(CancellationToken.None); + await _db.TreeLayouts.Where(x => x.Kind == kind).ExecuteDeleteAsync(CancellationToken.None); - _db.TreeLayouts.AddRange(layouts); - await _db.SaveChangesAsync(CancellationToken.None); + _db.TreeLayouts.AddRange(layouts); + await _db.SaveChangesAsync(CancellationToken.None); - _logger.Information($"Partial tree ({kind}) layouts completed."); - } + _logger.Information($"Partial tree ({kind}) layouts completed."); + } - /// - /// Finds close family members for the page. - /// - private TreeLayoutVM GetCloseFamilyTree(RelationContext ctx, Guid pageId) - { - return GetSubtree( - ctx, - pageId, - fc => - { - // explicitly avoid e.g. other husbands of a man's wife - if (fc is {Distance: 2, RelationType: RelationType.Spouse, LastRelationType: RelationType.Spouse}) - return null; + /// + /// Finds close family members for the page. + /// + private TreeLayoutVM GetCloseFamilyTree(RelationContext ctx, Guid pageId) + { + return GetSubtree( + ctx, + pageId, + fc => + { + // explicitly avoid e.g. other husbands of a man's wife + if (fc is {Distance: 2, RelationType: RelationType.Spouse, LastRelationType: RelationType.Spouse}) + return null; - return fc.Distance switch - { - < 2 => TraverseMode.Normal, - 2 => fc.RelationType == RelationType.Child - ? TraverseMode.SetParents - : TraverseMode.DeadEnd, - _ => null - }; - }); - } + return fc.Distance switch + { + < 2 => TraverseMode.Normal, + 2 => fc.RelationType == RelationType.Child + ? TraverseMode.SetParents + : TraverseMode.DeadEnd, + _ => null + }; + }); + } - /// - /// Finds ancestors for the page. - /// - private TreeLayoutVM GetAncestorsTree(RelationContext ctx, Guid pageId) - { - return GetSubtree( - ctx, - pageId, - fc => fc.RelationType == RelationType.Parent - ? TraverseMode.Normal - : null - ); - } + /// + /// Finds ancestors for the page. + /// + private TreeLayoutVM GetAncestorsTree(RelationContext ctx, Guid pageId) + { + return GetSubtree( + ctx, + pageId, + fc => fc.RelationType == RelationType.Parent + ? TraverseMode.Normal + : null + ); + } - /// - /// Finds descendants for the page. - /// - private TreeLayoutVM GetDescendantsTree(RelationContext ctx, Guid pageId) - { - return GetSubtree( - ctx, - pageId, - fc => - { - if (fc.RelationType == RelationType.Child) - return TraverseMode.Normal; + /// + /// Finds descendants for the page. + /// + private TreeLayoutVM GetDescendantsTree(RelationContext ctx, Guid pageId) + { + return GetSubtree( + ctx, + pageId, + fc => + { + if (fc.RelationType == RelationType.Child) + return TraverseMode.Normal; - if (fc.RelationType == RelationType.Parent && fc.PageId != pageId) - return TraverseMode.DeadEnd; + if (fc.RelationType == RelationType.Parent && fc.PageId != pageId) + return TraverseMode.DeadEnd; - return null; - }, - TraverseMode.TraverseRelations - ); - } + return null; + }, + TraverseMode.TraverseRelations + ); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.cs b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.cs index cc8f6277..6dbb1c92 100644 --- a/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.cs +++ b/src/Bonsai/Areas/Admin/Logic/Tree/TreeLayoutJob.cs @@ -19,224 +19,223 @@ using Newtonsoft.Json; using Serilog; -namespace Bonsai.Areas.Admin.Logic.Tree +namespace Bonsai.Areas.Admin.Logic.Tree; + +public partial class TreeLayoutJob: JobBase { - public partial class TreeLayoutJob: JobBase + public TreeLayoutJob(AppDbContext db, INodeJSService js, BonsaiConfigService config, ILogger logger) { - public TreeLayoutJob(AppDbContext db, INodeJSService js, BonsaiConfigService config, ILogger logger) - { - _db = db; - _js = js; - _config = config.GetDynamicConfig(); - _logger = logger; - } - - private readonly AppDbContext _db; - private readonly INodeJSService _js; - private readonly DynamicConfig _config; - private readonly ILogger _logger; + _db = db; + _js = js; + _config = config.GetDynamicConfig(); + _logger = logger; + } - protected override async Task ExecuteAsync(CancellationToken token) - { - await _db.Pages.ExecuteUpdateAsync(x => x.SetProperty(p => p.TreeLayoutId, (Guid?)null), token); - await _db.TreeLayouts.ExecuteDeleteAsync(token); + private readonly AppDbContext _db; + private readonly INodeJSService _js; + private readonly DynamicConfig _config; + private readonly ILogger _logger; - var opts = new RelationContextOptions { PeopleOnly = true, TreeRelationsOnly = true }; - var ctx = await RelationContext.LoadContextAsync(_db, opts); + protected override async Task ExecuteAsync(CancellationToken token) + { + await _db.Pages.ExecuteUpdateAsync(x => x.SetProperty(p => p.TreeLayoutId, (Guid?)null), token); + await _db.TreeLayouts.ExecuteDeleteAsync(token); - await ProcessPartialTreeAsync(TreeKind.CloseFamily, GetCloseFamilyTree, ctx, token); - await ProcessPartialTreeAsync(TreeKind.Ancestors, GetAncestorsTree, ctx, token); - await ProcessPartialTreeAsync(TreeKind.Descendants, GetDescendantsTree, ctx, token); + var opts = new RelationContextOptions { PeopleOnly = true, TreeRelationsOnly = true }; + var ctx = await RelationContext.LoadContextAsync(_db, opts); - await ProcessFullTreeAsync(ctx, token); - } + await ProcessPartialTreeAsync(TreeKind.CloseFamily, GetCloseFamilyTree, ctx, token); + await ProcessPartialTreeAsync(TreeKind.Ancestors, GetAncestorsTree, ctx, token); + await ProcessPartialTreeAsync(TreeKind.Descendants, GetDescendantsTree, ctx, token); - /// - /// Renders the tree using ELK.js. - /// - protected async Task RenderTreeAsync(TreeLayoutVM tree, int thoroughness, CancellationToken token) - { - var json = JsonConvert.SerializeObject(tree); - var result = await _js.InvokeFromFileAsync( - "./External/tree/tree-layout.js", - args: [json, thoroughness], - cancellationToken: token - ); + await ProcessFullTreeAsync(ctx, token); + } - if (string.IsNullOrEmpty(result) || result == "null") - throw new Exception("Failed to render tree: output is empty."); + /// + /// Renders the tree using ELK.js. + /// + protected async Task RenderTreeAsync(TreeLayoutVM tree, int thoroughness, CancellationToken token) + { + var json = JsonConvert.SerializeObject(tree); + var result = await _js.InvokeFromFileAsync( + "./External/tree/tree-layout.js", + args: [json, thoroughness], + cancellationToken: token + ); - return result; - } + if (string.IsNullOrEmpty(result) || result == "null") + throw new Exception("Failed to render tree: output is empty."); - /// - /// Returns the photo for a card, depending on the gender, reverting to a default one if unspecified. - /// - protected string GetPhoto(string actual, bool gender) - { - var defaultPhoto = gender - ? "~/assets/img/unknown-male.png" - : "~/assets/img/unknown-female.png"; + return result; + } - return StringHelper.Coalesce( - MediaPresenterService.GetSizedMediaPath(actual, MediaSize.Small), - defaultPhoto - ); - } + /// + /// Returns the photo for a card, depending on the gender, reverting to a default one if unspecified. + /// + protected string GetPhoto(string actual, bool gender) + { + var defaultPhoto = gender + ? "~/assets/img/unknown-male.png" + : "~/assets/img/unknown-female.png"; + + return StringHelper.Coalesce( + MediaPresenterService.GetSizedMediaPath(actual, MediaSize.Small), + defaultPhoto + ); + } - /// - /// Returns a subtree around a page using the relation filter. - /// - protected TreeLayoutVM GetSubtree(RelationContext context, Guid rootId, Func relFilter, TraverseMode initialMode = TraverseMode.Normal) - { - var danglingParents = new HashSet(); - var parentKeys = new HashSet(); + /// + /// Returns a subtree around a page using the relation filter. + /// + protected TreeLayoutVM GetSubtree(RelationContext context, Guid rootId, Func relFilter, TraverseMode initialMode = TraverseMode.Normal) + { + var danglingParents = new HashSet(); + var parentKeys = new HashSet(); - var persons = new Dictionary(); - var relations = new Dictionary(); + var persons = new Dictionary(); + var relations = new Dictionary(); - var pending = new Queue(); - pending.Enqueue(new Step(rootId, initialMode, 0, null)); + var pending = new Queue(); + pending.Enqueue(new Step(rootId, initialMode, 0, null)); - while (pending.TryDequeue(out var step)) - { - if (persons.ContainsKey(step.PageId)) - continue; + while (pending.TryDequeue(out var step)) + { + if (persons.ContainsKey(step.PageId)) + continue; - if (!context.Pages.TryGetValue(step.PageId, out var page)) - continue; + if (!context.Pages.TryGetValue(step.PageId, out var page)) + continue; - AddPage(page, step.Mode); + AddPage(page, step.Mode); - if (step.Mode.HasFlag(TraverseMode.TraverseRelations) && context.Relations.TryGetValue(step.PageId, out var rels)) + if (step.Mode.HasFlag(TraverseMode.TraverseRelations) && context.Relations.TryGetValue(step.PageId, out var rels)) + { + foreach (var rel in rels) { - foreach (var rel in rels) - { - var relMode = relFilter(new RelationFilterContext(step.PageId, step.Distance + 1, rel.Type, step.LastRelationType)); - if(relMode == null) - continue; + var relMode = relFilter(new RelationFilterContext(step.PageId, step.Distance + 1, rel.Type, step.LastRelationType)); + if(relMode == null) + continue; - pending.Enqueue(new Step(rel.DestinationId, relMode.Value, step.Distance + 1, rel.Type)); + pending.Enqueue(new Step(rel.DestinationId, relMode.Value, step.Distance + 1, rel.Type)); - if (rel.Type == RelationType.Spouse) - AddRelationship(page.Id, rel.DestinationId); - } + if (rel.Type == RelationType.Spouse) + AddRelationship(page.Id, rel.DestinationId); } } + } - foreach (var parentId in danglingParents) - { - if (persons.ContainsKey(parentId)) - continue; + foreach (var parentId in danglingParents) + { + if (persons.ContainsKey(parentId)) + continue; - if (!context.Pages.TryGetValue(parentId, out var page)) - continue; + if (!context.Pages.TryGetValue(parentId, out var page)) + continue; - AddPage(page, TraverseMode.DeadEnd); - } + AddPage(page, TraverseMode.DeadEnd); + } - return new TreeLayoutVM - { - PageId = rootId, - Persons = persons.Values.OrderBy(x => x.Name).ToList(), - Relations = relations.Values.OrderBy(x => x.Id).ToList() - }; + return new TreeLayoutVM + { + PageId = rootId, + Persons = persons.Values.OrderBy(x => x.Name).ToList(), + Relations = relations.Values.OrderBy(x => x.Id).ToList() + }; - void AddPage(RelationContext.PageExcerpt page, TraverseMode mode) - { - persons.Add( - page.Id, - new TreePersonVM - { - Id = page.Id.ToString(), - Name = page.Title, - MaidenName = page.MaidenName, - Birth = page.BirthDate?.ShortReadableDate, - Death = page.DeathDate?.ShortReadableDate, - IsMale = page.Gender ?? true, - IsDead = page.IsDead, - Photo = GetPhoto(page.MainPhotoPath, page.Gender ?? true), - Url = page.Key, - Parents = mode.HasFlag(TraverseMode.SetParents) - ? GetParentRelationshipId(page) - : null - } - ); - } + void AddPage(RelationContext.PageExcerpt page, TraverseMode mode) + { + persons.Add( + page.Id, + new TreePersonVM + { + Id = page.Id.ToString(), + Name = page.Title, + MaidenName = page.MaidenName, + Birth = page.BirthDate?.ShortReadableDate, + Death = page.DeathDate?.ShortReadableDate, + IsMale = page.Gender ?? true, + IsDead = page.IsDead, + Photo = GetPhoto(page.MainPhotoPath, page.Gender ?? true), + Url = page.Key, + Parents = mode.HasFlag(TraverseMode.SetParents) + ? GetParentRelationshipId(page) + : null + } + ); + } - string GetParentRelationshipId(RelationContext.PageExcerpt page) - { - if (!context.Relations.TryGetValue(page.Id, out var allRels)) - return null; + string GetParentRelationshipId(RelationContext.PageExcerpt page) + { + if (!context.Relations.TryGetValue(page.Id, out var allRels)) + return null; - var rels = allRels.Where(x => x.Type == RelationType.Parent).ToList(); - if (rels.Count == 0) - return null; + var rels = allRels.Where(x => x.Type == RelationType.Parent).ToList(); + if (rels.Count == 0) + return null; - danglingParents.AddRange(rels.Select(x => x.DestinationId)); + danglingParents.AddRange(rels.Select(x => x.DestinationId)); - var relKey = rels.Count == 1 - ? rels[0].DestinationId + ":unknown" - : rels.Select(x => x.DestinationId.ToString()).OrderBy(x => x).JoinString(":"); + var relKey = rels.Count == 1 + ? rels[0].DestinationId + ":unknown" + : rels.Select(x => x.DestinationId.ToString()).OrderBy(x => x).JoinString(":"); - if (!parentKeys.Contains(relKey)) + if (!parentKeys.Contains(relKey)) + { + if (rels.Count == 1) { - if (rels.Count == 1) + var fakeId = Guid.NewGuid(); + var relPage = context.Pages[rels[0].DestinationId]; + var fakeGender = !(relPage.Gender ?? true); + persons.Add(fakeId, new TreePersonVM { - var fakeId = Guid.NewGuid(); - var relPage = context.Pages[rels[0].DestinationId]; - var fakeGender = !(relPage.Gender ?? true); - persons.Add(fakeId, new TreePersonVM - { - Id = fakeId.ToString(), - Name = Texts.Admin_Tree_Unknown, - IsMale = fakeGender, - Photo = GetPhoto(null, fakeGender) - }); - - AddRelationship(rels[0].DestinationId, fakeId, relKey); - } - else - { - AddRelationship(rels[0].DestinationId, rels[1].DestinationId); - } + Id = fakeId.ToString(), + Name = Texts.Admin_Tree_Unknown, + IsMale = fakeGender, + Photo = GetPhoto(null, fakeGender) + }); - parentKeys.Add(relKey); + AddRelationship(rels[0].DestinationId, fakeId, relKey); + } + else + { + AddRelationship(rels[0].DestinationId, rels[1].DestinationId); } - return relKey; + parentKeys.Add(relKey); } - void AddRelationship(Guid r1, Guid r2, string keyOverride = null) - { - var from = r1.ToString(); - var to = r2.ToString(); - if (from.CompareTo(to) >= 1) - (from, to) = (to, from); - - var key = StringHelper.Coalesce(keyOverride, from + ":" + to); - if (relations.ContainsKey(key)) - return; - - relations.Add(key, new TreeRelationVM - { - Id = key, - From = from, - To = to - }); - } + return relKey; } - [Flags] - protected enum TraverseMode + void AddRelationship(Guid r1, Guid r2, string keyOverride = null) { - DeadEnd = 0, - TraverseRelations = 1, - SetParents = 2, - Normal = 3 + var from = r1.ToString(); + var to = r2.ToString(); + if (from.CompareTo(to) >= 1) + (from, to) = (to, from); + + var key = StringHelper.Coalesce(keyOverride, from + ":" + to); + if (relations.ContainsKey(key)) + return; + + relations.Add(key, new TreeRelationVM + { + Id = key, + From = from, + To = to + }); } + } - protected record struct Step(Guid PageId, TraverseMode Mode, int Distance, RelationType? LastRelationType); - protected record struct RelationFilterContext(Guid PageId, int Distance, RelationType RelationType, RelationType? LastRelationType); + [Flags] + protected enum TraverseMode + { + DeadEnd = 0, + TraverseRelations = 1, + SetParents = 2, + Normal = 3 } -} + + protected record struct Step(Guid PageId, TraverseMode Mode, int Distance, RelationType? LastRelationType); + protected record struct RelationFilterContext(Guid PageId, int Distance, RelationType RelationType, RelationType? LastRelationType); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/UsersManagerService.cs b/src/Bonsai/Areas/Admin/Logic/UsersManagerService.cs index e4ed9b49..90403d67 100644 --- a/src/Bonsai/Areas/Admin/Logic/UsersManagerService.cs +++ b/src/Bonsai/Areas/Admin/Logic/UsersManagerService.cs @@ -18,346 +18,345 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Admin.Logic +namespace Bonsai.Areas.Admin.Logic; + +/// +/// Service for handling user accounts. +/// +public class UsersManagerService { + public UsersManagerService(AppDbContext db, UserManager userMgr, IMapper mapper, BonsaiConfigService config) + { + _db = db; + _userMgr = userMgr; + _mapper = mapper; + _demoCfg = config.GetStaticConfig().DemoMode; + } + + private readonly AppDbContext _db; + private readonly UserManager _userMgr; + private readonly IMapper _mapper; + private readonly DemoModeConfig _demoCfg; + /// - /// Service for handling user accounts. + /// Returns the list of all registered users. /// - public class UsersManagerService + public async Task GetUsersAsync(UsersListRequestVM request) { - public UsersManagerService(AppDbContext db, UserManager userMgr, IMapper mapper, BonsaiConfigService config) - { - _db = db; - _userMgr = userMgr; - _mapper = mapper; - _demoCfg = config.GetStaticConfig().DemoMode; - } + const int PageSize = 20; - private readonly AppDbContext _db; - private readonly UserManager _userMgr; - private readonly IMapper _mapper; - private readonly DemoModeConfig _demoCfg; + request = NormalizeListRequest(request); - /// - /// Returns the list of all registered users. - /// - public async Task GetUsersAsync(UsersListRequestVM request) + var query = await LoadUsersAsync(); + + if (!string.IsNullOrEmpty(request.SearchQuery)) { - const int PageSize = 20; - - request = NormalizeListRequest(request); - - var query = await LoadUsersAsync(); - - if (!string.IsNullOrEmpty(request.SearchQuery)) - { - var nq = request.SearchQuery.ToLower(); - query = query.Where(x => x.FullName.ToLower().Contains(nq) - || x.Email.ToLower().Contains(nq)); - } - - if (request.Roles?.Length > 0) - query = query.Where(x => request.Roles.Contains(x.Role)); - - var totalCount = query.Count(); - var items = query.OrderBy(request.OrderBy, request.OrderDescending.Value) - .Skip(PageSize * request.Page) - .Take(PageSize) - .ToList(); - - return new UsersListVM - { - Items = items, - PageCount = (int) Math.Ceiling((double) totalCount / PageSize), - Request = request - }; + var nq = request.SearchQuery.ToLower(); + query = query.Where(x => x.FullName.ToLower().Contains(nq) + || x.Email.ToLower().Contains(nq)); } - /// - /// Finds the user by ID. - /// - public async Task GetAsync(string id) - { - var user = await _db.Users - .AsNoTracking() - .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); + if (request.Roles?.Length > 0) + query = query.Where(x => request.Roles.Contains(x.Role)); - return _mapper.Map(user); - } + var totalCount = query.Count(); + var items = query.OrderBy(request.OrderBy, request.OrderDescending.Value) + .Skip(PageSize * request.Page) + .Take(PageSize) + .ToList(); - /// - /// Retrieves the default values for an update operation. - /// - public async Task RequestUpdateAsync(string id) + return new UsersListVM { - var user = await _db.Users - .AsNoTracking() - .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - - ValidateDemoModeRestrictions(user); + Items = items, + PageCount = (int) Math.Ceiling((double) totalCount / PageSize), + Request = request + }; + } - var vm = _mapper.Map(user); + /// + /// Finds the user by ID. + /// + public async Task GetAsync(string id) + { + var user = await _db.Users + .AsNoTracking() + .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - var roles = await _userMgr.GetRolesAsync(user); - if (roles.Count > 0 && Enum.TryParse(roles.First(), out var role)) - vm.Role = role; + return _mapper.Map(user); + } - return vm; - } + /// + /// Retrieves the default values for an update operation. + /// + public async Task RequestUpdateAsync(string id) + { + var user = await _db.Users + .AsNoTracking() + .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - /// - /// Updates the user. - /// - public async Task UpdateAsync(UserEditorVM request, ClaimsPrincipal currUser) - { - await ValidateUpdateRequestAsync(request); + ValidateDemoModeRestrictions(user); - var user = await _db.Users - .GetAsync(x => x.Id == request.Id, Texts.Admin_Users_NotFound); + var vm = _mapper.Map(user); - ValidateDemoModeRestrictions(user); + var roles = await _userMgr.GetRolesAsync(user); + if (roles.Count > 0 && Enum.TryParse(roles.First(), out var role)) + vm.Role = role; - _mapper.Map(request, user); - user.IsValidated = true; + return vm; + } - if(!IsSelf(request.Id, currUser)) - { - var allRoles = await _userMgr.GetRolesAsync(user); - await _userMgr.RemoveFromRolesAsync(user, allRoles); + /// + /// Updates the user. + /// + public async Task UpdateAsync(UserEditorVM request, ClaimsPrincipal currUser) + { + await ValidateUpdateRequestAsync(request); - var role = request.Role.ToString(); - await _userMgr.AddToRoleAsync(user, role); - } + var user = await _db.Users + .GetAsync(x => x.Id == request.Id, Texts.Admin_Users_NotFound); - if(request.IsLocked && user.LockoutEnd == null) - user.LockoutEnd = DateTimeOffset.MaxValue; - else if (!request.IsLocked && user.LockoutEnd != null) - user.LockoutEnd = null; + ValidateDemoModeRestrictions(user); - return user; - } + _mapper.Map(request, user); + user.IsValidated = true; - /// - /// Retrieves the information about user removal. - /// - public async Task RequestRemoveAsync(string id, ClaimsPrincipal principal) + if(!IsSelf(request.Id, currUser)) { - var user = await _db.Users - .AsNoTracking() - .Include(x => x.Changes) - .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - - ValidateDemoModeRestrictions(user); - - return new RemoveUserVM - { - Id = id, - FullName = user.FirstName + " " + user.LastName, - IsSelf = id == _userMgr.GetUserId(principal), - IsFullyDeletable = !user.Changes.Any() - }; + var allRoles = await _userMgr.GetRolesAsync(user); + await _userMgr.RemoveFromRolesAsync(user, allRoles); + + var role = request.Role.ToString(); + await _userMgr.AddToRoleAsync(user, role); } - /// - /// Removes the user account. - /// - public async Task RemoveAsync(string id, ClaimsPrincipal currUser) - { - if(IsSelf(id, currUser)) - throw new OperationException(Texts.Admin_Users_CannotRemoveSelfMessage); + if(request.IsLocked && user.LockoutEnd == null) + user.LockoutEnd = DateTimeOffset.MaxValue; + else if (!request.IsLocked && user.LockoutEnd != null) + user.LockoutEnd = null; - var user = await _db.Users - .Include(x => x.Changes) - .Include(x => x.Page) - .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); + return user; + } - ValidateDemoModeRestrictions(user); + /// + /// Retrieves the information about user removal. + /// + public async Task RequestRemoveAsync(string id, ClaimsPrincipal principal) + { + var user = await _db.Users + .AsNoTracking() + .Include(x => x.Changes) + .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - if (user.Changes.Any()) - throw new OperationException(Texts.Admin_Users_CannotRemoveMessage); + ValidateDemoModeRestrictions(user); - var result = await _userMgr.DeleteAsync(user); - if(!result.Succeeded) - throw new OperationException(Texts.Admin_Users_RemoveFailedMessage); - } - - /// - /// Checks if the specified ID belongs to current user. - /// - public bool IsSelf(string id, ClaimsPrincipal principal) + return new RemoveUserVM { - return principal != null - && _userMgr.GetUserId(principal) == id; - } + Id = id, + FullName = user.FirstName + " " + user.LastName, + IsSelf = id == _userMgr.GetUserId(principal), + IsFullyDeletable = !user.Changes.Any() + }; + } - /// - /// Checks if the personal page can be created for this user. - /// - public async Task CanCreatePersonalPageAsync(UserEditorVM vm) - { - return await _db.Users - .Where(x => x.Id == vm.Id && x.Page == null) - .AnyAsync(); - } + /// + /// Removes the user account. + /// + public async Task RemoveAsync(string id, ClaimsPrincipal currUser) + { + if(IsSelf(id, currUser)) + throw new OperationException(Texts.Admin_Users_CannotRemoveSelfMessage); - /// - /// Resets the user's password. - /// - public async Task ResetPasswordAsync(UserPasswordEditorVM vm) - { - ValidatePasswordForm(vm); + var user = await _db.Users + .Include(x => x.Changes) + .Include(x => x.Page) + .GetAsync(x => x.Id == id, Texts.Admin_Users_NotFound); - var user = await _db.Users.GetAsync(x => x.Id == vm.Id, Texts.Admin_Users_NotFound); + ValidateDemoModeRestrictions(user); - ValidateDemoModeRestrictions(user); + if (user.Changes.Any()) + throw new OperationException(Texts.Admin_Users_CannotRemoveMessage); - var token = await _userMgr.GeneratePasswordResetTokenAsync(user); - var result = await _userMgr.ResetPasswordAsync(user, token, vm.Password); + var result = await _userMgr.DeleteAsync(user); + if(!result.Succeeded) + throw new OperationException(Texts.Admin_Users_RemoveFailedMessage); + } - if(!result.Succeeded) - throw new OperationException(Texts.Admin_Users_PasswordChangeFailedMessage); + /// + /// Checks if the specified ID belongs to current user. + /// + public bool IsSelf(string id, ClaimsPrincipal principal) + { + return principal != null + && _userMgr.GetUserId(principal) == id; + } - await _userMgr.SetLockoutEndDateAsync(user, null); - } + /// + /// Checks if the personal page can be created for this user. + /// + public async Task CanCreatePersonalPageAsync(UserEditorVM vm) + { + return await _db.Users + .Where(x => x.Id == vm.Id && x.Page == null) + .AnyAsync(); + } - /// - /// Creates a new user with login-password auth. - /// - public async Task CreateAsync(UserCreatorVM vm) - { - ValidatePasswordForm(vm); - await ValidateRegisterRequestAsync(vm); + /// + /// Resets the user's password. + /// + public async Task ResetPasswordAsync(UserPasswordEditorVM vm) + { + ValidatePasswordForm(vm); - var user = _mapper.Map(vm); - user.Id = Guid.NewGuid().ToString(); - user.AuthType = AuthType.Password; - user.IsValidated = true; + var user = await _db.Users.GetAsync(x => x.Id == vm.Id, Texts.Admin_Users_NotFound); - var createResult = await _userMgr.CreateAsync(user, vm.Password); - if (!createResult.Succeeded) - { - var msgs = createResult.Errors.Select(x => new KeyValuePair("", x.Description)).ToList(); - throw new ValidationException(msgs); - } + ValidateDemoModeRestrictions(user); - await _userMgr.AddToRoleAsync(user, vm.Role.ToString()); + var token = await _userMgr.GeneratePasswordResetTokenAsync(user); + var result = await _userMgr.ResetPasswordAsync(user, token, vm.Password); - return user; - } + if(!result.Succeeded) + throw new OperationException(Texts.Admin_Users_PasswordChangeFailedMessage); - #region Private helpers + await _userMgr.SetLockoutEndDateAsync(user, null); + } - /// - /// Returns the request with default/valid values. - /// - private UsersListRequestVM NormalizeListRequest(UsersListRequestVM vm) + /// + /// Creates a new user with login-password auth. + /// + public async Task CreateAsync(UserCreatorVM vm) + { + ValidatePasswordForm(vm); + await ValidateRegisterRequestAsync(vm); + + var user = _mapper.Map(vm); + user.Id = Guid.NewGuid().ToString(); + user.AuthType = AuthType.Password; + user.IsValidated = true; + + var createResult = await _userMgr.CreateAsync(user, vm.Password); + if (!createResult.Succeeded) { - if (vm == null) - vm = new UsersListRequestVM(); + var msgs = createResult.Errors.Select(x => new KeyValuePair("", x.Description)).ToList(); + throw new ValidationException(msgs); + } - var orderableFields = new[] {nameof(UserTitleVM.FullName), nameof(UserTitleVM.Email)}; - if (!orderableFields.Contains(vm.OrderBy)) - vm.OrderBy = orderableFields[0]; + await _userMgr.AddToRoleAsync(user, vm.Role.ToString()); - if (vm.Page < 0) - vm.Page = 0; + return user; + } - if (vm.OrderDescending == null) - vm.OrderDescending = false; + #region Private helpers - return vm; - } + /// + /// Returns the request with default/valid values. + /// + private UsersListRequestVM NormalizeListRequest(UsersListRequestVM vm) + { + if (vm == null) + vm = new UsersListRequestVM(); - /// - /// Loads users from the database. - /// - private async Task> LoadUsersAsync() - { - var roles = await _db.Roles - .ToDictionaryAsync(x => x.Id, x => Enum.Parse(x.Name)); + var orderableFields = new[] {nameof(UserTitleVM.FullName), nameof(UserTitleVM.Email)}; + if (!orderableFields.Contains(vm.OrderBy)) + vm.OrderBy = orderableFields[0]; - var userBindings = await _db.UserRoles - .ToDictionaryAsync(x => x.UserId, x => x.RoleId); + if (vm.Page < 0) + vm.Page = 0; - var users = await _db.Users - .ProjectToType(_mapper.Config) - .ToListAsync(); + if (vm.OrderDescending == null) + vm.OrderDescending = false; - foreach (var user in users) - user.Role = roles[userBindings[user.Id]]; + return vm; + } - return users.AsQueryable(); - } + /// + /// Loads users from the database. + /// + private async Task> LoadUsersAsync() + { + var roles = await _db.Roles + .ToDictionaryAsync(x => x.Id, x => Enum.Parse(x.Name)); - /// - /// Performs additional checks on the request. - /// - private async Task ValidateUpdateRequestAsync(UserEditorVM request) - { - var val = new Validator(); + var userBindings = await _db.UserRoles + .ToDictionaryAsync(x => x.UserId, x => x.RoleId); - var emailUsed = await _db.Users - .AnyAsync(x => x.Id != request.Id && x.Email == request.Email); + var users = await _db.Users + .ProjectToType(_mapper.Config) + .ToListAsync(); - if (emailUsed) - val.Add(nameof(request.Email), Texts.Admin_Validation_User_EmailExists); + foreach (var user in users) + user.Role = roles[userBindings[user.Id]]; - if (request.PersonalPageId != null) - { - var exists = await _db.Pages - .AnyAsync(x => x.Id == request.PersonalPageId); - if (!exists) - val.Add(nameof(request.PersonalPageId), Texts.Admin_Pages_NotFound); - } + return users.AsQueryable(); + } - val.ThrowIfInvalid(); - } + /// + /// Performs additional checks on the request. + /// + private async Task ValidateUpdateRequestAsync(UserEditorVM request) + { + var val = new Validator(); + + var emailUsed = await _db.Users + .AnyAsync(x => x.Id != request.Id && x.Email == request.Email); - /// - /// Ensures that the password form has been filled correctly. - /// - private void ValidatePasswordForm(IPasswordForm form) + if (emailUsed) + val.Add(nameof(request.Email), Texts.Admin_Validation_User_EmailExists); + + if (request.PersonalPageId != null) { - var val = new Validator(); + var exists = await _db.Pages + .AnyAsync(x => x.Id == request.PersonalPageId); + if (!exists) + val.Add(nameof(request.PersonalPageId), Texts.Admin_Pages_NotFound); + } - if (form.Password == null || form.Password.Length < 6) - val.Add(nameof(form.Password), Texts.Admin_Validation_User_PasswordTooShort); + val.ThrowIfInvalid(); + } - if (form.Password != form.PasswordCopy) - val.Add(nameof(form.PasswordCopy), Texts.Admin_Validation_User_PasswordDoesNotMatch); + /// + /// Ensures that the password form has been filled correctly. + /// + private void ValidatePasswordForm(IPasswordForm form) + { + var val = new Validator(); - val.ThrowIfInvalid(); - } + if (form.Password == null || form.Password.Length < 6) + val.Add(nameof(form.Password), Texts.Admin_Validation_User_PasswordTooShort); - /// - /// Performs additional checks on the registration request. - /// - private async Task ValidateRegisterRequestAsync(UserCreatorVM vm) - { - var val = new Validator(); + if (form.Password != form.PasswordCopy) + val.Add(nameof(form.PasswordCopy), Texts.Admin_Validation_User_PasswordDoesNotMatch); - if (FuzzyDate.TryParse(vm.Birthday) == null) - val.Add(nameof(vm.Birthday), Texts.Admin_Validation_User_InvalidBirthday); + val.ThrowIfInvalid(); + } - var emailExists = await _db.Users.AnyAsync(x => x.Email == vm.Email); - if (emailExists) - val.Add(nameof(vm.Email), Texts.AuthService_Error_EmailAlreadyExists); + /// + /// Performs additional checks on the registration request. + /// + private async Task ValidateRegisterRequestAsync(UserCreatorVM vm) + { + var val = new Validator(); - val.ThrowIfInvalid(); - } + if (FuzzyDate.TryParse(vm.Birthday) == null) + val.Add(nameof(vm.Birthday), Texts.Admin_Validation_User_InvalidBirthday); - /// - /// Checks if the user can be modified depending on current demo mode. - /// - private void ValidateDemoModeRestrictions(AppUser user) - { - if (!_demoCfg.Enabled) - return; + var emailExists = await _db.Users.AnyAsync(x => x.Email == vm.Email); + if (emailExists) + val.Add(nameof(vm.Email), Texts.AuthService_Error_EmailAlreadyExists); - if(user.Email == "admin@example.com" && _demoCfg.CreateDefaultAdmin) - throw new OperationException(Texts.Admin_ForbiddenInDemoMessage); - } + val.ThrowIfInvalid(); + } + + /// + /// Checks if the user can be modified depending on current demo mode. + /// + private void ValidateDemoModeRestrictions(AppUser user) + { + if (!_demoCfg.Enabled) + return; - #endregion + if(user.Email == "admin@example.com" && _demoCfg.CreateDefaultAdmin) + throw new OperationException(Texts.Admin_ForbiddenInDemoMessage); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Validation/ConsistencyErrorInfo.cs b/src/Bonsai/Areas/Admin/Logic/Validation/ConsistencyErrorInfo.cs index daa7778a..510a8657 100644 --- a/src/Bonsai/Areas/Admin/Logic/Validation/ConsistencyErrorInfo.cs +++ b/src/Bonsai/Areas/Admin/Logic/Validation/ConsistencyErrorInfo.cs @@ -1,26 +1,10 @@ using System; -namespace Bonsai.Areas.Admin.Logic.Validation -{ - /// - /// Information about contradictory facts. - /// - public class ConsistencyErrorInfo - { - public ConsistencyErrorInfo(string msg, params Guid[] pageIds) - { - Message = msg; - PageIds = pageIds; - } +namespace Bonsai.Areas.Admin.Logic.Validation; - /// - /// Detailed information about the inconsistency. - /// - public string Message { get; } - - /// - /// Related pages. - /// - public Guid[] PageIds { get; } - } -} +/// +/// Information about contradictory facts. +/// Detailed information about the inconsistency. +/// Related pages. +/// +public record ConsistencyErrorInfo(string Message, params Guid[] PageIds); \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Validation/PageValidator.cs b/src/Bonsai/Areas/Admin/Logic/Validation/PageValidator.cs index 27543b38..ed44a059 100644 --- a/src/Bonsai/Areas/Admin/Logic/Validation/PageValidator.cs +++ b/src/Bonsai/Areas/Admin/Logic/Validation/PageValidator.cs @@ -14,112 +14,111 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Admin.Logic.Validation +namespace Bonsai.Areas.Admin.Logic.Validation; + +/// +/// The validator that checks relation/fact consistency. +/// +public class PageValidator { - /// - /// The validator that checks relation/fact consistency. - /// - public class PageValidator + public PageValidator(AppDbContext db) { - public PageValidator(AppDbContext db) - { - _db = db; - } + _db = db; + } - private readonly AppDbContext _db; + private readonly AppDbContext _db; - /// - /// Checks the current relations and prepares them for database serialization. - /// - public async Task ValidateAsync(Page page, string rawFacts) - { - var context = await RelationContext.LoadContextAsync(_db); - AugmentRelationContext(context, page, rawFacts); + /// + /// Checks the current relations and prepares them for database serialization. + /// + public async Task ValidateAsync(Page page, string rawFacts) + { + var context = await RelationContext.LoadContextAsync(_db); + AugmentRelationContext(context, page, rawFacts); - var core = new ValidatorCore(); - core.Validate(context, new [] { page.Id }); - core.ThrowIfInvalid(context, nameof(PageEditorVM.Facts)); - } + var core = new ValidatorCore(); + core.Validate(context, [page.Id]); + core.ThrowIfInvalid(context, nameof(PageEditorVM.Facts)); + } - #region Helpers + #region Helpers - /// - /// Adds information from the current page to the context. - /// - private void AugmentRelationContext(RelationContext context, Page page, string rawFacts) + /// + /// Adds information from the current page to the context. + /// + private void AugmentRelationContext(RelationContext context, Page page, string rawFacts) + { + var facts = ParseFacts(page.Type, rawFacts); + var excerpt = new RelationContext.PageExcerpt { - var facts = ParseFacts(page.Type, rawFacts); - var excerpt = new RelationContext.PageExcerpt - { - Id = page.Id, - Key = page.Key, - Type = page.Type, - Title = page.Title, - Gender = Parse("Bio.Gender", "IsMale"), - BirthDate = Parse("Birth.Date", "Value"), - DeathDate = Parse("Death.Date", "Value"), - }; - - context.Augment(excerpt); - - T? Parse(params string[] parts) where T : struct - { - var curr = (JToken) facts; + Id = page.Id, + Key = page.Key, + Type = page.Type, + Title = page.Title, + Gender = Parse("Bio.Gender", "IsMale"), + BirthDate = Parse("Birth.Date", "Value"), + DeathDate = Parse("Death.Date", "Value"), + }; + + context.Augment(excerpt); + + T? Parse(params string[] parts) where T : struct + { + var curr = (JToken) facts; - foreach(var part in parts) - curr = curr?[part]; + foreach(var part in parts) + curr = curr?[part]; - var value = curr?.ToString(); + var value = curr?.ToString(); - if(typeof(T) == typeof(FuzzyDate)) - return (T?)(object)FuzzyDate.TryParse(value); + if(typeof(T) == typeof(FuzzyDate)) + return (T?)(object)FuzzyDate.TryParse(value); - return value.TryParse(); - } + return value.TryParse(); } + } + + /// + /// Deserializes the fact data. + /// + private JObject ParseFacts(PageType type, string rawFacts) + { + if (string.IsNullOrEmpty(rawFacts)) + return new JObject(); - /// - /// Deserializes the fact data. - /// - private JObject ParseFacts(PageType type, string rawFacts) + var pageFacts = ParseRaw(rawFacts); + foreach (var prop in pageFacts) { - if (string.IsNullOrEmpty(rawFacts)) - return new JObject(); + var def = FactDefinitions.TryGetDefinition(type, prop.Key); + if (def == null) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Validation_Page_UnknownFact, prop.Key)); - var pageFacts = ParseRaw(rawFacts); - foreach (var prop in pageFacts) + try { - var def = FactDefinitions.TryGetDefinition(type, prop.Key); - if (def == null) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Validation_Page_UnknownFact, prop.Key)); - - try - { - var model = JsonConvert.DeserializeObject(prop.Value.ToString(), def.Kind) as FactModelBase; - model.Validate(); - } - catch (Exception ex) when (!(ex is ValidationException)) - { - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Validation_Page_IncorrectFact, prop.Key)); - } + var model = JsonConvert.DeserializeObject(prop.Value.ToString(), def.Kind) as FactModelBase; + model.Validate(); } + catch (Exception ex) when (!(ex is ValidationException)) + { + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Validation_Page_IncorrectFact, prop.Key)); + } + } - return pageFacts; + return pageFacts; - JObject ParseRaw(string raw) + JObject ParseRaw(string raw) + { + try { - try - { - var json = JObject.Parse(raw); - return JsonHelper.RemoveEmptyChildren(json); - } - catch - { - throw new ValidationException(nameof(Page.Facts), Texts.Admin_Validation_Page_BadFactsFormat); - } + var json = JObject.Parse(raw); + return JsonHelper.RemoveEmptyChildren(json); + } + catch + { + throw new ValidationException(nameof(Page.Facts), Texts.Admin_Validation_Page_BadFactsFormat); } } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Validation/RelationValidator.cs b/src/Bonsai/Areas/Admin/Logic/Validation/RelationValidator.cs index d5a5bc83..32b077eb 100644 --- a/src/Bonsai/Areas/Admin/Logic/Validation/RelationValidator.cs +++ b/src/Bonsai/Areas/Admin/Logic/Validation/RelationValidator.cs @@ -8,53 +8,52 @@ using Bonsai.Data; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.Logic.Validation +namespace Bonsai.Areas.Admin.Logic.Validation; + +/// +/// Validates added relations. +/// +public class RelationValidator { - /// - /// Validates added relations. - /// - public class RelationValidator + public RelationValidator(AppDbContext db) { - public RelationValidator(AppDbContext db) - { - _db = db; - } + _db = db; + } - private readonly AppDbContext _db; + private readonly AppDbContext _db; - /// - /// Validates the context integrity. - /// - public async Task ValidateAsync(IReadOnlyList relations) - { - var firstRel = relations?.FirstOrDefault(); - if(firstRel == null) - throw new ArgumentNullException(); + /// + /// Validates the context integrity. + /// + public async Task ValidateAsync(IReadOnlyList relations) + { + var firstRel = relations?.FirstOrDefault(); + if(firstRel == null) + throw new ArgumentNullException(); - var context = await RelationContext.LoadContextAsync(_db); - foreach(var rel in relations) - context.Augment(CreateExcerpt(rel)); + var context = await RelationContext.LoadContextAsync(_db); + foreach(var rel in relations) + context.Augment(CreateExcerpt(rel)); - var core = new ValidatorCore(); - core.Validate(context, new [] { firstRel.SourceId, firstRel.DestinationId, firstRel.EventId ?? Guid.Empty }); - core.ThrowIfInvalid(context, nameof(RelationEditorVM.DestinationId)); - } + var core = new ValidatorCore(); + core.Validate(context, [firstRel.SourceId, firstRel.DestinationId, firstRel.EventId ?? Guid.Empty]); + core.ThrowIfInvalid(context, nameof(RelationEditorVM.DestinationId)); + } - /// - /// Maps the relation to an excerpt. - /// - private RelationContext.RelationExcerpt CreateExcerpt(Relation rel) + /// + /// Maps the relation to an excerpt. + /// + private RelationContext.RelationExcerpt CreateExcerpt(Relation rel) + { + return new RelationContext.RelationExcerpt { - return new RelationContext.RelationExcerpt - { - Id = rel.Id, - SourceId = rel.SourceId, - DestinationId = rel.DestinationId, - Duration = FuzzyRange.TryParse(rel.Duration), - EventId = rel.EventId, - IsComplementary = rel.IsComplementary, - Type = rel.Type - }; - } + Id = rel.Id, + SourceId = rel.SourceId, + DestinationId = rel.DestinationId, + Duration = FuzzyRange.TryParse(rel.Duration), + EventId = rel.EventId, + IsComplementary = rel.IsComplementary, + Type = rel.Type + }; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Logic/Validation/ValidatorCore.cs b/src/Bonsai/Areas/Admin/Logic/Validation/ValidatorCore.cs index e65d399a..6c291b10 100644 --- a/src/Bonsai/Areas/Admin/Logic/Validation/ValidatorCore.cs +++ b/src/Bonsai/Areas/Admin/Logic/Validation/ValidatorCore.cs @@ -7,202 +7,201 @@ using Bonsai.Data.Models; using Bonsai.Localization; -namespace Bonsai.Areas.Admin.Logic.Validation +namespace Bonsai.Areas.Admin.Logic.Validation; + +/// +/// The class with consistency validation logic. +/// +public class ValidatorCore { - /// - /// The class with consistency validation logic. - /// - public class ValidatorCore + public ValidatorCore() { - public ValidatorCore() - { - _errors = new List(); - } + _errors = new List(); + } - private readonly List _errors; + private readonly List _errors; - /// - /// The list of found consistency violations. - /// - public IReadOnlyList Errors => _errors; + /// + /// The list of found consistency violations. + /// + public IReadOnlyList Errors => _errors; - /// - /// Checks the context for contradictory facts. - /// - public void Validate(RelationContext context, Guid[] updatedPageIds = null) - { - CheckLifespans(context); - CheckWeddings(context); - CheckParents(context); - CheckParentLifespans(context); - - if(updatedPageIds != null) - foreach(var pageId in updatedPageIds) - if(pageId != Guid.Empty) - CheckLoops(context, pageId); - } + /// + /// Checks the context for contradictory facts. + /// + public void Validate(RelationContext context, Guid[] updatedPageIds = null) + { + CheckLifespans(context); + CheckWeddings(context); + CheckParents(context); + CheckParentLifespans(context); + + if(updatedPageIds != null) + foreach(var pageId in updatedPageIds) + if(pageId != Guid.Empty) + CheckLoops(context, pageId); + } - /// - /// Throws a ValidationException if there any violations found. - /// - public void ThrowIfInvalid(RelationContext context, string propName) - { - if (!Errors.Any()) - return; + /// + /// Throws a ValidationException if there any violations found. + /// + public void ThrowIfInvalid(RelationContext context, string propName) + { + if (!Errors.Any()) + return; - var msgs = new List>(); - foreach (var err in Errors) + var msgs = new List>(); + foreach (var err in Errors) + { + var msg = err.Message; + if (err.PageIds?.Length > 0) { - var msg = err.Message; - if (err.PageIds?.Length > 0) - { - var pages = err.PageIds.Select(x => context.Pages[x].Title); - msg += $" ({string.Join(", ", pages)})"; - } - - msgs.Add(new KeyValuePair(propName, msg)); + var pages = err.PageIds.Select(x => context.Pages[x].Title); + msg += $" ({string.Join(", ", pages)})"; } - throw new ValidationException(msgs); + msgs.Add(new KeyValuePair(propName, msg)); } - #region Checks + throw new ValidationException(msgs); + } + + #region Checks - /// - /// Checks the context for inconsistent wedding information. - /// - private void CheckWeddings(RelationContext context) + /// + /// Checks the context for inconsistent wedding information. + /// + private void CheckWeddings(RelationContext context) + { + foreach (var rel in context.Relations.Values.SelectMany(x => x)) { - foreach (var rel in context.Relations.Values.SelectMany(x => x)) - { - if (rel.Type != RelationType.Spouse || rel.IsComplementary) - continue; + if (rel.Type != RelationType.Spouse || rel.IsComplementary) + continue; - var first = context.Pages[rel.SourceId]; - var second = context.Pages[rel.DestinationId]; + var first = context.Pages[rel.SourceId]; + var second = context.Pages[rel.DestinationId]; - if (first.BirthDate >= second.DeathDate) - Error(string.Format(Texts.Admin_Validation_Page_SpouseLifetimesNoOverlap, first.BirthDate.Value.ReadableDate, second.DeathDate.Value.ReadableDate), first.Id, second.Id); + if (first.BirthDate >= second.DeathDate) + Error(string.Format(Texts.Admin_Validation_Page_SpouseLifetimesNoOverlap, first.BirthDate.Value.ReadableDate, second.DeathDate.Value.ReadableDate), first.Id, second.Id); - if (second.BirthDate >= first.DeathDate) - Error(string.Format(Texts.Admin_Validation_Page_SpouseLifetimesNoOverlap, second.BirthDate.Value.ReadableDate, first.DeathDate.Value.ReadableDate), first.Id, second.Id); + if (second.BirthDate >= first.DeathDate) + Error(string.Format(Texts.Admin_Validation_Page_SpouseLifetimesNoOverlap, second.BirthDate.Value.ReadableDate, first.DeathDate.Value.ReadableDate), first.Id, second.Id); - if (rel.Duration is FuzzyRange dur) - { - if(dur.RangeStart < first.BirthDate || dur.RangeEnd > first.DeathDate) - Error(string.Format(Texts.Admin_Validation_Page_MarriageExceedsLifetime, new FuzzyRange(first.BirthDate, first.DeathDate).ReadableRange), first.Id); + if (rel.Duration is FuzzyRange dur) + { + if(dur.RangeStart < first.BirthDate || dur.RangeEnd > first.DeathDate) + Error(string.Format(Texts.Admin_Validation_Page_MarriageExceedsLifetime, new FuzzyRange(first.BirthDate, first.DeathDate).ReadableRange), first.Id); - if(dur.RangeStart < second.BirthDate || dur.RangeEnd > second.DeathDate) - Error(string.Format(Texts.Admin_Validation_Page_MarriageExceedsLifetime, new FuzzyRange(second.BirthDate, second.DeathDate).ReadableRange), second.Id); - } + if(dur.RangeStart < second.BirthDate || dur.RangeEnd > second.DeathDate) + Error(string.Format(Texts.Admin_Validation_Page_MarriageExceedsLifetime, new FuzzyRange(second.BirthDate, second.DeathDate).ReadableRange), second.Id); } } + } - /// - /// Checks the lifespans consistency of each person/pet page. - /// - private void CheckLifespans(RelationContext context) + /// + /// Checks the lifespans consistency of each person/pet page. + /// + private void CheckLifespans(RelationContext context) + { + foreach (var page in context.Pages.Values) { - foreach (var page in context.Pages.Values) - { - if(page.BirthDate > page.DeathDate) - Error(Texts.Admin_Validation_Page_BirthAfterDeath, page.Id); - } + if(page.BirthDate > page.DeathDate) + Error(Texts.Admin_Validation_Page_BirthAfterDeath, page.Id); } + } - /// - /// Checks the context for inconsistencies with lifespans of parents/children. - /// - private void CheckParentLifespans(RelationContext context) + /// + /// Checks the context for inconsistencies with lifespans of parents/children. + /// + private void CheckParentLifespans(RelationContext context) + { + foreach (var rel in context.Relations.Values.SelectMany(x => x)) { - foreach (var rel in context.Relations.Values.SelectMany(x => x)) - { - if (rel.Type != RelationType.Child) - continue; + if (rel.Type != RelationType.Child) + continue; - var parent = context.Pages[rel.SourceId]; - var child = context.Pages[rel.DestinationId]; + var parent = context.Pages[rel.SourceId]; + var child = context.Pages[rel.DestinationId]; - if(parent.BirthDate >= child.BirthDate) - Error(Texts.Admin_Validation_Page_ParentYoungerThanChild, parent.Id, child.Id); - } + if(parent.BirthDate >= child.BirthDate) + Error(Texts.Admin_Validation_Page_ParentYoungerThanChild, parent.Id, child.Id); } + } + + /// + /// Finds loops of a particular relation in the relation graph. + /// + private void CheckLoops(RelationContext context, Guid pageId) + { + var isLoopFound = false; + var visited = context.Pages.ToDictionary(x => x.Key, x => false); + CheckLoopsInternal(pageId); - /// - /// Finds loops of a particular relation in the relation graph. - /// - private void CheckLoops(RelationContext context, Guid pageId) + void CheckLoopsInternal(Guid id) { - var isLoopFound = false; - var visited = context.Pages.ToDictionary(x => x.Key, x => false); - CheckLoopsInternal(pageId); + if (isLoopFound || !context.Relations.ContainsKey(id)) + return; - void CheckLoopsInternal(Guid id) + visited[id] = true; + + foreach (var rel in context.Relations[id]) { - if (isLoopFound || !context.Relations.ContainsKey(id)) - return; + if (rel.Type != RelationType.Parent) + continue; - visited[id] = true; + if (isLoopFound) + return; - foreach (var rel in context.Relations[id]) + if (visited[rel.DestinationId]) { - if (rel.Type != RelationType.Parent) - continue; - - if (isLoopFound) - return; - - if (visited[rel.DestinationId]) - { - isLoopFound = true; - Error(Texts.Admin_Validation_Page_ParentLoop, rel.DestinationId, pageId); - return; - } - - CheckLoopsInternal(rel.DestinationId); + isLoopFound = true; + Error(Texts.Admin_Validation_Page_ParentLoop, rel.DestinationId, pageId); + return; } + + CheckLoopsInternal(rel.DestinationId); } } + } - /// - /// Checks if a person has more than two parents. - /// - private void CheckParents(RelationContext context) + /// + /// Checks if a person has more than two parents. + /// + private void CheckParents(RelationContext context) + { + foreach (var page in context.Pages.Values) { - foreach (var page in context.Pages.Values) - { - if (!context.Relations.TryGetValue(page.Id, out var rels)) - continue; + if (!context.Relations.TryGetValue(page.Id, out var rels)) + continue; - var parents = rels.Where(x => x.Type == RelationType.Parent) - .ToList(); + var parents = rels.Where(x => x.Type == RelationType.Parent) + .ToList(); - if(parents.Count > 2) - Error(Texts.Admin_Validation_Page_ManyBioParents, page.Id); + if(parents.Count > 2) + Error(Texts.Admin_Validation_Page_ManyBioParents, page.Id); - if (parents.Count == 2) - { - var p1 = context.Pages[parents[0].DestinationId]; - var p2 = context.Pages[parents[1].DestinationId]; + if (parents.Count == 2) + { + var p1 = context.Pages[parents[0].DestinationId]; + var p2 = context.Pages[parents[1].DestinationId]; - if(p1.Gender == p2.Gender && p1.Gender != null) - Error(Texts.Admin_Validation_Page_BioParentsSameGender, p1.Id, p2.Id, page.Id); - } + if(p1.Gender == p2.Gender && p1.Gender != null) + Error(Texts.Admin_Validation_Page_BioParentsSameGender, p1.Id, p2.Id, page.Id); } } + } - #endregion - - #region Helpers + #endregion - /// - /// Adds a new violation info. - /// - private void Error(string msg, params Guid[] pages) - { - _errors.Add(new ConsistencyErrorInfo(msg, pages)); - } + #region Helpers - #endregion + /// + /// Adds a new violation info. + /// + private void Error(string msg, params Guid[] pages) + { + _errors.Add(new ConsistencyErrorInfo(msg, pages)); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/TagHelpers/ValidationListTagHelper.cs b/src/Bonsai/Areas/Admin/TagHelpers/ValidationListTagHelper.cs index 233c90fd..17ebd018 100644 --- a/src/Bonsai/Areas/Admin/TagHelpers/ValidationListTagHelper.cs +++ b/src/Bonsai/Areas/Admin/TagHelpers/ValidationListTagHelper.cs @@ -3,47 +3,46 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; -namespace Bonsai.Areas.Admin.TagHelpers +namespace Bonsai.Areas.Admin.TagHelpers; + +/// +/// Special validation error renderer for the fact list. +/// +[HtmlTargetElement(Attributes = "validation-list-for")] +public class ValidationListTagHelper: TagHelper { - /// - /// Special validation error renderer for the fact list. - /// - [HtmlTargetElement(Attributes = "validation-list-for")] - public class ValidationListTagHelper: TagHelper - { - [HtmlAttributeNotBound] - [ViewContext] - public ViewContext ViewContext { get; set; } + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } - [HtmlAttributeName("validation-list-caption")] - public string Caption { get; set; } + [HtmlAttributeName("validation-list-caption")] + public string Caption { get; set; } - [HtmlAttributeName("validation-list-for")] - public ModelExpression For { get; set; } + [HtmlAttributeName("validation-list-for")] + public ModelExpression For { get; set; } - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if (For == null) - return; + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (For == null) + return; - var model = ViewContext.ViewData.ModelState; - if (!model.TryGetValue(For.Name, out var state) || state.ValidationState != ModelValidationState.Invalid) - { - output.SuppressOutput(); - return; - } + var model = ViewContext.ViewData.ModelState; + if (!model.TryGetValue(For.Name, out var state) || state.ValidationState != ModelValidationState.Invalid) + { + output.SuppressOutput(); + return; + } - var ul = new TagBuilder("ul"); - ul.AddCssClass("mb-0"); - foreach (var error in state.Errors) - { - var li = new TagBuilder("li"); - li.InnerHtml.Append(error.ErrorMessage); - ul.InnerHtml.AppendHtml(li); - } - - output.Content.AppendHtml($"
{Caption}:
"); - output.Content.AppendHtml(ul); + var ul = new TagBuilder("ul"); + ul.AddCssClass("mb-0"); + foreach (var error in state.Errors) + { + var li = new TagBuilder("li"); + li.InnerHtml.Append(error.ErrorMessage); + ul.InnerHtml.AppendHtml(li); } + + output.Content.AppendHtml($"
{Caption}:
"); + output.Content.AppendHtml(ul); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Utils/ListRequestHelper.cs b/src/Bonsai/Areas/Admin/Utils/ListRequestHelper.cs index 4d04de24..ab693963 100644 --- a/src/Bonsai/Areas/Admin/Utils/ListRequestHelper.cs +++ b/src/Bonsai/Areas/Admin/Utils/ListRequestHelper.cs @@ -6,72 +6,71 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Impworks.Utils.Format; -namespace Bonsai.Areas.Admin.Utils +namespace Bonsai.Areas.Admin.Utils; + +/// +/// Helper methods for automatically managing ListRequests. +/// +public static class ListRequestHelper { /// - /// Helper methods for automatically managing ListRequests. + /// Gets all arbitrary values for the request. /// - public static class ListRequestHelper + public static IEnumerable> GetValues(ListRequestVM vm) { - /// - /// Gets all arbitrary values for the request. - /// - public static IEnumerable> GetValues(ListRequestVM vm) - { - var dict = new List>(); + var dict = new List>(); - if (vm == null) - return dict; + if (vm == null) + return dict; - var props = vm.GetType().GetProperties(); - foreach (var prop in props) - { - var propType = prop.PropertyType; - var value = prop.GetValue(vm); + var props = vm.GetType().GetProperties(); + foreach (var prop in props) + { + var propType = prop.PropertyType; + var value = prop.GetValue(vm); - if (value == null) - continue; + if (value == null) + continue; - var defValue = propType.IsValueType && Nullable.GetUnderlyingType(propType) == null - ? Activator.CreateInstance(propType) - : null; + var defValue = propType.IsValueType && Nullable.GetUnderlyingType(propType) == null + ? Activator.CreateInstance(propType) + : null; - if (value.Equals(defValue)) - continue; + if (value.Equals(defValue)) + continue; - var isEnumerable = propType.IsArray - || (propType != typeof(string) && propType.GetInterfaces().Contains(typeof(IEnumerable))); - if (isEnumerable) - { - foreach (object elem in (dynamic) value) - Add(prop.Name, elem); - } - else - { - Add(prop.Name, value); - } + var isEnumerable = propType.IsArray + || (propType != typeof(string) && propType.GetInterfaces().Contains(typeof(IEnumerable))); + if (isEnumerable) + { + foreach (object elem in (dynamic) value) + Add(prop.Name, elem); } - - return dict; - - void Add(string propName, object value) + else { - var str = value is IConvertible fmt - ? fmt.ToInvariantString() - : value.ToString(); - - dict.Add(new KeyValuePair(propName, str)); + Add(prop.Name, value); } } - /// - /// Returns the URL for current request. - /// - public static string GetUrl(string url, ListRequestVM request) + return dict; + + void Add(string propName, object value) { - var args = GetValues(request); - var strArgs = args.Select(x => HttpUtility.UrlEncode(x.Key) + "=" + HttpUtility.UrlEncode(x.Value)); - return url + "?" + string.Join("&", strArgs); + var str = value is IConvertible fmt + ? fmt.ToInvariantString() + : value.ToString(); + + dict.Add(new KeyValuePair(propName, str)); } } -} + + /// + /// Returns the URL for current request. + /// + public static string GetUrl(string url, ListRequestVM request) + { + var args = GetValues(request); + var strArgs = args.Select(x => HttpUtility.UrlEncode(x.Key) + "=" + HttpUtility.UrlEncode(x.Value)); + return url + "?" + string.Join("&", strArgs); + } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Utils/OperationResultMessage.cs b/src/Bonsai/Areas/Admin/Utils/OperationResultMessage.cs index b8d6af0a..7f83e3ab 100644 --- a/src/Bonsai/Areas/Admin/Utils/OperationResultMessage.cs +++ b/src/Bonsai/Areas/Admin/Utils/OperationResultMessage.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.Utils +namespace Bonsai.Areas.Admin.Utils; + +/// +/// Information about the previous operation passed between pages. +/// +public class OperationResultMessage { /// - /// Information about the previous operation passed between pages. + /// Message text. /// - public class OperationResultMessage - { - /// - /// Message text. - /// - public string Message { get; set; } + public string Message { get; init; } - /// - /// Success flag. - /// - public bool IsSuccess { get; set; } - } -} + /// + /// Success flag. + /// + public bool IsSuccess { get; init; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/Utils/UploadException.cs b/src/Bonsai/Areas/Admin/Utils/UploadException.cs index 185c8fa8..1fdf7e8a 100644 --- a/src/Bonsai/Areas/Admin/Utils/UploadException.cs +++ b/src/Bonsai/Areas/Admin/Utils/UploadException.cs @@ -1,22 +1,8 @@ using System; -namespace Bonsai.Areas.Admin.Utils -{ - /// - /// Exception that occurs during a file upload. - /// - public class UploadException: Exception - { - public UploadException() - { - } +namespace Bonsai.Areas.Admin.Utils; - public UploadException(string message) : base(message) - { - } - - public UploadException(string message, Exception innerException) : base(message, innerException) - { - } - } -} +/// +/// Exception that occurs during a file upload. +/// +public class UploadException(string message) : Exception(message); \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangeVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangeVM.cs index 996c8a8d..d9eab9d6 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangeVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangeVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Changesets +namespace Bonsai.Areas.Admin.ViewModels.Changesets; + +/// +/// Details about a particular property. +/// +public class ChangeVM { /// - /// Details about a particular property. + /// Title of the property. /// - public class ChangeVM - { - /// - /// Title of the property. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// Difference in property values. - /// - public string Diff { get; set; } - } -} + /// + /// Difference in property values. + /// + public string Diff { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetDetailsVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetDetailsVM.cs index 575369fe..671a7b4c 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetDetailsVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetDetailsVM.cs @@ -2,66 +2,65 @@ using System.Collections.Generic; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Changesets +namespace Bonsai.Areas.Admin.ViewModels.Changesets; + +/// +/// Base information about a changeset. +/// +public class ChangesetDetailsVM { /// - /// Base information about a changeset. + /// ID of the changeset. /// - public class ChangesetDetailsVM - { - /// - /// ID of the changeset. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Edit date. - /// - public DateTimeOffset Date { get; set; } + /// + /// Edit date. + /// + public DateTimeOffset Date { get; set; } - /// - /// User that has authored the edit. - /// - public string Author { get; set; } + /// + /// User that has authored the edit. + /// + public string Author { get; set; } - /// - /// Type of the change. - /// - public ChangesetType ChangeType { get; set; } + /// + /// Type of the change. + /// + public ChangesetType ChangeType { get; set; } - /// - /// Type of the changed entity. - /// - public ChangesetEntityType EntityType { get; set; } + /// + /// Type of the changed entity. + /// + public ChangesetEntityType EntityType { get; set; } - /// - /// ID of the entity that has been edited. - /// - public Guid EntityId { get; set; } + /// + /// ID of the entity that has been edited. + /// + public Guid EntityId { get; set; } - /// - /// Flag indicating that the entity has a current version that can be viewed. - /// - public bool EntityExists { get; set; } + /// + /// Flag indicating that the entity has a current version that can be viewed. + /// + public bool EntityExists { get; set; } - /// - /// Frontend-based key of the entity. - /// - public string EntityKey { get; set; } + /// + /// Frontend-based key of the entity. + /// + public string EntityKey { get; set; } - /// - /// URL of the thumbnail (for media changesets). - /// - public string ThumbnailUrl { get; set; } + /// + /// URL of the thumbnail (for media changesets). + /// + public string ThumbnailUrl { get; set; } - /// - /// Changed items. - /// - public IReadOnlyList Changes { get; set; } + /// + /// Changed items. + /// + public IReadOnlyList Changes { get; set; } - /// - /// Flag indicating that this changeset can be reverted. - /// - public bool CanRevert { get; set; } - } -} + /// + /// Flag indicating that this changeset can be reverted. + /// + public bool CanRevert { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetTitleVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetTitleVM.cs index ff24adbf..5d4c05d6 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetTitleVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetTitleVM.cs @@ -1,71 +1,70 @@ using System; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Changesets +namespace Bonsai.Areas.Admin.ViewModels.Changesets; + +/// +/// Brief information about a changeset. +/// +public class ChangesetTitleVM { /// - /// Brief information about a changeset. + /// Changeset ID. /// - public class ChangesetTitleVM - { - /// - /// Changeset ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Edit date. - /// - public DateTimeOffset Date { get; set; } + /// + /// Edit date. + /// + public DateTimeOffset Date { get; set; } - /// - /// Type of the change. - /// - public ChangesetType ChangeType { get; set; } + /// + /// Type of the change. + /// + public ChangesetType ChangeType { get; set; } - /// - /// Name of the changed entity. - /// - public string EntityTitle { get; set; } + /// + /// Name of the changed entity. + /// + public string EntityTitle { get; set; } - /// - /// URL of the entity's thumbnail. - /// - public string EntityThumbnailUrl { get; set; } + /// + /// URL of the entity's thumbnail. + /// + public string EntityThumbnailUrl { get; set; } - /// - /// Type of the changed entity. - /// - public ChangesetEntityType EntityType { get; set; } + /// + /// Type of the changed entity. + /// + public ChangesetEntityType EntityType { get; set; } - /// - /// ID of the entity that has been edited. - /// - public Guid EntityId { get; set; } + /// + /// ID of the entity that has been edited. + /// + public Guid EntityId { get; set; } - /// - /// Flag indicating that the entity has a current version that can be viewed. - /// - public bool EntityExists { get; set; } + /// + /// Flag indicating that the entity has a current version that can be viewed. + /// + public bool EntityExists { get; set; } - /// - /// Frontend-based key of the entity. - /// - public string EntityKey { get; set; } + /// + /// Frontend-based key of the entity. + /// + public string EntityKey { get; set; } - /// - /// Author of the change. - /// - public string Author { get; set; } + /// + /// Author of the change. + /// + public string Author { get; set; } - /// - /// Type of the page (if the changeset is page-related). - /// - public PageType? PageType { get; set; } + /// + /// Type of the page (if the changeset is page-related). + /// + public PageType? PageType { get; set; } - /// - /// Flag indicating that this changeset can be reverted. - /// - public bool CanRevert { get; set; } - } -} + /// + /// Flag indicating that this changeset can be reverted. + /// + public bool CanRevert { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListRequestVM.cs index 858104d9..05120581 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListRequestVM.cs @@ -2,48 +2,47 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Changesets +namespace Bonsai.Areas.Admin.ViewModels.Changesets; + +/// +/// Request for changesets. +/// +public class ChangesetsListRequestVM: ListRequestVM { - /// - /// Request for changesets. - /// - public class ChangesetsListRequestVM: ListRequestVM + public ChangesetsListRequestVM() { - public ChangesetsListRequestVM() - { - OrderDescending = true; - } + OrderDescending = true; + } - /// - /// Filter by entity. - /// - public Guid? EntityId { get; set; } + /// + /// Filter by entity. + /// + public Guid? EntityId { get; set; } - /// - /// Filter by author. - /// - public string UserId { get; set; } + /// + /// Filter by author. + /// + public string UserId { get; set; } - /// - /// Found entity types. - /// - public ChangesetEntityType[] EntityTypes { get; set; } + /// + /// Found entity types. + /// + public ChangesetEntityType[] EntityTypes { get; set; } - /// - /// Types of the change. - /// - public ChangesetType[] ChangesetTypes { get; set; } + /// + /// Types of the change. + /// + public ChangesetType[] ChangesetTypes { get; set; } - /// - /// Checks if the request has no filter applied. - /// - public override bool IsEmpty() - { - return base.IsEmpty() - && EntityId == null - && string.IsNullOrEmpty(UserId) - && (EntityTypes == null || EntityTypes.Length == 0) - && (ChangesetTypes == null || ChangesetTypes.Length == 0); - } + /// + /// Checks if the request has no filter applied. + /// + public override bool IsEmpty() + { + return base.IsEmpty() + && EntityId == null + && string.IsNullOrEmpty(UserId) + && (EntityTypes == null || EntityTypes.Length == 0) + && (ChangesetTypes == null || ChangesetTypes.Length == 0); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListVM.cs index 5ea5f138..e32e3a7a 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Changesets/ChangesetsListVM.cs @@ -1,20 +1,19 @@ using Bonsai.Areas.Admin.ViewModels.Common; -namespace Bonsai.Areas.Admin.ViewModels.Changesets +namespace Bonsai.Areas.Admin.ViewModels.Changesets; + +/// +/// List of found changesets. +/// +public class ChangesetsListVM: ListResultVM { /// - /// List of found changesets. + /// Title of the filter-by entity. /// - public class ChangesetsListVM: ListResultVM - { - /// - /// Title of the filter-by entity. - /// - public string EntityTitle { get; set; } + public string EntityTitle { get; set; } - /// - /// Title of the filter-by user. - /// - public string UserTitle { get; set; } - } -} + /// + /// Title of the filter-by user. + /// + public string UserTitle { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/LinkVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/LinkVM.cs index 5f01a3da..a3119c1c 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/LinkVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/LinkVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// Info about a single link used somewhere in a message. +/// +public class LinkVM { /// - /// Info about a single link used somewhere in a message. + /// Displayed title. /// - public class LinkVM - { - /// - /// Displayed title. - /// - public string Title { get; set; } + public string Title { get; init; } - /// - /// URL of the link. - /// - public string Url { get; set; } - } -} + /// + /// URL of the link. + /// + public string Url { get; init; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/ListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/ListRequestVM.cs index 2c6665ef..0300e3de 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/ListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/ListRequestVM.cs @@ -1,42 +1,41 @@ -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// Base request for entity list filtering. +/// +public class ListRequestVM { /// - /// Base request for entity list filtering. + /// Search query for page names. /// - public class ListRequestVM - { - /// - /// Search query for page names. - /// - public string SearchQuery { get; set; } + public string SearchQuery { get; set; } - /// - /// Ordering field. - /// - public string OrderBy { get; set; } - - /// - /// Ordering direction. - /// - public bool? OrderDescending { get; set; } + /// + /// Ordering field. + /// + public string OrderBy { get; set; } - /// - /// Current page (0-based). - /// - public int Page { get; set; } + /// + /// Ordering direction. + /// + public bool? OrderDescending { get; set; } - /// - /// Checks if the request is empty. - /// - public virtual bool IsEmpty() - { - return string.IsNullOrEmpty(SearchQuery) - && Page == 0; - } + /// + /// Current page (0-based). + /// + public int Page { get; set; } - /// - /// Creates a clone of this object. - /// - public static T Clone(T request) where T: ListRequestVM => (T) request.MemberwiseClone(); + /// + /// Checks if the request is empty. + /// + public virtual bool IsEmpty() + { + return string.IsNullOrEmpty(SearchQuery) + && Page == 0; } -} + + /// + /// Creates a clone of this object. + /// + public static T Clone(T request) where T: ListRequestVM => (T) request.MemberwiseClone(); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/ListResultVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/ListResultVM.cs index 870a6c24..9e5932b6 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/ListResultVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/ListResultVM.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// Base VM for all result list views. +/// +public class ListResultVM + where TRequest: ListRequestVM { /// - /// Base VM for all result list views. + /// Current search query. /// - public class ListResultVM - where TRequest: ListRequestVM - { - /// - /// Current search query. - /// - public TRequest Request { get; set; } + public TRequest Request { get; set; } - /// - /// List of pages. - /// - public IReadOnlyList Items { get; set; } + /// + /// List of pages. + /// + public IReadOnlyList Items { get; set; } - /// - /// Number of pages of data. - /// - public int PageCount { get; set; } - } -} + /// + /// Number of pages of data. + /// + public int PageCount { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/PickRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/PickRequestVM.cs index 485800c4..d8feb434 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/PickRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/PickRequestVM.cs @@ -1,15 +1,29 @@ using System; -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// Request arguments for picking a page/media. +/// +public class PickRequestVM where T: Enum { /// - /// Request arguments for picking a page/media. + /// Search query. + /// + public string Query { get; set; } + + /// + /// Number of items to display. + /// + public int? Count { get; set; } + + /// + /// Number of items to skip (e.g. pagination). + /// + public int? Offset { get; set; } + + /// + /// Types of entities to include (e.g. Person/Pet/etc for pages, Photo/Video for media). /// - public class PickRequestVM where T: Enum - { - public string Query { get; set; } - public int? Count { get; set; } - public int? Offset { get; set; } - public T[] Types { get; set; } - } -} + public T[] Types { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryInfoVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryInfoVM.cs index f8dbf017..7c0ab8aa 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryInfoVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryInfoVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// DTO for displaying a removal confirmation page for any entity. +/// +public class RemoveEntryInfoVM { /// - /// DTO for displaying a removal confirmation page for any entity. + /// Details of the entry to remove. /// - public class RemoveEntryInfoVM - { - /// - /// Details of the entry to remove. - /// - public T Entry { get; set; } + public T Entry { get; set; } - /// - /// Flag indicating that the current user is allowed to irreversibly remove the entry. - /// - public bool CanRemoveCompletely { get; set; } - } + /// + /// Flag indicating that the current user is allowed to irreversibly remove the entry. + /// + public bool CanRemoveCompletely { get; set; } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryRequestVM.cs index 53d3ce2f..f0146548 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Common/RemoveEntryRequestVM.cs @@ -1,10 +1,19 @@ using System; -namespace Bonsai.Areas.Admin.ViewModels.Common +namespace Bonsai.Areas.Admin.ViewModels.Common; + +/// +/// Request to remove an entity (page, media). +/// +public class RemoveEntryRequestVM { - public class RemoveEntryRequestVM - { - public Guid Id { get; set; } - public bool RemoveCompletely { get; set; } - } + /// + /// ID of the removed entity. + /// + public Guid Id { get; set; } + + /// + /// Flag to remove all underlying data and related changesets. + /// + public bool RemoveCompletely { get; set; } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Components/ListEnumFilterItemVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Components/ListEnumFilterItemVM.cs index 2efef943..5a651913 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Components/ListEnumFilterItemVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Components/ListEnumFilterItemVM.cs @@ -1,28 +1,27 @@ -namespace Bonsai.Areas.Admin.ViewModels.Components +namespace Bonsai.Areas.Admin.ViewModels.Components; + +/// +/// Displays the single filter item. +/// +public class ListEnumFilterItemVM { /// - /// Displays the single filter item. + /// Name of the field. /// - public class ListEnumFilterItemVM - { - /// - /// Name of the field. - /// - public string PropertyName { get; set; } + public string PropertyName { get; set; } - /// - /// Element's readable title. - /// - public string Title { get; set; } + /// + /// Element's readable title. + /// + public string Title { get; set; } - /// - /// Element's underlying value. - /// - public string Value { get; set; } + /// + /// Element's underlying value. + /// + public string Value { get; set; } - /// - /// Flag indicating that the element is currently selected. - /// - public bool IsActive { get; set; } - } -} + /// + /// Flag indicating that the element is currently selected. + /// + public bool IsActive { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Components/ListHeaderVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Components/ListHeaderVM.cs index 765e393f..13c5e3ec 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Components/ListHeaderVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Components/ListHeaderVM.cs @@ -1,25 +1,24 @@ -namespace Bonsai.Areas.Admin.ViewModels.Components +namespace Bonsai.Areas.Admin.ViewModels.Components; + +/// +/// Data for rendering a list's header. +/// +public class ListHeaderVM { /// - /// Data for rendering a list's header. + /// Header rendered title. /// - public class ListHeaderVM - { - /// - /// Header rendered title. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// Header's new sort URL. - /// - public string Url { get; set; } + /// + /// Header's new sort URL. + /// + public string Url { get; set; } - /// - /// False = ascending sort. - /// True = descending sort. - /// Null = not sorted by this field. - /// - public bool? IsDescending { get; set; } - } -} + /// + /// False = ascending sort. + /// True = descending sort. + /// Null = not sorted by this field. + /// + public bool? IsDescending { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Components/ListItemFilterVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Components/ListItemFilterVM.cs index e45c6b67..5d41d900 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Components/ListItemFilterVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Components/ListItemFilterVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Components +namespace Bonsai.Areas.Admin.ViewModels.Components; + +/// +/// Component for +/// +public class ListItemFilterVM { /// - /// Component for + /// Readable title of the filter-by entity. /// - public class ListItemFilterVM - { - /// - /// Readable title of the filter-by entity. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// Link for filter cancellation. - /// - public string CancelUrl { get; set; } - } -} + /// + /// Link for filter cancellation. + /// + public string CancelUrl { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Components/ListPaginatorPageVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Components/ListPaginatorPageVM.cs index bffcb83a..2852a74b 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Components/ListPaginatorPageVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Components/ListPaginatorPageVM.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Areas.Admin.ViewModels.Components +namespace Bonsai.Areas.Admin.ViewModels.Components; + +/// +/// Element of a single page. +/// +public class ListPaginatorPageVM { /// - /// Element of a single page. + /// Flag indicating that this page is selected. /// - public class ListPaginatorPageVM - { - /// - /// Flag indicating that this page is selected. - /// - public bool IsCurrent { get; set; } + public bool IsCurrent { get; set; } - /// - /// Displayed title. - /// - public string Title { get; set; } + /// + /// Displayed title. + /// + public string Title { get; set; } - /// - /// Link URL. - /// - public string Url { get; set; } - } -} + /// + /// Link URL. + /// + public string Url { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Components/ListSearchFieldVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Components/ListSearchFieldVM.cs deleted file mode 100644 index a37e708a..00000000 --- a/src/Bonsai/Areas/Admin/ViewModels/Components/ListSearchFieldVM.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace Bonsai.Areas.Admin.ViewModels.Components -{ - /// - /// Details for a list search form. - /// - public class ListSearchFieldVM - { - /// - /// Request URL. - /// - public string Url { get; set; } - - /// - /// The search query's current value. - /// - public string SearchQuery { get; set; } - - /// - /// The other values for current search (order, etc.). - /// - public Dictionary OtherValues { get; set; } - } -} diff --git a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/ChangesetEventVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/ChangesetEventVM.cs index 8ef64953..396a89db 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/ChangesetEventVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/ChangesetEventVM.cs @@ -7,61 +7,60 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Dashboard +namespace Bonsai.Areas.Admin.ViewModels.Dashboard; + +/// +/// Details of a changeset to be displayed in the dashboard view. +/// +public class ChangesetEventVM: IMapped { /// - /// Details of a changeset to be displayed in the dashboard view. + /// Edit date. /// - public class ChangesetEventVM: IMapped - { - /// - /// Edit date. - /// - public DateTimeOffset Date { get; set; } + public DateTimeOffset Date { get; set; } - /// - /// Type of the changed entity. - /// - public ChangesetEntityType EntityType { get; set; } + /// + /// Type of the changed entity. + /// + public ChangesetEntityType EntityType { get; set; } - /// - /// Type of the change. - /// - public ChangesetType ChangeType { get; set; } + /// + /// Type of the change. + /// + public ChangesetType ChangeType { get; set; } - /// - /// Number of elements grouped in this change (only for MediaThumbnails for now). - /// - public int ElementCount { get; set; } + /// + /// Number of elements grouped in this change (only for MediaThumbnails for now). + /// + public int ElementCount { get; set; } - /// - /// Thumbnails for media (limited to 50). - /// - public IReadOnlyList MediaThumbnails { get; set; } + /// + /// Thumbnails for media (limited to 50). + /// + public IReadOnlyList MediaThumbnails { get; set; } - /// - /// Author of the change. - /// - public UserTitleVM User { get; set; } + /// + /// Author of the change. + /// + public UserTitleVM User { get; set; } - /// - /// Link to the entity. - /// - public LinkVM MainLink { get; set; } + /// + /// Link to the entity. + /// + public LinkVM MainLink { get; set; } - /// - /// Related links. - /// - public IReadOnlyList ExtraLinks { get; set; } + /// + /// Related links. + /// + public IReadOnlyList ExtraLinks { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.EntityType, x => x.EntityType) - .Map(x => x.ChangeType, x => x.ChangeType) - .Map(x => x.Date, x => x.Date) - .Map(x => x.User, x => x.Author) - .IgnoreNonMapped(true); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.EntityType, x => x.EntityType) + .Map(x => x.ChangeType, x => x.ChangeType) + .Map(x => x.Date, x => x.Date) + .Map(x => x.User, x => x.Author) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/DashboardVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/DashboardVM.cs index 43c1c3dc..a66b6da0 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/DashboardVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/DashboardVM.cs @@ -1,50 +1,49 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Admin.ViewModels.Dashboard +namespace Bonsai.Areas.Admin.ViewModels.Dashboard; + +/// +/// Dashboard information. +/// +public class DashboardVM { /// - /// Dashboard information. - /// - public class DashboardVM - { - /// - /// Last edits made by users. - /// - public IReadOnlyList Events { get; set; } - - /// - /// Total number of pages. - /// - public int PagesCount { get; set; } - - /// - /// Number of pages with a low completion score that need more info. - /// - public int PagesToImproveCount { get; set; } - - /// - /// Total number of media. - /// - public int MediaCount { get; set; } - - /// - /// Number of photos without tags. - /// - public int MediaToTagCount { get; set; } - - /// - /// Total number of relations. - /// - public int RelationsCount { get; set; } - - /// - /// Total number of users. - /// - public int UsersCount { get; set; } - - /// - /// Number of newly registered users. - /// - public int UsersPendingValidationCount { get; set; } - } -} + /// Last edits made by users. + /// + public IReadOnlyList Events { get; set; } + + /// + /// Total number of pages. + /// + public int PagesCount { get; set; } + + /// + /// Number of pages with a low completion score that need more info. + /// + public int PagesToImproveCount { get; set; } + + /// + /// Total number of media. + /// + public int MediaCount { get; set; } + + /// + /// Number of photos without tags. + /// + public int MediaToTagCount { get; set; } + + /// + /// Total number of relations. + /// + public int RelationsCount { get; set; } + + /// + /// Total number of users. + /// + public int UsersCount { get; set; } + + /// + /// Number of newly registered users. + /// + public int UsersPendingValidationCount { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/MediaThumbnailExtendedVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/MediaThumbnailExtendedVM.cs index 745ef71e..799cd535 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Dashboard/MediaThumbnailExtendedVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Dashboard/MediaThumbnailExtendedVM.cs @@ -8,45 +8,44 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Dashboard +namespace Bonsai.Areas.Admin.ViewModels.Dashboard; + +/// +/// Additional information about a media file. +/// +public class MediaThumbnailExtendedVM: MediaThumbnailVM, IMapped { /// - /// Additional information about a media file. + /// Unique ID. /// - public class MediaThumbnailExtendedVM: MediaThumbnailVM, IMapped - { - /// - /// Unique ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Date of the media file's upload. - /// - public DateTimeOffset UploadDate { get; set; } + /// + /// Date of the media file's upload. + /// + public DateTimeOffset UploadDate { get; set; } - /// - /// Number of tagged entities. - /// - public int MediaTagsCount { get; set; } + /// + /// Number of tagged entities. + /// + public int MediaTagsCount { get; set; } - /// - /// Readable title. - /// - public string Title { get; set; } + /// + /// Readable title. + /// + public string Title { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Key, x => x.Key) - .Map(x => x.Type, x => x.Type) - .Map(x => x.Date, x => FuzzyDate.TryParse(x.Date)) - .Map(x => x.UploadDate, x => x.UploadDate) - .Map(x => x.Title, x => x.Title) - .Map(x => x.IsProcessed, x => x.IsProcessed) - .Map(x => x.MediaTagsCount, x => x.Tags.Count(y => y.Type == MediaTagType.DepictedEntity)) - .Map(x => x.ThumbnailUrl, x => MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small)); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Key, x => x.Key) + .Map(x => x.Type, x => x.Type) + .Map(x => x.Date, x => FuzzyDate.TryParse(x.Date)) + .Map(x => x.UploadDate, x => x.UploadDate) + .Map(x => x.Title, x => x.Title) + .Map(x => x.IsProcessed, x => x.IsProcessed) + .Map(x => x.MediaTagsCount, x => x.Tags.Count(y => y.Type == MediaTagType.DepictedEntity)) + .Map(x => x.ThumbnailUrl, x => MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small)); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/DynamicConfig/UpdateDynamicConfigVM.cs b/src/Bonsai/Areas/Admin/ViewModels/DynamicConfig/UpdateDynamicConfigVM.cs index e96f64fb..c466c899 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/DynamicConfig/UpdateDynamicConfigVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/DynamicConfig/UpdateDynamicConfigVM.cs @@ -6,63 +6,62 @@ using Bonsai.Localization; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.DynamicConfig +namespace Bonsai.Areas.Admin.ViewModels.DynamicConfig; + +/// +/// Configuration properties. +/// +public class UpdateDynamicConfigVM: IMapped { /// - /// Configuration properties. + /// The title of the website. Displayed in the top bar and browser title. /// - public class UpdateDynamicConfigVM: IMapped - { - /// - /// The title of the website. Displayed in the top bar and browser title. - /// - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_TitleEmpty")] - [StringLength(200, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_TitleTooLong")] - public string Title { get; set; } + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_TitleEmpty")] + [StringLength(200, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_TitleTooLong")] + public string Title { get; set; } - /// - /// Flag indicating that the website allows unauthorized visitors to view the contents. - /// - public bool AllowGuests { get; set; } + /// + /// Flag indicating that the website allows unauthorized visitors to view the contents. + /// + public bool AllowGuests { get; set; } - /// - /// Flag indicating that new registrations are accepted. - /// - public bool AllowRegistration { get; set; } + /// + /// Flag indicating that new registrations are accepted. + /// + public bool AllowRegistration { get; set; } - /// - /// Flag indicating that black ribbon should not be displayed on deceased relatives in tree view. - /// - public bool HideBlackRibbon { get; set; } + /// + /// Flag indicating that black ribbon should not be displayed on deceased relatives in tree view. + /// + public bool HideBlackRibbon { get; set; } - /// - /// Tree render thoroughness coefficient. - /// - [Range(1, 100, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_ThoroughnessRange")] - public int TreeRenderThoroughness { get; set; } + /// + /// Tree render thoroughness coefficient. + /// + [Range(1, 100, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_DynamicConfig_Validation_ThoroughnessRange")] + public int TreeRenderThoroughness { get; set; } - /// - /// Kinds of tree which should be rendered automatically. - /// - public TreeKind[] TreeKinds { get; set; } + /// + /// Kinds of tree which should be rendered automatically. + /// + public TreeKind[] TreeKinds { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Title, x => x.Title) - .Map(x => x.AllowGuests, x => x.AllowGuests) - .Map(x => x.AllowRegistration, x => x.AllowRegistration) - .Map(x => x.TreeRenderThoroughness, x => x.TreeRenderThoroughness) - .Map(x => x.HideBlackRibbon, x => x.HideBlackRibbon) - .Map(x => x.TreeKinds, x => x.TreeKinds == null ? 0 : x.TreeKinds.Aggregate((TreeKind)0, (a, b) => a | b)); + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Title, x => x.Title) + .Map(x => x.AllowGuests, x => x.AllowGuests) + .Map(x => x.AllowRegistration, x => x.AllowRegistration) + .Map(x => x.TreeRenderThoroughness, x => x.TreeRenderThoroughness) + .Map(x => x.HideBlackRibbon, x => x.HideBlackRibbon) + .Map(x => x.TreeKinds, x => x.TreeKinds == null ? 0 : x.TreeKinds.Aggregate((TreeKind)0, (a, b) => a | b)); - config.NewConfig() - .Map(x => x.Title, x => x.Title) - .Map(x => x.AllowGuests, x => x.AllowGuests) - .Map(x => x.AllowRegistration, x => x.AllowRegistration) - .Map(x => x.TreeRenderThoroughness, x => x.TreeRenderThoroughness) - .Map(x => x.HideBlackRibbon, x => x.HideBlackRibbon) - .Map(x => x.TreeKinds, x => Enum.GetValues().Where(y => x.TreeKinds.HasFlag(y)).ToArray()); - } + config.NewConfig() + .Map(x => x.Title, x => x.Title) + .Map(x => x.AllowGuests, x => x.AllowGuests) + .Map(x => x.AllowRegistration, x => x.AllowRegistration) + .Map(x => x.TreeRenderThoroughness, x => x.TreeRenderThoroughness) + .Map(x => x.HideBlackRibbon, x => x.HideBlackRibbon) + .Map(x => x.TreeKinds, x => Enum.GetValues().Where(y => x.TreeKinds.HasFlag(y)).ToArray()); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorDataVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorDataVM.cs index 64573ad3..789c8407 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorDataVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorDataVM.cs @@ -1,19 +1,18 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Strongly typed structure with additional properties of the media editor. +/// +public class MediaEditorDataVM { - /// - /// Strongly typed structure with additional properties of the media editor. - /// - public class MediaEditorDataVM - { - public IEnumerable LocationItem { get; set; } - public IEnumerable EventItem { get; set; } - public IEnumerable DepictedEntityItems { get; set; } + public IEnumerable LocationItem { get; set; } + public IEnumerable EventItem { get; set; } + public IEnumerable DepictedEntityItems { get; set; } - public IEnumerable SaveActions { get; set; } + public IEnumerable SaveActions { get; set; } - public string ThumbnailUrl { get; set; } - } -} + public string ThumbnailUrl { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorSaveAction.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorSaveAction.cs index 3c13e508..1791c9ba 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorSaveAction.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorSaveAction.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Possible actions to execute during media editor save. +/// +public enum MediaEditorSaveAction { /// - /// Possible actions to execute during media editor save. + /// Just save the current editor. /// - public enum MediaEditorSaveAction - { - /// - /// Just save the current editor. - /// - Save = 0, + Save = 0, - /// - /// Save the editor and edit the next media without tags. - /// - SaveAndShowNext = 1, - } -} + /// + /// Save the editor and edit the next media without tags. + /// + SaveAndShowNext = 1, +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorVM.cs index 16b2d8ba..2c07ca11 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaEditorVM.cs @@ -5,80 +5,79 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Editable details of a media file. +/// +public class MediaEditorVM: IMapped, IVersionable { /// - /// Editable details of a media file. + /// Surrogate ID. /// - public class MediaEditorVM: IMapped, IVersionable - { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Type of the media file. - /// - public MediaType Type { get; set; } + /// + /// Type of the media file. + /// + public MediaType Type { get; set; } - /// - /// Path to the file on the server. - /// - public string FilePath { get; set; } + /// + /// Path to the file on the server. + /// + public string FilePath { get; set; } - /// - /// Media creation date. - /// - [StringLength(30)] - public string Date { get; set; } + /// + /// Media creation date. + /// + [StringLength(30)] + public string Date { get; set; } - /// - /// Title of the media (for documents). - /// - public string Title { get; set; } + /// + /// Title of the media (for documents). + /// + public string Title { get; set; } - /// - /// Markdown description of the media file. - /// - public string Description { get; set; } + /// + /// Markdown description of the media file. + /// + public string Description { get; set; } - /// - /// Serialized tag info. - /// - public string DepictedEntities { get; set; } + /// + /// Serialized tag info. + /// + public string DepictedEntities { get; set; } - /// - /// ID of the page or name of the location. - /// - public string Location { get; set; } + /// + /// ID of the page or name of the location. + /// + public string Location { get; set; } - /// - /// ID of the page or name of the event. - /// - public string Event { get; set; } + /// + /// ID of the page or name of the event. + /// + public string Event { get; set; } - /// - /// Action to execute on save. - /// - public MediaEditorSaveAction SaveAction { get; set; } + /// + /// Action to execute on save. + /// + public MediaEditorSaveAction SaveAction { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Type, x => x.Type) - .Map(x => x.FilePath, x => x.FilePath) - .Map(x => x.Date, x => x.Date) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Description, x => x.Description) - .IgnoreNonMapped(true); + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Type, x => x.Type) + .Map(x => x.FilePath, x => x.FilePath) + .Map(x => x.Date, x => x.Date) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Description, x => x.Description) + .IgnoreNonMapped(true); - config.NewConfig() - .Map(x => x.Date, x => x.Date) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Description, x => x.Description) - .IgnoreNonMapped(true); - } + config.NewConfig() + .Map(x => x.Date, x => x.Date) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Description, x => x.Description) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListRequestVM.cs index d5abedd2..ac61d787 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListRequestVM.cs @@ -2,31 +2,30 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// The request for filtering media files. +/// +public class MediaListRequestVM: ListRequestVM { /// - /// The request for filtering media files. + /// Related page. /// - public class MediaListRequestVM: ListRequestVM - { - /// - /// Related page. - /// - public Guid? EntityId { get; set; } + public Guid? EntityId { get; set; } - /// - /// Related media types. - /// - public MediaType[] Types { get; set; } + /// + /// Related media types. + /// + public MediaType[] Types { get; set; } - /// - /// Checks if the request has no filter applied. - /// - public override bool IsEmpty() - { - return base.IsEmpty() - && EntityId == null - && (Types == null || Types.Length == 0); - } + /// + /// Checks if the request has no filter applied. + /// + public override bool IsEmpty() + { + return base.IsEmpty() + && EntityId == null + && (Types == null || Types.Length == 0); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListVM.cs index 8c9b395f..9dacacb4 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaListVM.cs @@ -1,16 +1,15 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Areas.Admin.ViewModels.Dashboard; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// List of found media files. +/// +public class MediaListVM: ListResultVM { /// - /// List of found media files. + /// Title of the page to filter by. /// - public class MediaListVM: ListResultVM - { - /// - /// Title of the page to filter by. - /// - public string EntityTitle { get; set; } - } -} + public string EntityTitle { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaTagVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaTagVM.cs index 8d361904..defbc9a0 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaTagVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaTagVM.cs @@ -1,25 +1,24 @@ using System; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Information about a single tagged entity on the photo. +/// +public class MediaTagVM { /// - /// Information about a single tagged entity on the photo. + /// ID of the tagged entity (if specified). /// - public class MediaTagVM - { - /// - /// ID of the tagged entity (if specified). - /// - public Guid? PageId { get; set; } + public Guid? PageId { get; set; } - /// - /// Title of the tagged entity (if no page is specified). - /// - public string ObjectTitle { get; set; } + /// + /// Title of the tagged entity (if no page is specified). + /// + public string ObjectTitle { get; set; } - /// - /// Semicolon-separated coordinates of the tag. - /// - public string Coordinates { get; set; } - } -} + /// + /// Semicolon-separated coordinates of the tag. + /// + public string Coordinates { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadRequestVM.cs index 8a108c68..9c4eab27 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadRequestVM.cs @@ -1,33 +1,32 @@ -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Data about an uploaded media file. +/// +public class MediaUploadRequestVM { /// - /// Data about an uploaded media file. + /// Flag indicating that file name should be used as title (sanitized). /// - public class MediaUploadRequestVM - { - /// - /// Flag indicating that file name should be used as title (sanitized). - /// - public bool UseFileNameAsTitle { get; set; } + public bool UseFileNameAsTitle { get; set; } - /// - /// Optional title for the page. - /// - public string Title { get; set; } + /// + /// Optional title for the page. + /// + public string Title { get; set; } - /// - /// Date of the media's creation. - /// - public string Date { get; set; } + /// + /// Date of the media's creation. + /// + public string Date { get; set; } - /// - /// Location (title or page's GUID). - /// - public string Location { get; set; } + /// + /// Location (title or page's GUID). + /// + public string Location { get; set; } - /// - /// Event (title or page's GUID). - /// - public string Event { get; set; } - } -} + /// + /// Event (title or page's GUID). + /// + public string Event { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadResultVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadResultVM.cs index 09e9a248..a22e15f8 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadResultVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/MediaUploadResultVM.cs @@ -4,40 +4,39 @@ using Bonsai.Code.Infrastructure; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +/// +/// Results of the media file's upload. +/// +public class MediaUploadResultVM: IMapped { /// - /// Results of the media file's upload. + /// Unique ID. /// - public class MediaUploadResultVM: IMapped - { - /// - /// Unique ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Unique key. - /// - public string Key { get; set; } + /// + /// Unique key. + /// + public string Key { get; set; } - /// - /// Full path to preview. - /// - public string ThumbnailPath { get; set; } + /// + /// Full path to preview. + /// + public string ThumbnailPath { get; set; } - /// - /// Flag indicating that the media has been processed completely. - /// - public bool IsProcessed { get; set; } + /// + /// Flag indicating that the media has been processed completely. + /// + public bool IsProcessed { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Key, x => x.Key) - .Map(x => x.IsProcessed, x => x.IsProcessed) - .Map(x => x.ThumbnailPath, x => MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small)); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Key, x => x.Key) + .Map(x => x.IsProcessed, x => x.IsProcessed) + .Map(x => x.ThumbnailPath, x => MediaPresenterService.GetSizedMediaPath(x.FilePath, MediaSize.Small)); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Media/RemoveMediaInfoVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Media/RemoveMediaInfoVM.cs index 48b94b65..74bbc418 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Media/RemoveMediaInfoVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Media/RemoveMediaInfoVM.cs @@ -1,7 +1,6 @@ -namespace Bonsai.Areas.Admin.ViewModels.Media +namespace Bonsai.Areas.Admin.ViewModels.Media; + +public class RemoveMediaInfoVM { - public class RemoveMediaInfoVM - { - public string ThumbnailUrl { get; set; } - } -} + public string ThumbnailUrl { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuGroupVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuGroupVM.cs index f2722136..17f3c1dc 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuGroupVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuGroupVM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Admin.ViewModels.Menu +namespace Bonsai.Areas.Admin.ViewModels.Menu; + +/// +/// A collection of grouped menu items. +/// +public class MenuGroupVM { - /// - /// A collection of grouped menu items. - /// - public class MenuGroupVM + public MenuGroupVM(params MenuItemVM[] items) { - public MenuGroupVM(params MenuItemVM[] items) - { - Items = items; - } - - /// - /// The items in current group. - /// - public IReadOnlyList Items { get; } + Items = items; } -} + + /// + /// The items in current group. + /// + public IReadOnlyList Items { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuItemVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuItemVM.cs index f3fe76ae..083bd3e5 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuItemVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Menu/MenuItemVM.cs @@ -1,33 +1,32 @@ -namespace Bonsai.Areas.Admin.ViewModels.Menu +namespace Bonsai.Areas.Admin.ViewModels.Menu; + +/// +/// Element of the menu. +/// +public class MenuItemVM { /// - /// Element of the menu. + /// Displayed title of the element. /// - public class MenuItemVM - { - /// - /// Displayed title of the element. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// URL of the element. - /// - public string Url { get; set; } + /// + /// URL of the element. + /// + public string Url { get; set; } - /// - /// Icon class. - /// - public string Icon { get; set; } + /// + /// Icon class. + /// + public string Icon { get; set; } - /// - /// Flag indicating that the element is selected. - /// - public bool IsSelected { get; set; } + /// + /// Flag indicating that the element is selected. + /// + public bool IsSelected { get; set; } - /// - /// Additional pill text for showing notifications in the section. - /// - public int? NotificationsCount { get; set; } - } -} + /// + /// Additional pill text for showing notifications in the section. + /// + public int? NotificationsCount { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageDraftInfoVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageDraftInfoVM.cs index 609e56b6..8c87cb00 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageDraftInfoVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageDraftInfoVM.cs @@ -1,15 +1,14 @@ using System; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Information about a saved page draft. +/// +public class PageDraftInfoVM { /// - /// Information about a saved page draft. + /// Draft's timestamp. /// - public class PageDraftInfoVM - { - /// - /// Draft's timestamp. - /// - public DateTimeOffset LastUpdateDate { get; set; } - } -} + public DateTimeOffset LastUpdateDate { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorDataVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorDataVM.cs index 5f9f519f..f0faac69 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorDataVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorDataVM.cs @@ -3,61 +3,60 @@ using Bonsai.Code.DomainModel.Facts; using Microsoft.AspNetCore.Mvc.Rendering; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Additional information for the page editor. +/// +public class PageEditorDataVM { /// - /// Additional information for the page editor. - /// - public class PageEditorDataVM - { - /// - /// Flag indicating that a new page is being created. - /// - public bool IsNew { get; set; } - - /// - /// Known page types. - /// - public IReadOnlyList PageTypes { get; set; } - - /// - /// Known groups of facts. - /// - public IEnumerable FactGroups { get; set; } - - /// - /// List of editor template files. - /// - public IReadOnlyList EditorTemplates { get; set; } - - /// - /// Currently active tab. - /// - public string Tab { get; set; } - - /// - /// List of comma-separated fields that contain errors. - /// - public string ErrorFields { get; set; } - - /// - /// Thumbnail URL for the selected main photo. - /// - public string MainPhotoThumbnailUrl { get; set; } - - /// - /// ID of the draft, if any. - /// - public Guid? DraftId { get; set; } - - /// - /// Last update of the draft. - /// - public DateTimeOffset? DraftLastUpdateDate { get; set; } - - /// - /// Flag indicating that a draft notification should be displayed at the top. - /// - public bool DraftDisplayNotification { get; set; } - } -} + /// Flag indicating that a new page is being created. + /// + public bool IsNew { get; set; } + + /// + /// Known page types. + /// + public IReadOnlyList PageTypes { get; set; } + + /// + /// Known groups of facts. + /// + public IEnumerable FactGroups { get; set; } + + /// + /// List of editor template files. + /// + public IReadOnlyList EditorTemplates { get; set; } + + /// + /// Currently active tab. + /// + public string Tab { get; set; } + + /// + /// List of comma-separated fields that contain errors. + /// + public string ErrorFields { get; set; } + + /// + /// Thumbnail URL for the selected main photo. + /// + public string MainPhotoThumbnailUrl { get; set; } + + /// + /// ID of the draft, if any. + /// + public Guid? DraftId { get; set; } + + /// + /// Last update of the draft. + /// + public DateTimeOffset? DraftLastUpdateDate { get; set; } + + /// + /// Flag indicating that a draft notification should be displayed at the top. + /// + public bool DraftDisplayNotification { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorVM.cs index 5f2f0e46..1a191c15 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageEditorVM.cs @@ -9,69 +9,68 @@ using Mapster; using Newtonsoft.Json; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Detailed information about a page's contents. +/// +public class PageEditorVM: IMapped, IVersionable { /// - /// Detailed information about a page's contents. + /// Surrogate ID. /// - public class PageEditorVM: IMapped, IVersionable - { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Page title. - /// - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_Pages_Validation_TitleEmpty")] - [StringLength(200, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_Pages_Validation_TitleTooLong")] - public string Title { get; set; } + /// + /// Page title. + /// + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_Pages_Validation_TitleEmpty")] + [StringLength(200, ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Admin_Pages_Validation_TitleTooLong")] + public string Title { get; set; } - /// - /// Type of the entity described by this page. - /// - public PageType Type { get; set; } + /// + /// Type of the entity described by this page. + /// + public PageType Type { get; set; } - /// - /// Free text description of the entity. - /// - public string Description { get; set; } + /// + /// Free text description of the entity. + /// + public string Description { get; set; } - /// - /// Serialized collection of facts related to current entity. - /// - public string Facts { get; set; } + /// + /// Serialized collection of facts related to current entity. + /// + public string Facts { get; set; } - /// - /// Key of the main photo. - /// - public string MainPhotoKey { get; set; } + /// + /// Key of the main photo. + /// + public string MainPhotoKey { get; set; } - /// - /// Aliases for current page. - /// - public string Aliases { get; set; } + /// + /// Aliases for current page. + /// + public string Aliases { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Type, x => x.Type) - .Map(x => x.Description, x => x.Description) - .Map(x => x.Facts, x => x.Facts) - .Map(x => x.MainPhotoKey, x => x.MainPhoto.Key) - .Map(x => x.Aliases, x => x.Aliases != null ? JsonConvert.SerializeObject(x.Aliases.OrderBy(y => y.Order).Select(y => y.Title)) : null); + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Type, x => x.Type) + .Map(x => x.Description, x => x.Description) + .Map(x => x.Facts, x => x.Facts) + .Map(x => x.MainPhotoKey, x => x.MainPhoto.Key) + .Map(x => x.Aliases, x => x.Aliases != null ? JsonConvert.SerializeObject(x.Aliases.OrderBy(y => y.Order).Select(y => y.Title)) : null); - config.NewConfig() - .Map(x => x.Title, x => x.Title) - .Map(x => x.Type, x => x.Type) - .Map(x => x.Description, x => x.Description) - .Map(x => x.Facts, x => x.Facts) - .Map(x => x.LastUpdateDate, x => DateTimeOffset.Now) - .Map(x => x.Key, x => PageHelper.EncodeTitle(x.Title)) - .IgnoreNonMapped(true); - } + config.NewConfig() + .Map(x => x.Title, x => x.Title) + .Map(x => x.Type, x => x.Type) + .Map(x => x.Description, x => x.Description) + .Map(x => x.Facts, x => x.Facts) + .Map(x => x.LastUpdateDate, x => DateTimeOffset.Now) + .Map(x => x.Key, x => PageHelper.EncodeTitle(x.Title)) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageRelationVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageRelationVM.cs index 8f3647dd..1abc6926 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageRelationVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageRelationVM.cs @@ -4,49 +4,48 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Information about a relation. +/// +public class PageRelationVM: IMapped { /// - /// Information about a relation. + /// ID of the source page. /// - public class PageRelationVM: IMapped - { - /// - /// ID of the source page. - /// - public Guid SourceId { get; set; } + public Guid SourceId { get; set; } - /// - /// ID of the second page. - /// - public Guid DestinationId { get; set; } + /// + /// ID of the second page. + /// + public Guid DestinationId { get; set; } - /// - /// ID of the related event. - /// - public Guid? EventId { get; set; } + /// + /// ID of the related event. + /// + public Guid? EventId { get; set; } - /// - /// Type of the relation. - /// - public RelationType Type { get; set; } + /// + /// Type of the relation. + /// + public RelationType Type { get; set; } - /// - /// Timespan of the relation. - /// - [StringLength(30)] - public string Duration { get; set; } + /// + /// Timespan of the relation. + /// + [StringLength(30)] + public string Duration { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .TwoWays() - .Map(x => x.SourceId, x => x.SourceId) - .Map(x => x.DestinationId, x => x.DestinationId) - .Map(x => x.Type, x => x.Type) - .Map(x => x.Duration, x => x.Duration) - .Map(x => x.EventId, x => x.EventId) - .IgnoreNonMapped(true); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .TwoWays() + .Map(x => x.SourceId, x => x.SourceId) + .Map(x => x.DestinationId, x => x.DestinationId) + .Map(x => x.Type, x => x.Type) + .Map(x => x.Duration, x => x.Duration) + .Map(x => x.EventId, x => x.EventId) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoreCriterionVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoreCriterionVM.cs index 3b8b43bf..f8c5317d 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoreCriterionVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoreCriterionVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Details of a scoring point for a page. +/// +public class PageScoreCriterionVM { /// - /// Details of a scoring point for a page. + /// Name of the scoring criterion. /// - public class PageScoreCriterionVM - { - /// - /// Name of the scoring criterion. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// Flag indicating that the criterion matched. - /// - public bool IsFilled { get; set; } - } -} + /// + /// Flag indicating that the criterion matched. + /// + public bool IsFilled { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoredVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoredVM.cs index c660dfad..6ba84003 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoredVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PageScoredVM.cs @@ -5,136 +5,135 @@ using Bonsai.Localization; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// Page details with extended completeness scoring info. +/// +public class PageScoredVM: IMapped { /// - /// Page details with extended completeness scoring info. + /// Surrogate ID. + /// + public Guid Id { get; set; } + + /// + /// Page title. + /// + public string Title { get; set; } + + /// + /// Page key (title, url-encoded). + /// + public string Key { get; set; } + + /// + /// Type of the entity described by this page. + /// + public PageType Type { get; set; } + + /// + /// Photograph for info block. + /// + public string MainPhotoPath { get; set; } + + /// + /// Date of the page's creation. + /// + public DateTimeOffset CreationDate { get; set; } + + /// + /// Date of the page's last revision. + /// + public DateTimeOffset LastUpdateDate { get; set; } + + public bool HasText { get; set; } + public bool HasPhoto { get; set; } + public bool HasRelations { get; set; } + public bool HasGender { get; set; } + public bool HasHumanName { get; set; } + public bool HasAnimalName { get; set; } + public bool HasAnimalSpecies { get; set; } + public bool HasBirthday { get; set; } + public bool HasBirthPlace { get; set; } + public bool HasEventDate { get; set; } + public bool HasLocationAddress { get; set; } + + /// + /// Page completeness score (1..100) depending on page type and its content flags. /// - public class PageScoredVM: IMapped + public int CompletenessScore { get; set; } + + public IEnumerable Criterions { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } - - /// - /// Page title. - /// - public string Title { get; set; } - - /// - /// Page key (title, url-encoded). - /// - public string Key { get; set; } - - /// - /// Type of the entity described by this page. - /// - public PageType Type { get; set; } - - /// - /// Photograph for info block. - /// - public string MainPhotoPath { get; set; } - - /// - /// Date of the page's creation. - /// - public DateTimeOffset CreationDate { get; set; } - - /// - /// Date of the page's last revision. - /// - public DateTimeOffset LastUpdateDate { get; set; } - - public bool HasText { get; set; } - public bool HasPhoto { get; set; } - public bool HasRelations { get; set; } - public bool HasGender { get; set; } - public bool HasHumanName { get; set; } - public bool HasAnimalName { get; set; } - public bool HasAnimalSpecies { get; set; } - public bool HasBirthday { get; set; } - public bool HasBirthPlace { get; set; } - public bool HasEventDate { get; set; } - public bool HasLocationAddress { get; set; } - - /// - /// Page completeness score (1..100) depending on page type and its content flags. - /// - public int CompletenessScore { get; set; } - - public IEnumerable Criterions + get { - get + yield return GetCriterion(Texts.Admin_Pages_Criterion_Text, x => x.HasText); + + if (Type == PageType.Person) { - yield return GetCriterion(Texts.Admin_Pages_Criterion_Text, x => x.HasText); - - if (Type == PageType.Person) - { - yield return GetCriterion(Texts.Admin_Pages_Criterion_HumanName, x => x.HasHumanName); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthday, x => x.HasBirthday); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthplace, x => x.HasBirthPlace); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Gender, x => x.HasGender); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Relations, x => x.HasRelations); - } - - if (Type == PageType.Pet) - { - yield return GetCriterion(Texts.Admin_Pages_Criterion_AnimalName, x => x.HasAnimalName); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthday, x => x.HasBirthday); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Species, x => x.HasAnimalSpecies); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Relations, x => x.HasRelations); - } - - if (Type == PageType.Event) - { - yield return GetCriterion(Texts.Admin_Pages_Criterion_Date, x => x.HasEventDate); - } - - if (Type == PageType.Location) - { - yield return GetCriterion(Texts.Admin_Pages_Criterion_Address, x => x.HasLocationAddress); - yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); - } + yield return GetCriterion(Texts.Admin_Pages_Criterion_HumanName, x => x.HasHumanName); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthday, x => x.HasBirthday); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthplace, x => x.HasBirthPlace); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Gender, x => x.HasGender); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Relations, x => x.HasRelations); } - } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Key, x => x.Key) - .Map(x => x.Type, x => x.Type) - .Map(x => x.MainPhotoPath, x => x.MainPhoto.FilePath) - .Map(x => x.CreationDate, x => x.CreationDate) - .Map(x => x.LastUpdateDate, x => x.LastUpdateDate) - .Map(x => x.HasText, x => x.HasText) - .Map(x => x.HasPhoto, x => x.HasPhoto) - .Map(x => x.HasRelations, x => x.HasRelations) - .Map(x => x.HasHumanName, x => x.HasHumanName) - .Map(x => x.HasAnimalName, x => x.HasAnimalName) - .Map(x => x.HasAnimalSpecies, x => x.HasAnimalSpecies) - .Map(x => x.HasBirthday, x => x.HasBirthday) - .Map(x => x.HasBirthPlace, x => x.HasBirthPlace) - .Map(x => x.HasEventDate, x => x.HasEventDate) - .Map(x => x.HasLocationAddress, x => x.HasLocationAddress) - .Map(x => x.CompletenessScore, x => x.CompletenessScore); - } + if (Type == PageType.Pet) + { + yield return GetCriterion(Texts.Admin_Pages_Criterion_AnimalName, x => x.HasAnimalName); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Birthday, x => x.HasBirthday); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Species, x => x.HasAnimalSpecies); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Relations, x => x.HasRelations); + } - /// - /// Creates the criterion for specified field. - /// - private PageScoreCriterionVM GetCriterion(string title, Func func) - { - return new PageScoreCriterionVM + if (Type == PageType.Event) + { + yield return GetCriterion(Texts.Admin_Pages_Criterion_Date, x => x.HasEventDate); + } + + if (Type == PageType.Location) { - Title = title, - IsFilled = func(this) - }; + yield return GetCriterion(Texts.Admin_Pages_Criterion_Address, x => x.HasLocationAddress); + yield return GetCriterion(Texts.Admin_Pages_Criterion_Photo, x => x.HasPhoto); + } } } -} + + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Key, x => x.Key) + .Map(x => x.Type, x => x.Type) + .Map(x => x.MainPhotoPath, x => x.MainPhoto.FilePath) + .Map(x => x.CreationDate, x => x.CreationDate) + .Map(x => x.LastUpdateDate, x => x.LastUpdateDate) + .Map(x => x.HasText, x => x.HasText) + .Map(x => x.HasPhoto, x => x.HasPhoto) + .Map(x => x.HasRelations, x => x.HasRelations) + .Map(x => x.HasHumanName, x => x.HasHumanName) + .Map(x => x.HasAnimalName, x => x.HasAnimalName) + .Map(x => x.HasAnimalSpecies, x => x.HasAnimalSpecies) + .Map(x => x.HasBirthday, x => x.HasBirthday) + .Map(x => x.HasBirthPlace, x => x.HasBirthPlace) + .Map(x => x.HasEventDate, x => x.HasEventDate) + .Map(x => x.HasLocationAddress, x => x.HasLocationAddress) + .Map(x => x.CompletenessScore, x => x.CompletenessScore); + } + + /// + /// Creates the criterion for specified field. + /// + private PageScoreCriterionVM GetCriterion(string title, Func func) + { + return new PageScoreCriterionVM + { + Title = title, + IsFilled = func(this) + }; + } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListRequestVM.cs index 96a9008d..75b86391 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListRequestVM.cs @@ -1,25 +1,24 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Pages +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// The request for filtering pages. +/// +public class PagesListRequestVM: ListRequestVM { /// - /// The request for filtering pages. + /// List of selected page types. /// - public class PagesListRequestVM: ListRequestVM - { - /// - /// List of selected page types. - /// - public PageType[] Types { get; set; } + public PageType[] Types { get; set; } - /// - /// Checks if the request has no filter applied. - /// - public override bool IsEmpty() - { - return base.IsEmpty() - && (Types == null || Types.Length == 0); - } + /// + /// Checks if the request has no filter applied. + /// + public override bool IsEmpty() + { + return base.IsEmpty() + && (Types == null || Types.Length == 0); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListVM.cs index ffebb446..26dc87ee 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Pages/PagesListVM.cs @@ -1,11 +1,8 @@ using Bonsai.Areas.Admin.ViewModels.Common; -namespace Bonsai.Areas.Admin.ViewModels.Pages -{ - /// - /// List of pages. - /// - public class PagesListVM: ListResultVM - { - } -} +namespace Bonsai.Areas.Admin.ViewModels.Pages; + +/// +/// List of pages. +/// +public class PagesListVM: ListResultVM; \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorDataVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorDataVM.cs index b3e3edd3..66ac288b 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorDataVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorDataVM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Strongly typed structure for additional properties of the Relation editor. +/// +public class RelationEditorDataVM { - /// - /// Strongly typed structure for additional properties of the Relation editor. - /// - public class RelationEditorDataVM - { - public bool IsNew { get; set; } + public bool IsNew { get; set; } - public RelationEditorPropertiesVM Properties { get; set; } + public RelationEditorPropertiesVM Properties { get; set; } - public IEnumerable SourceItems { get; set; } - public IEnumerable DestinationItem { get; set; } - public IEnumerable EventItem { get; set; } - public IEnumerable RelationTypes { get; set; } - } -} + public IEnumerable SourceItems { get; set; } + public IEnumerable DestinationItem { get; set; } + public IEnumerable EventItem { get; set; } + public IEnumerable RelationTypes { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorPropertiesVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorPropertiesVM.cs index 5ea6d312..387fc2d5 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorPropertiesVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorPropertiesVM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Extended properties for the relation editor. +/// +public class RelationEditorPropertiesVM { - /// - /// Extended properties for the relation editor. - /// - public class RelationEditorPropertiesVM - { - public string SourceName { get; set; } - public string DestinationName { get; set; } + public string SourceName { get; set; } + public string DestinationName { get; set; } - public IReadOnlyList SourceTypes { get; set; } - public IReadOnlyList DestinationTypes { get; set; } + public IReadOnlyList SourceTypes { get; set; } + public IReadOnlyList DestinationTypes { get; set; } - public bool ShowDuration { get; set; } - public bool ShowEvent { get; set; } - } -} + public bool ShowDuration { get; set; } + public bool ShowEvent { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorVM.cs index 5a8660f7..f1af6cd3 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationEditorVM.cs @@ -5,62 +5,61 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Editable details of a relation. +/// +public class RelationEditorVM: IMapped, IVersionable { /// - /// Editable details of a relation. + /// Surrogate ID. /// - public class RelationEditorVM: IMapped, IVersionable - { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// ID of the source page. - /// - public Guid[] SourceIds { get; set; } + /// + /// ID of the source page. + /// + public Guid[] SourceIds { get; set; } - /// - /// ID of the second page. - /// - public Guid? DestinationId { get; set; } + /// + /// ID of the second page. + /// + public Guid? DestinationId { get; set; } - /// - /// ID of the related event. - /// - public Guid? EventId { get; set; } + /// + /// ID of the related event. + /// + public Guid? EventId { get; set; } - /// - /// Type of the relation. - /// - public RelationType Type { get; set; } + /// + /// Type of the relation. + /// + public RelationType Type { get; set; } - /// - /// Relation timespan's start. - /// - public string DurationStart { get; set; } + /// + /// Relation timespan's start. + /// + public string DurationStart { get; set; } - /// - /// Relation timespan's end. - /// - public string DurationEnd { get; set; } + /// + /// Relation timespan's end. + /// + public string DurationEnd { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.SourceIds, x => new[] {x.SourceId}) - .Map(x => x.DestinationId, x => x.DestinationId) - .Map(x => x.EventId, x => x.EventId) - .Map(x => x.Type, x => x.Type) - .Map(x => x.DurationStart, x => FuzzyRange.TrySplit(x.Duration)[0]) - .Map(x => x.DurationEnd, x => FuzzyRange.TrySplit(x.Duration)[1]); + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.SourceIds, x => new[] {x.SourceId}) + .Map(x => x.DestinationId, x => x.DestinationId) + .Map(x => x.EventId, x => x.EventId) + .Map(x => x.Type, x => x.Type) + .Map(x => x.DurationStart, x => FuzzyRange.TrySplit(x.Duration)[0]) + .Map(x => x.DurationEnd, x => FuzzyRange.TrySplit(x.Duration)[1]); - config.NewConfig() - .Map(x => x.Duration, x => FuzzyRange.TryCombine(x.DurationStart, x.DurationEnd)) - .Map(x => x.SourceId, x => x.SourceIds != null && x.SourceIds.Length > 0 ? x.SourceIds[0] : Guid.Empty); - } + config.NewConfig() + .Map(x => x.Duration, x => FuzzyRange.TryCombine(x.DurationStart, x.DurationEnd)) + .Map(x => x.SourceId, x => x.SourceIds != null && x.SourceIds.Length > 0 ? x.SourceIds[0] : Guid.Empty); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationSuggestQueryVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationSuggestQueryVM.cs index 825b1bb0..f509f8d3 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationSuggestQueryVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationSuggestQueryVM.cs @@ -1,36 +1,35 @@ using System; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Collection of arguments for a page lookup in relation editor. +/// +public class RelationSuggestQueryVM { /// - /// Collection of arguments for a page lookup in relation editor. + /// Search query. /// - public class RelationSuggestQueryVM - { - /// - /// Search query. - /// - public string Query { get; set; } + public string Query { get; set; } - /// - /// List of expected page types. - /// - public PageType[] Types { get; set; } + /// + /// List of expected page types. + /// + public PageType[] Types { get; set; } - /// - /// Selected relation type. - /// - public RelationType RelationType { get; set; } + /// + /// Selected relation type. + /// + public RelationType RelationType { get; set; } - /// - /// Selected source page ID (for destination picking). - /// - public Guid? SourceId { get; set; } + /// + /// Selected source page ID (for destination picking). + /// + public Guid? SourceId { get; set; } - /// - /// Selected destination page ID (for source picking). - /// - public Guid? DestinationId { get; set; } - } -} + /// + /// Selected destination page ID (for source picking). + /// + public Guid? DestinationId { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationTitleVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationTitleVM.cs index 6e633366..858fb57f 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationTitleVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationTitleVM.cs @@ -5,52 +5,51 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Brief information about a relation between two pages. +/// +public class RelationTitleVM: IMapped { /// - /// Brief information about a relation between two pages. + /// Surrogate ID. /// - public class RelationTitleVM: IMapped - { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// The first entity in the relation. - /// - public PageTitleExtendedVM Source { get; set; } + /// + /// The first entity in the relation. + /// + public PageTitleExtendedVM Source { get; set; } - /// - /// The second entity in the relation. - /// - public PageTitleExtendedVM Destination { get; set; } + /// + /// The second entity in the relation. + /// + public PageTitleExtendedVM Destination { get; set; } - /// - /// Related event (e.g. wedding). - /// - public PageTitleExtendedVM Event { get; set; } + /// + /// Related event (e.g. wedding). + /// + public PageTitleExtendedVM Event { get; set; } - /// - /// Type of the relation. - /// - public RelationType Type { get; set; } + /// + /// Type of the relation. + /// + public RelationType Type { get; set; } - /// - /// Timespan of the relation. - /// - public FuzzyRange? Duration { get; set; } + /// + /// Timespan of the relation. + /// + public FuzzyRange? Duration { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Type, x => x.Type) - .Map(x => x.Source, x => x.Source) - .Map(x => x.Destination, x => x.Destination) - .Map(x => x.Event, x => x.Event) - .Map(x => x.Duration, x => FuzzyRange.TryParse(x.Duration)); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Type, x => x.Type) + .Map(x => x.Source, x => x.Source) + .Map(x => x.Destination, x => x.Destination) + .Map(x => x.Event, x => x.Event) + .Map(x => x.Duration, x => FuzzyRange.TryParse(x.Duration)); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListDataVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListDataVM.cs index 4de874d5..69ad26fb 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListDataVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListDataVM.cs @@ -1,13 +1,12 @@ -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Additional data for relation filters. +/// +public class RelationsListDataVM { /// - /// Additional data for relation filters. + /// Title of the filtered entity. /// - public class RelationsListDataVM - { - /// - /// Title of the filtered entity. - /// - public string EntityTitle { get; set; } - } -} + public string EntityTitle { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListRequestVM.cs index a1da2c85..52905f72 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListRequestVM.cs @@ -2,31 +2,30 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Request for finding relations. +/// +public class RelationsListRequestVM: ListRequestVM { /// - /// Request for finding relations. + /// Filter by entity. /// - public class RelationsListRequestVM: ListRequestVM - { - /// - /// Filter by entity. - /// - public Guid? EntityId { get; set; } + public Guid? EntityId { get; set; } - /// - /// Allowed types of relations. - /// - public RelationType[] Types { get; set; } + /// + /// Allowed types of relations. + /// + public RelationType[] Types { get; set; } - /// - /// Checks if the request has no filter applied. - /// - public override bool IsEmpty() - { - return base.IsEmpty() - && EntityId == null - && (Types == null || Types.Length == 0); - } + /// + /// Checks if the request has no filter applied. + /// + public override bool IsEmpty() + { + return base.IsEmpty() + && EntityId == null + && (Types == null || Types.Length == 0); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListVM.cs index 3bf6c634..4d78db16 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Relations/RelationsListVM.cs @@ -1,15 +1,14 @@ using Bonsai.Areas.Admin.ViewModels.Common; -namespace Bonsai.Areas.Admin.ViewModels.Relations +namespace Bonsai.Areas.Admin.ViewModels.Relations; + +/// +/// Found relations. +/// +public class RelationsListVM: ListResultVM { /// - /// Found relations. + /// Title of the page to filter by. /// - public class RelationsListVM: ListResultVM - { - /// - /// Title of the page to filter by. - /// - public string EntityTitle { get; set; } - } -} + public string EntityTitle { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeLayoutVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeLayoutVM.cs index b7550ac5..684c35a4 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeLayoutVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeLayoutVM.cs @@ -1,26 +1,25 @@ using System; using System.Collections.Generic; -namespace Bonsai.Areas.Admin.ViewModels.Tree +namespace Bonsai.Areas.Admin.ViewModels.Tree; + +/// +/// Contents of a family subtree. +/// +public class TreeLayoutVM { /// - /// Contents of a family subtree. + /// Related page (for partial trees only). /// - public class TreeLayoutVM - { - /// - /// Related page (for partial trees only). - /// - public Guid? PageId { get; set; } + public Guid? PageId { get; set; } - /// - /// All available spouse relations. - /// - public IReadOnlyList Relations { get; set; } + /// + /// All available spouse relations. + /// + public IReadOnlyList Relations { get; set; } - /// - /// All known persons. - /// - public IReadOnlyList Persons { get; set; } - } -} + /// + /// All known persons. + /// + public IReadOnlyList Persons { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreePersonVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreePersonVM.cs index 4a384f41..8d012f8e 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreePersonVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreePersonVM.cs @@ -1,58 +1,57 @@ -namespace Bonsai.Areas.Admin.ViewModels.Tree +namespace Bonsai.Areas.Admin.ViewModels.Tree; + +/// +/// A person displayed on the tree. +/// +public class TreePersonVM { /// - /// A person displayed on the tree. - /// - public class TreePersonVM - { - /// - /// ID of the person. - /// - public string Id { get; set; } - - /// - /// Person's full name. - /// - public string Name { get; set; } - - /// - /// Maiden name of the person (if any). - /// - public string MaidenName { get; set; } - - /// - /// Gender flag. - /// - public bool IsMale { get; set; } - - /// - /// Date of birth. - /// - public string Birth { get; set; } - - /// - /// Date of death. - /// - public string Death { get; set; } - - /// - /// Flag indicating that the person is dead (even if the date is unknown). - /// - public bool IsDead { get; set; } - - /// - /// URL to the photo. - /// - public string Photo { get; set; } - - /// - /// URL to the page. - /// - public string Url { get; set; } - - /// - /// ID of the parent relation. - /// - public string Parents { get; set; } - } -} + /// ID of the person. + /// + public string Id { get; set; } + + /// + /// Person's full name. + /// + public string Name { get; set; } + + /// + /// Maiden name of the person (if any). + /// + public string MaidenName { get; set; } + + /// + /// Gender flag. + /// + public bool IsMale { get; set; } + + /// + /// Date of birth. + /// + public string Birth { get; set; } + + /// + /// Date of death. + /// + public string Death { get; set; } + + /// + /// Flag indicating that the person is dead (even if the date is unknown). + /// + public bool IsDead { get; set; } + + /// + /// URL to the photo. + /// + public string Photo { get; set; } + + /// + /// URL to the page. + /// + public string Url { get; set; } + + /// + /// ID of the parent relation. + /// + public string Parents { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeRelationVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeRelationVM.cs index d2afb79b..751b6d80 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeRelationVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Tree/TreeRelationVM.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Areas.Admin.ViewModels.Tree +namespace Bonsai.Areas.Admin.ViewModels.Tree; + +/// +/// Spouse relation. +/// +public class TreeRelationVM { /// - /// Spouse relation. + /// Relation's unique ID. /// - public class TreeRelationVM - { - /// - /// Relation's unique ID. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// ID of the first related person. - /// - public string From { get; set; } + /// + /// ID of the first related person. + /// + public string From { get; set; } - /// - /// ID of the second related person. - /// - public string To { get; set; } - } -} + /// + /// ID of the second related person. + /// + public string To { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/IPasswordForm.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/IPasswordForm.cs index e8e590e3..e8466263 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/IPasswordForm.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/IPasswordForm.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// Common interface for password-related VMs. +/// +interface IPasswordForm { /// - /// Common interface for password-related VMs. + /// Password. /// - interface IPasswordForm - { - /// - /// Password. - /// - string Password { get; } + string Password { get; } - /// - /// Password copy for typo checking. - /// - string PasswordCopy { get; } - } -} + /// + /// Password copy for typo checking. + /// + string PasswordCopy { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/RemoveUserVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/RemoveUserVM.cs index e0659084..63ddc1c8 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/RemoveUserVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/RemoveUserVM.cs @@ -1,29 +1,28 @@ -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// Information about a user removal request. +/// +public class RemoveUserVM { /// - /// Information about a user removal request. + /// Surrogate ID. /// - public class RemoveUserVM - { - /// - /// Surrogate ID. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// User's full name (for clarification). - /// - public string FullName { get; set; } + /// + /// User's full name (for clarification). + /// + public string FullName { get; set; } - /// - /// Flag indicating that user attempts to remove their own account. - /// - public bool IsSelf { get; set; } + /// + /// Flag indicating that user attempts to remove their own account. + /// + public bool IsSelf { get; set; } - /// - /// Flag indicating that the user can be removed completely. - /// Otherwise, the account is only locked. - /// - public bool IsFullyDeletable { get; set; } - } -} + /// + /// Flag indicating that the user can be removed completely. + /// Otherwise, the account is only locked. + /// + public bool IsFullyDeletable { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UserCreatorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UserCreatorVM.cs index ba9a53a2..2b47c1e0 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UserCreatorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UserCreatorVM.cs @@ -4,37 +4,36 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// VM for creating a new password-authorized user. +/// +public class UserCreatorVM : RegisterUserVM, IPasswordForm { /// - /// VM for creating a new password-authorized user. + /// Assigned role. /// - public class UserCreatorVM : RegisterUserVM, IPasswordForm - { - /// - /// Assigned role. - /// - public UserRole Role { get; set; } + public UserRole Role { get; set; } - /// - /// ID of the personal page. - /// - public Guid? PersonalPageId { get; set; } + /// + /// ID of the personal page. + /// + public Guid? PersonalPageId { get; set; } - /// - /// Configures automatic mapping. - /// - public override void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Birthday, x => x.Birthday) - .Map(x => x.FirstName, x => x.FirstName) - .Map(x => x.MiddleName, x => x.MiddleName) - .Map(x => x.LastName, x => x.LastName) - .Map(x => x.Email, x => x.Email) - .Map(x => x.PageId, x => x.PersonalPageId) - .Map(x => x.UserName, x => Regex.Replace(x.Email, "[^a-z0-9]", "")) - .IgnoreNonMapped(true); - } + /// + /// Configures automatic mapping. + /// + public override void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Birthday, x => x.Birthday) + .Map(x => x.FirstName, x => x.FirstName) + .Map(x => x.MiddleName, x => x.MiddleName) + .Map(x => x.LastName, x => x.LastName) + .Map(x => x.Email, x => x.Email) + .Map(x => x.PageId, x => x.PersonalPageId) + .Map(x => x.UserName, x => Regex.Replace(x.Email, "[^a-z0-9]", "")) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorDataVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorDataVM.cs index 06a670e5..36b6422d 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorDataVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorDataVM.cs @@ -1,31 +1,30 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// Additional data for the user editor page. +/// +public class UserEditorDataVM { /// - /// Additional data for the user editor page. + /// Flag indicating that an admin is editing their own profile. /// - public class UserEditorDataVM - { - /// - /// Flag indicating that an admin is editing their own profile. - /// - public bool IsSelf { get; set; } + public bool IsSelf { get; set; } - /// - /// Flag indicating that a personal page does not exist yet for this profile. - /// - public bool CanCreatePersonalPage { get; set; } + /// + /// Flag indicating that a personal page does not exist yet for this profile. + /// + public bool CanCreatePersonalPage { get; set; } - /// - /// Available roles for the user. - /// - public IReadOnlyList UserRoleItems { get; set; } + /// + /// Available roles for the user. + /// + public IReadOnlyList UserRoleItems { get; set; } - /// - /// Selected page (if any). - /// - public IReadOnlyList PageItems { get; set; } - } -} + /// + /// Selected page (if any). + /// + public IReadOnlyList PageItems { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorVM.cs index 8c96f1e7..838e3236 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UserEditorVM.cs @@ -5,67 +5,66 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// VM for editing a user. +/// +public class UserEditorVM: RegisterUserVM { /// - /// VM for editing a user. + /// Surrogate user ID. /// - public class UserEditorVM: RegisterUserVM - { - /// - /// Surrogate user ID. - /// - [Required] - public string Id { get; set; } + [Required] + public string Id { get; set; } - /// - /// Flag indicating user's current validation state. - /// - public bool IsValidated { get; set; } - - /// - /// Assigned role. - /// - public UserRole Role { get; set; } + /// + /// Flag indicating user's current validation state. + /// + public bool IsValidated { get; set; } - /// - /// ID of the personal page. - /// - public Guid? PersonalPageId { get; set; } + /// + /// Assigned role. + /// + public UserRole Role { get; set; } - /// - /// Flag indicating that the user is in lockout mode. - /// - public bool IsLocked { get; set; } + /// + /// ID of the personal page. + /// + public Guid? PersonalPageId { get; set; } - /// - /// Configures automatic mapping. - /// - public override void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Birthday, x => x.Birthday) - .Map(x => x.FirstName, x => x.FirstName) - .Map(x => x.MiddleName, x => x.MiddleName) - .Map(x => x.LastName, x => x.LastName) - .Map(x => x.Email, x => x.Email) - .Map(x => x.NormalizedEmail, x => x.Email.ToUpperInvariant()) - .Map(x => x.PageId, x => x.PersonalPageId) - .Map(x => x.UserName, x => ClearEmail(x.Email)) - .Map(x => x.NormalizedUserName, x => ClearEmail(x.Email).ToUpperInvariant()); + /// + /// Flag indicating that the user is in lockout mode. + /// + public bool IsLocked { get; set; } - config.NewConfig() - .Map(x => x.Birthday, x => x.Birthday) - .Map(x => x.FirstName, x => x.FirstName) - .Map(x => x.MiddleName, x => x.MiddleName) - .Map(x => x.LastName, x => x.LastName) - .Map(x => x.Email, x => x.Email) - .Map(x => x.PersonalPageId, x => x.PageId) - .Map(x => x.IsLocked, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.Now) - .Map(x => x.IsValidated, x => x.IsValidated) - .IgnoreNonMapped(true); - } + /// + /// Configures automatic mapping. + /// + public override void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Birthday, x => x.Birthday) + .Map(x => x.FirstName, x => x.FirstName) + .Map(x => x.MiddleName, x => x.MiddleName) + .Map(x => x.LastName, x => x.LastName) + .Map(x => x.Email, x => x.Email) + .Map(x => x.NormalizedEmail, x => x.Email.ToUpperInvariant()) + .Map(x => x.PageId, x => x.PersonalPageId) + .Map(x => x.UserName, x => ClearEmail(x.Email)) + .Map(x => x.NormalizedUserName, x => ClearEmail(x.Email).ToUpperInvariant()); - private static string ClearEmail(string email) => Regex.Replace(email, "[^a-z0-9]", ""); + config.NewConfig() + .Map(x => x.Birthday, x => x.Birthday) + .Map(x => x.FirstName, x => x.FirstName) + .Map(x => x.MiddleName, x => x.MiddleName) + .Map(x => x.LastName, x => x.LastName) + .Map(x => x.Email, x => x.Email) + .Map(x => x.PersonalPageId, x => x.PageId) + .Map(x => x.IsLocked, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.Now) + .Map(x => x.IsValidated, x => x.IsValidated) + .IgnoreNonMapped(true); } -} + + private static string ClearEmail(string email) => Regex.Replace(email, "[^a-z0-9]", ""); +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UserPasswordEditorVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UserPasswordEditorVM.cs index 47718a0e..0ae86f8b 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UserPasswordEditorVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UserPasswordEditorVM.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// VM for updating a user's password. +/// +public class UserPasswordEditorVM: IPasswordForm { /// - /// VM for updating a user's password. + /// ID of the user. /// - public class UserPasswordEditorVM: IPasswordForm - { - /// - /// ID of the user. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Password. - /// - public string Password { get; set; } + /// + /// Password. + /// + public string Password { get; set; } - /// - /// Password copy for typo checking. - /// - public string PasswordCopy { get; set; } - } -} + /// + /// Password copy for typo checking. + /// + public string PasswordCopy { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UserTitleVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UserTitleVM.cs index 6b32f976..fa058e44 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UserTitleVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UserTitleVM.cs @@ -3,63 +3,62 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// Brief information about the user. +/// +public class UserTitleVM: IMapped { /// - /// Brief information about the user. + /// Surrogate ID. /// - public class UserTitleVM: IMapped - { - /// - /// Surrogate ID. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Readable name of the user. - /// - public string FullName { get; set; } + /// + /// Readable name of the user. + /// + public string FullName { get; set; } - /// - /// Email address. - /// - public string Email { get; set; } + /// + /// Email address. + /// + public string Email { get; set; } - /// - /// Flag indicating that the user is locally authorized with a login and password. - /// - public AuthType AuthType { get; set; } + /// + /// Flag indicating that the user is locally authorized with a login and password. + /// + public AuthType AuthType { get; set; } - /// - /// The user's role with richest access level. - /// - public UserRole Role { get; set; } + /// + /// The user's role with richest access level. + /// + public UserRole Role { get; set; } - /// - /// Date of the current lockout. - /// - public DateTimeOffset? LockoutEnd { get; set; } + /// + /// Date of the current lockout. + /// + public DateTimeOffset? LockoutEnd { get; set; } - /// - /// ID of the related page (if any). - /// - public Guid? PageId { get; set; } + /// + /// ID of the related page (if any). + /// + public Guid? PageId { get; set; } - /// - /// Flag indicating the user's gender. - /// - public bool? IsMale { get; set; } + /// + /// Flag indicating the user's gender. + /// + public bool? IsMale { get; set; } - public void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Email, x => x.Email) - .Map(x => x.FullName, y => y.FirstName + " " + y.LastName) - .Map(x => x.AuthType, x => x.AuthType) - .Map(x => x.LockoutEnd, x => x.LockoutEnd) - .Map(x => x.PageId, x => x.PageId) - .Ignore(x => x.Role, x => x.IsMale); - } + public void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Email, x => x.Email) + .Map(x => x.FullName, y => y.FirstName + " " + y.LastName) + .Map(x => x.AuthType, x => x.AuthType) + .Map(x => x.LockoutEnd, x => x.LockoutEnd) + .Map(x => x.PageId, x => x.PageId) + .Ignore(x => x.Role, x => x.IsMale); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListRequestVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListRequestVM.cs index 63d91031..dd5cb8a8 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListRequestVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListRequestVM.cs @@ -1,16 +1,15 @@ using Bonsai.Areas.Admin.ViewModels.Common; using Bonsai.Data.Models; -namespace Bonsai.Areas.Admin.ViewModels.Users +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// Request for filtering users. +/// +public class UsersListRequestVM: ListRequestVM { /// - /// Request for filtering users. + /// List of selected page types. /// - public class UsersListRequestVM: ListRequestVM - { - /// - /// List of selected page types. - /// - public UserRole[] Roles { get; set; } - } -} + public UserRole[] Roles { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListVM.cs b/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListVM.cs index 5ae115af..701d998c 100644 --- a/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListVM.cs +++ b/src/Bonsai/Areas/Admin/ViewModels/Users/UsersListVM.cs @@ -1,11 +1,8 @@ using Bonsai.Areas.Admin.ViewModels.Common; -namespace Bonsai.Areas.Admin.ViewModels.Users -{ - /// - /// List of users. - /// - public class UsersListVM: ListResultVM - { - } -} +namespace Bonsai.Areas.Admin.ViewModels.Users; + +/// +/// List of users. +/// +public class UsersListVM: ListResultVM; \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Components/PageImageTagHelper.cs b/src/Bonsai/Areas/Front/Components/PageImageTagHelper.cs index ac9796f8..4785f434 100644 --- a/src/Bonsai/Areas/Front/Components/PageImageTagHelper.cs +++ b/src/Bonsai/Areas/Front/Components/PageImageTagHelper.cs @@ -5,59 +5,51 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Razor.TagHelpers; -namespace Bonsai.Areas.Front.Components +namespace Bonsai.Areas.Front.Components; + +/// +/// Displays a photograph for a page. +/// +[HtmlTargetElement("page-image", Attributes = "image")] +public class PageImageTagHelper(IUrlHelper url) : TagHelper { /// - /// Displays a photograph for a page. + /// Path to the image. + /// + public string Image { get; set; } + + /// + /// Path to a fallback image (if the original one is not set). + /// + public string FallbackImage { get; set; } + + /// + /// Type of the entity. + /// + public PageType? Type { get; set; } + + /// + /// Size of the desired image. + /// + public MediaSize? Size { get; set; } + + /// + /// Renders the tag. /// - [HtmlTargetElement("page-image", Attributes = "image")] - public class PageImageTagHelper: TagHelper + public override void Process(TagHelperContext context, TagHelperOutput output) { - public PageImageTagHelper(IUrlHelper url) - { - _url = url; - } - - private readonly IUrlHelper _url; - - /// - /// Path to the image. - /// - public string Image { get; set; } - - /// - /// Path to a fallback image (if the original one is not set). - /// - public string FallbackImage { get; set; } - - /// - /// Type of the entity. - /// - public PageType? Type { get; set; } - - /// - /// Size of the desired image. - /// - public MediaSize? Size { get; set; } - - /// - /// Renders the tag. - /// - public override void Process(TagHelperContext context, TagHelperOutput output) - { - var path = _url.Content(PageHelper.GetPageImageUrl(Type, Image, FallbackImage, Size)); - var className = "image"; + var path = url.Content(PageHelper.GetPageImageUrl(Type, Image, FallbackImage, Size)); + var className = "image"; - output.TagName = "div"; - output.TagMode = TagMode.StartTagAndEndTag; + output.TagName = "div"; + output.TagMode = TagMode.StartTagAndEndTag; - var existingClass = output.Attributes.FirstOrDefault(x => x.Name == "class")?.Value; - if (existingClass == null) - output.Attributes.Add("class", className); - else - output.Attributes.SetAttribute("class", $"{className} {existingClass}"); + var existingClass = output.Attributes.FirstOrDefault(x => x.Name == "class")?.Value; + if (existingClass == null) + output.Attributes.Add("class", className); + else + output.Attributes.SetAttribute("class", $"{className} {existingClass}"); - output.Attributes.Add("style", $"background-image: url({path})"); - } + output.Attributes.Add("style", $"background-image: url({path})"); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Components/UserHeaderComponent.cs b/src/Bonsai/Areas/Front/Components/UserHeaderComponent.cs index ae73ca91..31b7abde 100644 --- a/src/Bonsai/Areas/Front/Components/UserHeaderComponent.cs +++ b/src/Bonsai/Areas/Front/Components/UserHeaderComponent.cs @@ -2,28 +2,20 @@ using Bonsai.Areas.Front.Logic.Auth; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Components +namespace Bonsai.Areas.Front.Components; + +/// +/// The component for displaying the user bar. +/// +public class UserHeaderComponent(AuthService auth) : ViewComponent { /// - /// The component for displaying the user bar. + /// Displays the user info bar in the header. /// - public class UserHeaderComponent: ViewComponent + public async Task InvokeAsync() { - public UserHeaderComponent(AuthService auth) - { - _auth = auth; - } - - private readonly AuthService _auth; - - /// - /// Displays the user info bar in the header. - /// - public async Task InvokeAsync() - { - var user = await _auth.GetCurrentUserAsync(HttpContext.User); - ViewBag.ReturnUrl = Request.Path.ToString(); - return View("~/Areas/Front/Views/Components/UserHeader.cshtml", user); - } + var user = await auth.GetCurrentUserAsync(HttpContext.User); + ViewBag.ReturnUrl = Request.Path.ToString(); + return View("~/Areas/Front/Views/Components/UserHeader.cshtml", user); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/AuthController.cs b/src/Bonsai/Areas/Front/Controllers/AuthController.cs index 440dc7da..b1b6d2fe 100644 --- a/src/Bonsai/Areas/Front/Controllers/AuthController.cs +++ b/src/Bonsai/Areas/Front/Controllers/AuthController.cs @@ -16,292 +16,291 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// Controller for registration/authorization pages. +/// +[Area("Front")] +[Route("auth")] +public class AuthController: AppControllerBase { + public AuthController( + AuthService auth, + AuthProviderService provs, + PagesManagerService pages, + ISearchEngine search, + BonsaiConfigService cfgProvider, + IBackgroundJobService jobs, + AppDbContext db + ) + { + _auth = auth; + _provs = provs; + _pages = pages; + _search = search; + _cfgProvider = cfgProvider; + _jobs = jobs; + _db = db; + } + + private readonly AuthService _auth; + private readonly AuthProviderService _provs; + private readonly PagesManagerService _pages; + private readonly ISearchEngine _search; + private readonly BonsaiConfigService _cfgProvider; + private readonly IBackgroundJobService _jobs; + private readonly AppDbContext _db; + /// - /// Controller for registration/authorization pages. + /// Displays the authorization page. /// - [Area("Front")] - [Route("auth")] - public class AuthController: AppControllerBase + [HttpGet] + [Route("login")] + public async Task Login(string returnUrl = null) { - public AuthController( - AuthService auth, - AuthProviderService provs, - PagesManagerService pages, - ISearchEngine search, - BonsaiConfigService cfgProvider, - IBackgroundJobService jobs, - AppDbContext db - ) + if (await _auth.IsFirstUserAsync()) + return RedirectToAction("Register"); + + var user = await _auth.GetCurrentUserAsync(User); + var status = user?.IsValidated switch { - _auth = auth; - _provs = provs; - _pages = pages; - _search = search; - _cfgProvider = cfgProvider; - _jobs = jobs; - _db = db; - } + true => LoginStatus.Succeeded, + false => LoginStatus.Unvalidated, + null => (LoginStatus?) null + }; + if (status == LoginStatus.Succeeded) + return RedirectToAction("Index", "Home"); - private readonly AuthService _auth; - private readonly AuthProviderService _provs; - private readonly PagesManagerService _pages; - private readonly ISearchEngine _search; - private readonly BonsaiConfigService _cfgProvider; - private readonly IBackgroundJobService _jobs; - private readonly AppDbContext _db; - - /// - /// Displays the authorization page. - /// - [HttpGet] - [Route("login")] - public async Task Login(string returnUrl = null) - { - if (await _auth.IsFirstUserAsync()) - return RedirectToAction("Register"); - - var user = await _auth.GetCurrentUserAsync(User); - var status = user?.IsValidated switch - { - true => LoginStatus.Succeeded, - false => LoginStatus.Unvalidated, - null => (LoginStatus?) null - }; - if (status == LoginStatus.Succeeded) - return RedirectToAction("Index", "Home"); - - return await ViewLoginFormAsync(status, returnUrl); - } + return await ViewLoginFormAsync(status, returnUrl); + } - /// - /// Sends the authorization request. - /// - [HttpPost] - [Route("externalLogin")] - public ActionResult ExternalLogin(string provider, string returnUrl) + /// + /// Sends the authorization request. + /// + [HttpPost] + [Route("externalLogin")] + public ActionResult ExternalLogin(string provider, string returnUrl) + { + var redirectUrl = Url.Action("LoginCallback", new {returnUrl = returnUrl}); + var authProps = new AuthenticationProperties { - var redirectUrl = Url.Action("LoginCallback", new {returnUrl = returnUrl}); - var authProps = new AuthenticationProperties - { - AllowRefresh = true, - IsPersistent = true, - RedirectUri = redirectUrl, - Items = { ["LoginProvider"] = provider } - }; - return Challenge(authProps, provider); - } + AllowRefresh = true, + IsPersistent = true, + RedirectUri = redirectUrl, + Items = { ["LoginProvider"] = provider } + }; + return Challenge(authProps, provider); + } - /// - /// Attempts to authorize the user via a login-password pair. - /// - [HttpPost] - [Route("login")] - public async Task Login(LocalLoginVM vm) - { - var result = await _auth.LocalLoginAsync(vm); + /// + /// Attempts to authorize the user via a login-password pair. + /// + [HttpPost] + [Route("login")] + public async Task Login(LocalLoginVM vm) + { + var result = await _auth.LocalLoginAsync(vm); - if (result.Status == LoginStatus.Succeeded) - return RedirectLocal(vm.ReturnUrl); + if (result.Status == LoginStatus.Succeeded) + return RedirectLocal(vm.ReturnUrl); - return await ViewLoginFormAsync(result.Status); - } + return await ViewLoginFormAsync(result.Status); + } - /// - /// Logs the user out. - /// - [HttpGet] - [Route("logout")] - public async Task Logout() - { - await _auth.LogoutAsync(); - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - return RedirectToAction("Index", "Home", new { area = "Front" }); - } + /// + /// Logs the user out. + /// + [HttpGet] + [Route("logout")] + public async Task Logout() + { + await _auth.LogoutAsync(); + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + return RedirectToAction("Index", "Home", new { area = "Front" }); + } - /// - /// Invoked by external login provider when the authorization is successful. - /// - [Route("loginCallback")] - public async Task LoginCallback(string returnUrl) - { - var result = await _auth.ExternalLoginAsync(); + /// + /// Invoked by external login provider when the authorization is successful. + /// + [Route("loginCallback")] + public async Task LoginCallback(string returnUrl) + { + var result = await _auth.ExternalLoginAsync(); - if (result.Status == LoginStatus.Succeeded) - return RedirectLocal(returnUrl); + if (result.Status == LoginStatus.Succeeded) + return RedirectLocal(returnUrl); - if (result.Status == LoginStatus.NewUser) + if (result.Status == LoginStatus.NewUser) + { + Session.Set(new RegistrationInfo { - Session.Set(new RegistrationInfo - { - FormData = _auth.GetRegistrationData(result.Principal), - Login = result.ExternalLogin - }); - return RedirectToAction("Register"); - } - - HttpContext.User = result.Principal; - - return await ViewLoginFormAsync(result.Status, returnUrl); + FormData = _auth.GetRegistrationData(result.Principal), + Login = result.ExternalLogin + }); + return RedirectToAction("Register"); } - /// - /// Displays the user registration form. - /// - [HttpGet] - [Route("register")] - public async Task Register() - { - var user = await _auth.GetCurrentUserAsync(User); - if (user != null) - { - return user.IsValidated - ? RedirectToAction("Index", "Home") - : RedirectToAction("Login"); - } + HttpContext.User = result.Principal; - if (!await CanRegisterAsync()) - return View("RegisterDisabled"); + return await ViewLoginFormAsync(result.Status, returnUrl); + } - var extAuth = Session.Get(); - var vm = extAuth?.FormData ?? new RegisterUserVM(); - vm.CreatePersonalPage = true; - return await ViewRegisterFormAsync(vm, usesPasswordAuth: extAuth == null); + /// + /// Displays the user registration form. + /// + [HttpGet] + [Route("register")] + public async Task Register() + { + var user = await _auth.GetCurrentUserAsync(User); + if (user != null) + { + return user.IsValidated + ? RedirectToAction("Index", "Home") + : RedirectToAction("Login"); } - /// - /// Displays the user registration form. - /// - [HttpPost] - [Route("register")] - public async Task Register(RegisterUserVM vm) - { - if (!await CanRegisterAsync()) - return View("RegisterDisabled"); + if (!await CanRegisterAsync()) + return View("RegisterDisabled"); - var info = Session.Get(); + var extAuth = Session.Get(); + var vm = extAuth?.FormData ?? new RegisterUserVM(); + vm.CreatePersonalPage = true; + return await ViewRegisterFormAsync(vm, usesPasswordAuth: extAuth == null); + } - if(!ModelState.IsValid) - return await ViewRegisterFormAsync(vm, usesPasswordAuth: info == null); + /// + /// Displays the user registration form. + /// + [HttpPost] + [Route("register")] + public async Task Register(RegisterUserVM vm) + { + if (!await CanRegisterAsync()) + return View("RegisterDisabled"); - try - { - var result = await _auth.RegisterAsync(vm, info?.Login); - if (!result.IsValidated) - return RedirectToAction("RegisterSuccess", "Auth"); + var info = Session.Get(); - if (vm.CreatePersonalPage) - { - _db.Entry(result.User).State = EntityState.Unchanged; - result.User.Page = await _pages.CreateDefaultUserPageAsync(vm, result.Principal); - await _db.SaveChangesAsync(); + if(!ModelState.IsValid) + return await ViewRegisterFormAsync(vm, usesPasswordAuth: info == null); - await _search.AddPageAsync(result.User.Page); - await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); - } + try + { + var result = await _auth.RegisterAsync(vm, info?.Login); + if (!result.IsValidated) + return RedirectToAction("RegisterSuccess", "Auth"); - return RedirectToAction("Index", "Home"); - } - catch (ValidationException ex) + if (vm.CreatePersonalPage) { - SetModelState(ex); + _db.Entry(result.User).State = EntityState.Unchanged; + result.User.Page = await _pages.CreateDefaultUserPageAsync(vm, result.Principal); + await _db.SaveChangesAsync(); - return await ViewRegisterFormAsync(vm, usesPasswordAuth: info == null); + await _search.AddPageAsync(result.User.Page); + await _jobs.RunAsync(JobBuilder.For().SupersedeAll()); } - } - /// - /// Displays the "Registration success" page. - /// - [Route("registerSuccess")] - public ActionResult RegisterSuccess() + return RedirectToAction("Index", "Home"); + } + catch (ValidationException ex) { - if (Session.Get() == null) - return RedirectToAction("Index", "Dashboard", new { area = "Admin" }); + SetModelState(ex); - Session.Remove(); - - return View(); + return await ViewRegisterFormAsync(vm, usesPasswordAuth: info == null); } + } - /// - /// Displays a message when external auth has failed. - /// - [HttpGet] - [Route("failed")] - public ActionResult Failed() - { - return View(); - } + /// + /// Displays the "Registration success" page. + /// + [Route("registerSuccess")] + public ActionResult RegisterSuccess() + { + if (Session.Get() == null) + return RedirectToAction("Index", "Dashboard", new { area = "Admin" }); - #region Private helpers + Session.Remove(); - /// - /// Checks if the redirect is a local page. - /// - private ActionResult RedirectLocal(string returnUrl) - { - if (!string.IsNullOrEmpty(returnUrl)) - { - var currHost = HttpContext.Request.Host.Host.ToLower(); - var canRedirect = Url.IsLocalUrl(returnUrl) - || new Uri(returnUrl, UriKind.Absolute).Host.Contains(currHost); + return View(); + } - if (canRedirect) - return Redirect(returnUrl); - } + /// + /// Displays a message when external auth has failed. + /// + [HttpGet] + [Route("failed")] + public ActionResult Failed() + { + return View(); + } - return RedirectToAction("Index", "Home"); - } + #region Private helpers - /// - /// Displays the registration form. - /// - private async Task ViewRegisterFormAsync(RegisterUserVM vm, bool usesPasswordAuth) + /// + /// Checks if the redirect is a local page. + /// + private ActionResult RedirectLocal(string returnUrl) + { + if (!string.IsNullOrEmpty(returnUrl)) { - ViewBag.Data = new RegisterUserDataVM - { - IsFirstUser = await _auth.IsFirstUserAsync(), - UsePasswordAuth = usesPasswordAuth - }; + var currHost = HttpContext.Request.Host.Host.ToLower(); + var canRedirect = Url.IsLocalUrl(returnUrl) + || new Uri(returnUrl, UriKind.Absolute).Host.Contains(currHost); - return View("RegisterForm", vm); + if (canRedirect) + return Redirect(returnUrl); } - /// - /// Displays the login page. - /// - private async Task ViewLoginFormAsync(LoginStatus? status, string returnUrl = null) + return RedirectToAction("Index", "Home"); + } + + /// + /// Displays the registration form. + /// + private async Task ViewRegisterFormAsync(RegisterUserVM vm, bool usesPasswordAuth) + { + ViewBag.Data = new RegisterUserDataVM { - var dynCfg = _cfgProvider.GetDynamicConfig(); - ViewBag.Data = new LoginDataVM - { - ReturnUrl = returnUrl, - AllowGuests = dynCfg.AllowGuests, - AllowRegistration = dynCfg.AllowRegistration, - AllowPasswordAuth = _cfgProvider.GetStaticConfig().Auth.AllowPasswordAuth, - Providers = _provs.AvailableProviders, - IsFirstUser = await _auth.IsFirstUserAsync(), - Status = status - }; - return View("Login", new LocalLoginVM()); - } + IsFirstUser = await _auth.IsFirstUserAsync(), + UsePasswordAuth = usesPasswordAuth + }; + + return View("RegisterForm", vm); + } - /// - /// Checks if the registration is allowed. - /// - private async Task CanRegisterAsync() + /// + /// Displays the login page. + /// + private async Task ViewLoginFormAsync(LoginStatus? status, string returnUrl = null) + { + var dynCfg = _cfgProvider.GetDynamicConfig(); + ViewBag.Data = new LoginDataVM { - if (_cfgProvider.GetDynamicConfig().AllowRegistration) - return true; + ReturnUrl = returnUrl, + AllowGuests = dynCfg.AllowGuests, + AllowRegistration = dynCfg.AllowRegistration, + AllowPasswordAuth = _cfgProvider.GetStaticConfig().Auth.AllowPasswordAuth, + Providers = _provs.AvailableProviders, + IsFirstUser = await _auth.IsFirstUserAsync(), + Status = status + }; + return View("Login", new LocalLoginVM()); + } - if (await _auth.IsFirstUserAsync()) - return true; + /// + /// Checks if the registration is allowed. + /// + private async Task CanRegisterAsync() + { + if (_cfgProvider.GetDynamicConfig().AllowRegistration) + return true; - return false; - } + if (await _auth.IsFirstUserAsync()) + return true; - #endregion + return false; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/CalendarController.cs b/src/Bonsai/Areas/Front/Controllers/CalendarController.cs index 39580385..8d55e92f 100644 --- a/src/Bonsai/Areas/Front/Controllers/CalendarController.cs +++ b/src/Bonsai/Areas/Front/Controllers/CalendarController.cs @@ -5,41 +5,40 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// The controller for calendar-related views. +/// +[Area("front")] +[Route("util/cal")] +[Authorize(Policy = AuthRequirement.Name)] +public class CalendarController: AppControllerBase { - /// - /// The controller for calendar-related views. - /// - [Area("front")] - [Route("util/cal")] - [Authorize(Policy = AuthRequirement.Name)] - public class CalendarController: AppControllerBase + public CalendarController(CalendarPresenterService calendar) { - public CalendarController(CalendarPresenterService calendar) - { - _calendar = calendar; - } + _calendar = calendar; + } - private readonly CalendarPresenterService _calendar; + private readonly CalendarPresenterService _calendar; - /// - /// Displays the calendar grid. - /// - [Route("grid")] - public async Task MonthGrid([FromQuery] int year, [FromQuery] int month) - { - var vm = await _calendar.GetMonthEventsAsync(year, month); - return View(vm); - } + /// + /// Displays the calendar grid. + /// + [Route("grid")] + public async Task MonthGrid([FromQuery] int year, [FromQuery] int month) + { + var vm = await _calendar.GetMonthEventsAsync(year, month); + return View(vm); + } - /// - /// Displays the list of events for a particular day. - /// - [Route("list")] - public async Task DayList([FromQuery] int year, [FromQuery] int month, [FromQuery] int? day = null) - { - var vm = await _calendar.GetDayEventsAsync(year, month, day); - return View(vm); - } + /// + /// Displays the list of events for a particular day. + /// + [Route("list")] + public async Task DayList([FromQuery] int year, [FromQuery] int month, [FromQuery] int? day = null) + { + var vm = await _calendar.GetDayEventsAsync(year, month, day); + return View(vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/ErrorController.cs b/src/Bonsai/Areas/Front/Controllers/ErrorController.cs index 02647437..b838f8ea 100644 --- a/src/Bonsai/Areas/Front/Controllers/ErrorController.cs +++ b/src/Bonsai/Areas/Front/Controllers/ErrorController.cs @@ -3,31 +3,30 @@ using Bonsai.Code.Infrastructure; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// Controller for displaying error messages. +/// +[Route("error")] +[Area("Front")] +public class ErrorController: AppControllerBase { - /// - /// Controller for displaying error messages. - /// - [Route("error")] - [Area("Front")] - public class ErrorController: AppControllerBase + public ErrorController(AuthService auth) { - public ErrorController(AuthService auth) - { - _auth = auth; - } + _auth = auth; + } - private readonly AuthService _auth; + private readonly AuthService _auth; - /// - /// Displays the "not found" - /// - [Route("404")] - [HttpGet] - public async Task NotFoundError() - { - ViewBag.User = await _auth.GetCurrentUserAsync(User); - return View("NotFound"); - } + /// + /// Displays the "not found" + /// + [Route("404")] + [HttpGet] + public async Task NotFoundError() + { + ViewBag.User = await _auth.GetCurrentUserAsync(User); + return View("NotFound"); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/HomeController.cs b/src/Bonsai/Areas/Front/Controllers/HomeController.cs index 1f7ee508..c082b82c 100644 --- a/src/Bonsai/Areas/Front/Controllers/HomeController.cs +++ b/src/Bonsai/Areas/Front/Controllers/HomeController.cs @@ -6,38 +6,37 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// Main controller that displays a basic page. +/// +[Route("")] +[Area("Front")] +[Authorize(Policy = AuthRequirement.Name)] +public class HomeController : AppControllerBase { + public HomeController(PagePresenterService pages, MediaPresenterService media) + { + _pages = pages; + _media = media; + } + + private readonly PagePresenterService _pages; + private readonly MediaPresenterService _media; + /// - /// Main controller that displays a basic page. + /// Returns the main page. /// [Route("")] - [Area("Front")] - [Authorize(Policy = AuthRequirement.Name)] - public class HomeController : AppControllerBase + public async Task Index() { - public HomeController(PagePresenterService pages, MediaPresenterService media) - { - _pages = pages; - _media = media; - } - - private readonly PagePresenterService _pages; - private readonly MediaPresenterService _media; - - /// - /// Returns the main page. - /// - [Route("")] - public async Task Index() + var vm = new HomeVM { - var vm = new HomeVM - { - LastUpdatedPages = await _pages.GetLastUpdatedPagesAsync(5), - LastUploadedMedia = await _media.GetLastUploadedMediaAsync(10) - }; + LastUpdatedPages = await _pages.GetLastUpdatedPagesAsync(5), + LastUploadedMedia = await _media.GetLastUploadedMediaAsync(10) + }; - return View(vm); - } + return View(vm); } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/MediaController.cs b/src/Bonsai/Areas/Front/Controllers/MediaController.cs index 051d0331..30821108 100644 --- a/src/Bonsai/Areas/Front/Controllers/MediaController.cs +++ b/src/Bonsai/Areas/Front/Controllers/MediaController.cs @@ -6,36 +6,35 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// The controller for displaying media information. +/// +[Area("Front")] +[Route("m")] +[Authorize(Policy = AuthRequirement.Name)] +public class MediaController: AppControllerBase { - /// - /// The controller for displaying media information. - /// - [Area("Front")] - [Route("m")] - [Authorize(Policy = AuthRequirement.Name)] - public class MediaController: AppControllerBase + public MediaController(MediaPresenterService media, CacheService cache, AuthService auth) { - public MediaController(MediaPresenterService media, CacheService cache, AuthService auth) - { - _media = media; - _cache = cache; - _auth = auth; - } + _media = media; + _cache = cache; + _auth = auth; + } - private readonly MediaPresenterService _media; - private readonly CacheService _cache; - private readonly AuthService _auth; + private readonly MediaPresenterService _media; + private readonly CacheService _cache; + private readonly AuthService _auth; - /// - /// Displays media and details. - /// - [Route("{key}")] - public async Task ViewMedia(string key) - { - var vm = await _cache.GetOrAddAsync(key, async() => await _media.GetMediaAsync(key)); - ViewBag.User = await _auth.GetCurrentUserAsync(User); - return View(vm); - } + /// + /// Displays media and details. + /// + [Route("{key}")] + public async Task ViewMedia(string key) + { + var vm = await _cache.GetOrAddAsync(key, async() => await _media.GetMediaAsync(key)); + ViewBag.User = await _auth.GetCurrentUserAsync(User); + return View(vm); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/PageController.cs b/src/Bonsai/Areas/Front/Controllers/PageController.cs index 4b13512e..e2457d62 100644 --- a/src/Bonsai/Areas/Front/Controllers/PageController.cs +++ b/src/Bonsai/Areas/Front/Controllers/PageController.cs @@ -11,87 +11,86 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// The root controller for pages. +/// +[Area("Front")] +[Route("p")] +[Authorize(Policy = AuthRequirement.Name)] +public class PageController: AppControllerBase { + public PageController(PagePresenterService pages, AuthService auth, CacheService cache) + { + _pages = pages; + _auth = auth; + _cache = cache; + } + + private readonly PagePresenterService _pages; + private readonly AuthService _auth; + private readonly CacheService _cache; + /// - /// The root controller for pages. + /// Displays the page description. /// - [Area("Front")] - [Route("p")] - [Authorize(Policy = AuthRequirement.Name)] - public class PageController: AppControllerBase + [Route("{key}")] + public async Task Description(string key) { - public PageController(PagePresenterService pages, AuthService auth, CacheService cache) - { - _pages = pages; - _auth = auth; - _cache = cache; - } + return await DisplayTab(key, () => _pages.GetPageDescriptionAsync(key)); + } - private readonly PagePresenterService _pages; - private readonly AuthService _auth; - private readonly CacheService _cache; + /// + /// Displays the related media files. + /// + [Route("{key}/media")] + public async Task Media(string key) + { + return await DisplayTab(key, () => _pages.GetPageMediaAsync(key)); + } - /// - /// Displays the page description. - /// - [Route("{key}")] - public async Task Description(string key) - { - return await DisplayTab(key, () => _pages.GetPageDescriptionAsync(key)); - } + /// + /// Displays the tree pane. + /// + [Route("{key}/tree")] + public async Task Tree(string key) + { + return await DisplayTab(key, () => _pages.GetPageTreeAsync(key)); + } + + /// + /// Displays the references to current page. + /// + [Route("{key}/refs")] + public async Task References(string key) + { + return await DisplayTab(key, () => _pages.GetPageReferencesAsync(key)); + } - /// - /// Displays the related media files. - /// - [Route("{key}/media")] - public async Task Media(string key) - { - return await DisplayTab(key, () => _pages.GetPageMediaAsync(key)); - } + /// + /// Displays the page tab. + /// + private async Task DisplayTab(string key, Func> bodyGetter, [CallerMemberName] string methodName = null) + where T: PageTitleVM + { + var encKey = PageHelper.EncodeTitle(key); + if (encKey != key) + return RedirectToActionPermanent(methodName, new { key = encKey }); - /// - /// Displays the tree pane. - /// - [Route("{key}/tree")] - public async Task Tree(string key) + try { - return await DisplayTab(key, () => _pages.GetPageTreeAsync(key)); + ViewBag.User = await _auth.GetCurrentUserAsync(User); + var vm = new PageVM + { + Body = await _cache.GetOrAddAsync(key, bodyGetter), + InfoBlock = await _cache.GetOrAddAsync(key, async () => await _pages.GetPageInfoBlockAsync(key)) + }; + return View(vm); } - - /// - /// Displays the references to current page. - /// - [Route("{key}/refs")] - public async Task References(string key) + catch(RedirectRequiredException ex) { - return await DisplayTab(key, () => _pages.GetPageReferencesAsync(key)); - } - - /// - /// Displays the page tab. - /// - private async Task DisplayTab(string key, Func> bodyGetter, [CallerMemberName] string methodName = null) - where T: PageTitleVM - { - var encKey = PageHelper.EncodeTitle(key); - if (encKey != key) - return RedirectToActionPermanent(methodName, new { key = encKey }); - - try - { - ViewBag.User = await _auth.GetCurrentUserAsync(User); - var vm = new PageVM - { - Body = await _cache.GetOrAddAsync(key, bodyGetter), - InfoBlock = await _cache.GetOrAddAsync(key, async () => await _pages.GetPageInfoBlockAsync(key)) - }; - return View(vm); - } - catch(RedirectRequiredException ex) - { - return RedirectToActionPermanent(methodName, new { key = ex.Key }); - } + return RedirectToActionPermanent(methodName, new { key = ex.Key }); } } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/SearchController.cs b/src/Bonsai/Areas/Front/Controllers/SearchController.cs index caa69cb8..261cdf85 100644 --- a/src/Bonsai/Areas/Front/Controllers/SearchController.cs +++ b/src/Bonsai/Areas/Front/Controllers/SearchController.cs @@ -9,63 +9,62 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// The controller for searching for pages. +/// +[Area("front")] +[Route("s")] +[Authorize(Policy = AuthRequirement.Name)] +public class SearchController: AppControllerBase { - /// - /// The controller for searching for pages. - /// - [Area("front")] - [Route("s")] - [Authorize(Policy = AuthRequirement.Name)] - public class SearchController: AppControllerBase + public SearchController(SearchPresenterService search) { - public SearchController(SearchPresenterService search) - { - _search = search; - } + _search = search; + } - private readonly SearchPresenterService _search; + private readonly SearchPresenterService _search; - /// - /// Returns the global search's autocomplete results. - /// - [HttpGet] - [Route("~/util/suggest/{*query}")] - public async Task> Suggest(string query) - { - var hints = await _search.SuggestAsync(query); - return hints; - } + /// + /// Returns the global search's autocomplete results. + /// + [HttpGet] + [Route("~/util/suggest/{*query}")] + public async Task> Suggest(string query) + { + var hints = await _search.SuggestAsync(query); + return hints; + } - /// - /// Returns the search results. - /// - [HttpGet] - [Route("")] - public async Task Search([FromQuery] string query) - { - var exact = await _search.FindExactAsync(query); - if (exact != null) - return RedirectToAction("Description", "Page", new { key = exact.Key }); + /// + /// Returns the search results. + /// + [HttpGet] + [Route("")] + public async Task Search([FromQuery] string query) + { + var exact = await _search.FindExactAsync(query); + if (exact != null) + return RedirectToAction("Description", "Page", new { key = exact.Key }); - var results = await _search.SearchAsync(query); - var vm = new SearchVM {Query = query, Results = results}; - return View(vm); - } + var results = await _search.SearchAsync(query); + var vm = new SearchVM {Query = query, Results = results}; + return View(vm); + } - /// - /// Returns the search results. - /// - [HttpGet] - [Route("results")] - public async Task SearchResults([FromQuery] string query, [FromQuery] int page = 0) - { - var results = await _search.SearchAsync(query, Math.Max(0, page)); + /// + /// Returns the search results. + /// + [HttpGet] + [Route("results")] + public async Task SearchResults([FromQuery] string query, [FromQuery] int page = 0) + { + var results = await _search.SearchAsync(query, Math.Max(0, page)); - if(results.Count > 0) - return View(results); + if(results.Count > 0) + return View(results); - return NotFound(); - } + return NotFound(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Controllers/TreeController.cs b/src/Bonsai/Areas/Front/Controllers/TreeController.cs index 13150fcb..8968af88 100644 --- a/src/Bonsai/Areas/Front/Controllers/TreeController.cs +++ b/src/Bonsai/Areas/Front/Controllers/TreeController.cs @@ -8,51 +8,50 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bonsai.Areas.Front.Controllers +namespace Bonsai.Areas.Front.Controllers; + +/// +/// Controller for displaying trees. +/// +[Area("Front")] +[Route("tree")] +[Authorize(Policy = AuthRequirement.Name)] +public class TreeController : AppControllerBase { - /// - /// Controller for displaying trees. - /// - [Area("Front")] - [Route("tree")] - [Authorize(Policy = AuthRequirement.Name)] - public class TreeController : AppControllerBase + public TreeController(PagePresenterService pages, TreePresenterService tree, CacheService cache) { - public TreeController(PagePresenterService pages, TreePresenterService tree, CacheService cache) - { - _pages = pages; - _tree = tree; - _cache = cache; - } + _pages = pages; + _tree = tree; + _cache = cache; + } - private readonly PagePresenterService _pages; - private readonly TreePresenterService _tree; - private readonly CacheService _cache; + private readonly PagePresenterService _pages; + private readonly TreePresenterService _tree; + private readonly CacheService _cache; - /// - /// Displays the internal page for a tree. - /// - [Route("{key}")] - public async Task Main(string key, [FromQuery] TreeKind kind = TreeKind.FullTree) - { - var encKey = PageHelper.EncodeTitle(key); - if (encKey != key) - return RedirectToActionPermanent("Main", new { key = encKey }); + /// + /// Displays the internal page for a tree. + /// + [Route("{key}")] + public async Task Main(string key, [FromQuery] TreeKind kind = TreeKind.FullTree) + { + var encKey = PageHelper.EncodeTitle(key); + if (encKey != key) + return RedirectToActionPermanent("Main", new { key = encKey }); - var model = await _cache.GetOrAddAsync(key + "." + kind, () => _pages.GetPageTreeAsync(key, kind)); + var model = await _cache.GetOrAddAsync(key + "." + kind, () => _pages.GetPageTreeAsync(key, kind)); - return View(model); - } + return View(model); + } - /// - /// Returns the rendered tree. - /// - [Route("~/util/tree/{key}")] - public async Task GetTreeData(string key, [FromQuery] TreeKind kind = TreeKind.FullTree) - { - var encKey = PageHelper.EncodeTitle(key); - var data = await _tree.GetTreeAsync(encKey, kind); - return Json(data); - } + /// + /// Returns the rendered tree. + /// + [Route("~/util/tree/{key}")] + public async Task GetTreeData(string key, [FromQuery] TreeKind kind = TreeKind.FullTree) + { + var encKey = PageHelper.EncodeTitle(key); + var data = await _tree.GetTreeAsync(encKey, kind); + return Json(data); } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Auth/AuthHandler.cs b/src/Bonsai/Areas/Front/Logic/Auth/AuthHandler.cs index 0d41dcbd..f0f1ed13 100644 --- a/src/Bonsai/Areas/Front/Logic/Auth/AuthHandler.cs +++ b/src/Bonsai/Areas/Front/Logic/Auth/AuthHandler.cs @@ -4,37 +4,36 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -namespace Bonsai.Areas.Front.Logic.Auth +namespace Bonsai.Areas.Front.Logic.Auth; + +/// +/// Authorization handler for requiring login depending on the config. +/// +public class AuthHandler: AuthorizationHandler { + public AuthHandler(BonsaiConfigService cfgProvider, UserManager userMgr) + { + _cfgProvider = cfgProvider; + _userMgr = userMgr; + } + + private readonly BonsaiConfigService _cfgProvider; + private readonly UserManager _userMgr; + /// - /// Authorization handler for requiring login depending on the config. + /// Checks the authorization if the config requires it. /// - public class AuthHandler: AuthorizationHandler + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthRequirement requirement) { - public AuthHandler(BonsaiConfigService cfgProvider, UserManager userMgr) + var cfg = _cfgProvider.GetDynamicConfig(); + if (cfg.AllowGuests) { - _cfgProvider = cfgProvider; - _userMgr = userMgr; + context.Succeed(requirement); + return; } - private readonly BonsaiConfigService _cfgProvider; - private readonly UserManager _userMgr; - - /// - /// Checks the authorization if the config requires it. - /// - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthRequirement requirement) - { - var cfg = _cfgProvider.GetDynamicConfig(); - if (cfg.AllowGuests) - { - context.Succeed(requirement); - return; - } - - var user = await _userMgr.GetUserAsync(context.User); - if(user?.IsValidated == true) - context.Succeed(requirement); - } + var user = await _userMgr.GetUserAsync(context.User); + if(user?.IsValidated == true) + context.Succeed(requirement); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Auth/AuthProviderService.cs b/src/Bonsai/Areas/Front/Logic/Auth/AuthProviderService.cs index d6330a23..8c726390 100644 --- a/src/Bonsai/Areas/Front/Logic/Auth/AuthProviderService.cs +++ b/src/Bonsai/Areas/Front/Logic/Auth/AuthProviderService.cs @@ -7,120 +7,122 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Areas.Front.Logic.Auth +namespace Bonsai.Areas.Front.Logic.Auth; + +/// +/// The service for handling external authentication providers. +/// +public class AuthProviderService { /// - /// The service for handling external authentication providers. + /// The total list of supported providers. /// - public class AuthProviderService - { - /// - /// The total list of supported providers. - /// - private static List SupportedProviders = new List + private static readonly List SupportedProviders = + [ + new AuthProviderVM { - new AuthProviderVM - { - Key = "Vkontakte", - Caption = Texts.AuthProvider_Vkontakte, - IconClass = "fa fa-vk", - TryActivate = (cfg, auth) => - { - if (cfg?.Auth?.Vkontakte == null) - return false; - - var id = cfg.Auth.Vkontakte?.ClientId; - var secret = cfg.Auth.Vkontakte?.ClientSecret; - if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) - return false; - - auth.AddVkontakte(opts => - { - opts.ClientId = id; - opts.ClientSecret = secret; - opts.Scope.AddRange(new [] { "email" }); - opts.ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "bdate"); - opts.Fields.Add("bdate"); - opts.AccessDeniedPath = "/auth/failed"; - }); - - return true; - } - }, - - new AuthProviderVM + Key = "Vkontakte", + Caption = Texts.AuthProvider_Vkontakte, + IconClass = "fa fa-vk", + TryActivate = (cfg, auth) => { - Key = "Yandex", - Caption = Texts.AuthProvider_Yandex, - IconClass = "fa fa-yahoo", // the closest one that has an Y :) - TryActivate = (cfg, auth) => + if (cfg?.Auth?.Vkontakte == null) + return false; + + var id = cfg.Auth.Vkontakte?.ClientId; + var secret = cfg.Auth.Vkontakte?.ClientSecret; + if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) + return false; + + auth.AddVkontakte(opts => { - if (cfg?.Auth?.Yandex == null) - return false; - - var id = cfg.Auth.Yandex?.ClientId; - var secret = cfg.Auth.Yandex?.ClientSecret; - if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) - return false; - - auth.AddYandex(opts => - { - opts.ClientId = id; - opts.ClientSecret = secret; - opts.Scope.AddRange(new [] { "login:email", "login:birthday", "login:info" }); - opts.ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); - opts.AccessDeniedPath = "/auth/failed"; - }); - - return true; - } - }, - - new AuthProviderVM + opts.ClientId = id; + opts.ClientSecret = secret; + opts.Scope.AddRange(new[] {"email"}); + opts.ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "bdate"); + opts.Fields.Add("bdate"); + opts.AccessDeniedPath = "/auth/failed"; + }); + + return true; + } + }, + + + new AuthProviderVM + { + Key = "Yandex", + Caption = Texts.AuthProvider_Yandex, + IconClass = "fa fa-yahoo", // the closest one that has an Y :) + TryActivate = (cfg, auth) => { - Key = "Google", - Caption = Texts.AuthProvider_Google, - IconClass = "fa fa-google-plus-square", - TryActivate = (cfg, auth) => + if (cfg?.Auth?.Yandex == null) + return false; + + var id = cfg.Auth.Yandex?.ClientId; + var secret = cfg.Auth.Yandex?.ClientSecret; + if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) + return false; + + auth.AddYandex(opts => { - if (cfg?.Auth?.Google == null) - return false; - - var id = cfg.Auth.Google?.ClientId; - var secret = cfg.Auth.Google?.ClientSecret; - if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) - return false; - - auth.AddGoogle(opts => - { - opts.ClientId = id; - opts.ClientSecret = secret; - opts.Scope.AddRange(new [] { "email", "profile" }); - opts.AccessDeniedPath = "/auth/failed"; - }); - - return true; - } - }, - }; - - /// - /// List of providers that are currently configured as active. - /// - public IReadOnlyList AvailableProviders { get; private set; } - - /// - /// Configures all enabled configuration providers. - /// - public void Initialize(StaticConfig config, AuthenticationBuilder authBuilder) + opts.ClientId = id; + opts.ClientSecret = secret; + opts.Scope.AddRange(new[] {"login:email", "login:birthday", "login:info"}); + opts.ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); + opts.AccessDeniedPath = "/auth/failed"; + }); + + return true; + } + }, + + + new AuthProviderVM { - var available = new List(); + Key = "Google", + Caption = Texts.AuthProvider_Google, + IconClass = "fa fa-google-plus-square", + TryActivate = (cfg, auth) => + { + if (cfg?.Auth?.Google == null) + return false; - foreach (var prov in SupportedProviders) - if(prov.TryActivate(config, authBuilder)) - available.Add(prov); + var id = cfg.Auth.Google?.ClientId; + var secret = cfg.Auth.Google?.ClientSecret; + if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(secret)) + return false; - AvailableProviders = available; + auth.AddGoogle(opts => + { + opts.ClientId = id; + opts.ClientSecret = secret; + opts.Scope.AddRange(new[] {"email", "profile"}); + opts.AccessDeniedPath = "/auth/failed"; + }); + + return true; + } } + + ]; + + /// + /// List of providers that are currently configured as active. + /// + public IReadOnlyList AvailableProviders { get; private set; } + + /// + /// Configures all enabled configuration providers. + /// + public void Initialize(StaticConfig config, AuthenticationBuilder authBuilder) + { + var available = new List(); + + foreach (var prov in SupportedProviders) + if(prov.TryActivate(config, authBuilder)) + available.Add(prov); + + AvailableProviders = available; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Auth/AuthRequirement.cs b/src/Bonsai/Areas/Front/Logic/Auth/AuthRequirement.cs index 0da3f98e..45231cef 100644 --- a/src/Bonsai/Areas/Front/Logic/Auth/AuthRequirement.cs +++ b/src/Bonsai/Areas/Front/Logic/Auth/AuthRequirement.cs @@ -1,16 +1,15 @@ using Microsoft.AspNetCore.Authorization; -namespace Bonsai.Areas.Front.Logic.Auth +namespace Bonsai.Areas.Front.Logic.Auth; + +/// +/// Empty requirement class. +/// Is validated by . +/// +public class AuthRequirement: IAuthorizationRequirement { /// - /// Empty requirement class. - /// Is validated by . + /// Name of the policy. /// - public class AuthRequirement: IAuthorizationRequirement - { - /// - /// Name of the policy. - /// - public const string Name = "AuthRequirement"; - } -} + public const string Name = "AuthRequirement"; +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Auth/AuthService.cs b/src/Bonsai/Areas/Front/Logic/Auth/AuthService.cs index 3d5f5f50..7c95af34 100644 --- a/src/Bonsai/Areas/Front/Logic/Auth/AuthService.cs +++ b/src/Bonsai/Areas/Front/Logic/Auth/AuthService.cs @@ -13,224 +13,222 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Front.Logic.Auth +namespace Bonsai.Areas.Front.Logic.Auth; + +/// +/// Wrapper service for authorization. +/// +public class AuthService { + public AuthService(SignInManager signInManager, UserManager userManager, AppDbContext db, IMapper mapper) + { + _signMgr = signInManager; + _userMgr = userManager; + _db = db; + _mapper = mapper; + } + + private readonly SignInManager _signMgr; + private readonly UserManager _userMgr; + private readonly AppDbContext _db; + private readonly IMapper _mapper; + /// - /// Wrapper service for authorization. + /// Attempts to authenticate the user via OAuth. /// - public class AuthService + public async Task ExternalLoginAsync() { - public AuthService(SignInManager signInManager, UserManager userManager, AppDbContext db, IMapper mapper) - { - _signMgr = signInManager; - _userMgr = userManager; - _db = db; - _mapper = mapper; - } + var info = await _signMgr.GetExternalLoginInfoAsync(); + if (info == null) + return new LoginResultVM(LoginStatus.Failed); - private readonly SignInManager _signMgr; - private readonly UserManager _userMgr; - private readonly AppDbContext _db; - private readonly IMapper _mapper; + var result = await _signMgr.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true); + if (result.IsLockedOut || result.IsNotAllowed) + return new LoginResultVM(LoginStatus.LockedOut, info); - /// - /// Attempts to authenticate the user via OAuth. - /// - public async Task ExternalLoginAsync() - { - var info = await _signMgr.GetExternalLoginInfoAsync(); - if (info == null) - return new LoginResultVM(LoginStatus.Failed); + if(!result.Succeeded) + return new LoginResultVM(LoginStatus.NewUser, info); - var result = await _signMgr.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true); - if (result.IsLockedOut || result.IsNotAllowed) - return new LoginResultVM(LoginStatus.LockedOut, info); + var user = await FindUserAsync(info.LoginProvider, info.ProviderKey); + if (!user.IsValidated) + return new LoginResultVM(LoginStatus.Unvalidated, info); - if(!result.Succeeded) - return new LoginResultVM(LoginStatus.NewUser, info); + return new LoginResultVM(LoginStatus.Succeeded, info); + } - var user = await FindUserAsync(info.LoginProvider, info.ProviderKey); - if (!user.IsValidated) - return new LoginResultVM(LoginStatus.Unvalidated, info); + /// + /// Attempts to authorize the user via local auth. + /// + public async Task LocalLoginAsync(LocalLoginVM vm) + { + var email = vm.Login?.ToUpper(); + var user = await _db.Users.FirstOrDefaultAsync(x => x.NormalizedEmail == email); + if (user != null) + { + var info = await _signMgr.PasswordSignInAsync(user, vm.Password ?? "", isPersistent: true, lockoutOnFailure: true); + if (info.Succeeded) + return new LoginResultVM(LoginStatus.Succeeded); - return new LoginResultVM(LoginStatus.Succeeded, info); + if (info.IsLockedOut) + return new LoginResultVM(LoginStatus.LockedOut); } - /// - /// Attempts to authorize the user via local auth. - /// - public async Task LocalLoginAsync(LocalLoginVM vm) - { - var email = vm.Login?.ToUpper(); - var user = await _db.Users.FirstOrDefaultAsync(x => x.NormalizedEmail == email); - if (user != null) - { - var info = await _signMgr.PasswordSignInAsync(user, vm.Password ?? "", isPersistent: true, lockoutOnFailure: true); - if (info.Succeeded) - return new LoginResultVM(LoginStatus.Succeeded); - - if (info.IsLockedOut) - return new LoginResultVM(LoginStatus.LockedOut); - } + return new LoginResultVM(LoginStatus.Failed); + } - return new LoginResultVM(LoginStatus.Failed); - } + /// + /// Logs the user out. + /// + public async Task LogoutAsync() + { + await _signMgr.SignOutAsync(); + } - /// - /// Logs the user out. - /// - public async Task LogoutAsync() - { - await _signMgr.SignOutAsync(); - } + /// + /// Retrieves default values for registration form from the claims provided by external login provider. + /// + public RegisterUserVM GetRegistrationData(ClaimsPrincipal cp) + { + string GetClaim(string type) => cp.Claims.FirstOrDefault(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/" + type)?.Value; - /// - /// Retrieves default values for registration form from the claims provided by external login provider. - /// - public RegisterUserVM GetRegistrationData(ClaimsPrincipal cp) + return new RegisterUserVM { - string GetClaim(string type) => cp.Claims.FirstOrDefault(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/" + type)?.Value; - - return new RegisterUserVM - { - FirstName = GetClaim("givenname"), - LastName = GetClaim("surname"), - Email = GetClaim("emailaddress"), - Birthday = FormatDate(GetClaim("dateofbirth")) - }; - } + FirstName = GetClaim("givenname"), + LastName = GetClaim("surname"), + Email = GetClaim("emailaddress"), + Birthday = FormatDate(GetClaim("dateofbirth")) + }; + } - /// - /// Creates the user. - /// - public async Task RegisterAsync(RegisterUserVM vm, ExternalLoginData extLogin) - { - await ValidateRegisterRequestAsync(vm, usePasswordAuth: extLogin == null); + /// + /// Creates the user. + /// + public async Task RegisterAsync(RegisterUserVM vm, ExternalLoginData extLogin) + { + await ValidateRegisterRequestAsync(vm, usePasswordAuth: extLogin == null); - var isFirstUser = await IsFirstUserAsync(); + var isFirstUser = await IsFirstUserAsync(); - var user = _mapper.Map(vm); - user.Id = Guid.NewGuid().ToString(); - user.IsValidated = isFirstUser; - user.AuthType = extLogin == null ? AuthType.Password : AuthType.ExternalProvider; + var user = _mapper.Map(vm); + user.Id = Guid.NewGuid().ToString(); + user.IsValidated = isFirstUser; + user.AuthType = extLogin == null ? AuthType.Password : AuthType.ExternalProvider; - var createResult = await _userMgr.CreateAsync(user); - if (!createResult.Succeeded) - { - var msgs = createResult.Errors.Select(x => new KeyValuePair("", x.Description)).ToList(); - throw new ValidationException(msgs); - } + var createResult = await _userMgr.CreateAsync(user); + if (!createResult.Succeeded) + { + var msgs = createResult.Errors.Select(x => new KeyValuePair("", x.Description)).ToList(); + throw new ValidationException(msgs); + } - if (extLogin != null) - await _userMgr.AddLoginAsync(user, new UserLoginInfo(extLogin.LoginProvider, extLogin.ProviderKey, extLogin.LoginProvider)); - else - await _userMgr.AddPasswordAsync(user, vm.Password); + if (extLogin != null) + await _userMgr.AddLoginAsync(user, new UserLoginInfo(extLogin.LoginProvider, extLogin.ProviderKey, extLogin.LoginProvider)); + else + await _userMgr.AddPasswordAsync(user, vm.Password); - await _userMgr.AddToRoleAsync(user, isFirstUser ? nameof(UserRole.Admin) : nameof(UserRole.Unvalidated)); + await _userMgr.AddToRoleAsync(user, isFirstUser ? nameof(UserRole.Admin) : nameof(UserRole.Unvalidated)); - await _signMgr.SignInAsync(user, true); + await _signMgr.SignInAsync(user, true); - return new RegisterUserResultVM - { - IsValidated = user.IsValidated, - User = user, - Principal = await _signMgr.CreateUserPrincipalAsync(user) - }; - } - - /// - /// Returns the information about the current user. - /// - public async Task GetCurrentUserAsync(ClaimsPrincipal principal) + return new RegisterUserResultVM { - if (principal == null) - return null; - - var id = _userMgr.GetUserId(principal); - var user = await _db.Users - .AsNoTracking() - .Include(x => x.Page) - .FirstOrDefaultAsync(x => x.Id == id); - - if (user == null) - return null; - - var roles = await _userMgr.GetRolesAsync(user); - var isAdmin = roles.Contains(nameof(UserRole.Admin)) || roles.Contains(nameof(UserRole.Editor)); - - return new UserVM - { - Name = user.FirstName + " " + user.LastName, - Email = user.Email, - PageKey = user.Page?.Key, - IsAdministrator = isAdmin, - IsValidated = user.IsValidated - }; - } + IsValidated = user.IsValidated, + User = user, + Principal = await _signMgr.CreateUserPrincipalAsync(user) + }; + } - /// - /// Checks if there are users existing in the database. - /// - public async Task IsFirstUserAsync() - { - return await _db.Users.AnyAsync() == false; - } + /// + /// Returns the information about the current user. + /// + public async Task GetCurrentUserAsync(ClaimsPrincipal principal) + { + if (principal == null) + return null; - #region Private helpers + var id = _userMgr.GetUserId(principal); + var user = await _db.Users + .AsNoTracking() + .Include(x => x.Page) + .FirstOrDefaultAsync(x => x.Id == id); - /// - /// Finds the corresponding user. - /// - private async Task FindUserAsync(string provider, string key) + if (user == null) + return null; + + var roles = await _userMgr.GetRolesAsync(user); + var isAdmin = roles.Contains(nameof(UserRole.Admin)) || roles.Contains(nameof(UserRole.Editor)); + + return new UserVM { - var login = await _db.UserLogins - .FirstOrDefaultAsync(x => x.LoginProvider == provider - && x.ProviderKey == key); + Name = user.FirstName + " " + user.LastName, + Email = user.Email, + PageKey = user.Page?.Key, + IsAdministrator = isAdmin, + IsValidated = user.IsValidated + }; + } - if (login == null) - return null; + /// + /// Checks if there are users existing in the database. + /// + public async Task IsFirstUserAsync() + { + return await _db.Users.AnyAsync() == false; + } - return await _db.Users - .FirstOrDefaultAsync(x => x.Id == login.UserId); - } + #region Private helpers - /// - /// Performs additional checks on the registration request. - /// - private async Task ValidateRegisterRequestAsync(RegisterUserVM vm, bool usePasswordAuth) - { - var val = new Validator(); + /// + /// Finds the corresponding user. + /// + private async Task FindUserAsync(string provider, string key) + { + var login = await _db.UserLogins + .FirstOrDefaultAsync(x => x.LoginProvider == provider + && x.ProviderKey == key); - if (FuzzyDate.TryParse(vm.Birthday) == null) - val.Add(nameof(vm.Birthday), Texts.AuthService_Error_InvalidBirthday); + if (login == null) + return null; - var emailExists = await _db.Users.AnyAsync(x => x.Email == vm.Email); - if (emailExists) - val.Add(nameof(vm.Email), Texts.AuthService_Error_EmailAlreadyExists); + return await _db.Users + .FirstOrDefaultAsync(x => x.Id == login.UserId); + } - if (usePasswordAuth) - { - if (vm.Password == null || vm.Password.Length < 6) - val.Add(nameof(vm.Password), Texts.AuthService_Error_PasswordTooShort); + /// + /// Performs additional checks on the registration request. + /// + private async Task ValidateRegisterRequestAsync(RegisterUserVM vm, bool usePasswordAuth) + { + var val = new Validator(); - if (vm.Password != vm.PasswordCopy) - val.Add(nameof(vm.PasswordCopy), Texts.AuthService_Error_PasswordDoesNotMatch); - } + if (FuzzyDate.TryParse(vm.Birthday) == null) + val.Add(nameof(vm.Birthday), Texts.AuthService_Error_InvalidBirthday); - val.ThrowIfInvalid(); - } + var emailExists = await _db.Users.AnyAsync(x => x.Email == vm.Email); + if (emailExists) + val.Add(nameof(vm.Email), Texts.AuthService_Error_EmailAlreadyExists); - /// - /// Formats the date according to local format. - /// - private string FormatDate(string isoDate) + if (usePasswordAuth) { - if (DateTime.TryParse(isoDate, out var date)) - return new FuzzyDate(date).ToString(); + if (vm.Password == null || vm.Password.Length < 6) + val.Add(nameof(vm.Password), Texts.AuthService_Error_PasswordTooShort); - return null; + if (vm.Password != vm.PasswordCopy) + val.Add(nameof(vm.PasswordCopy), Texts.AuthService_Error_PasswordDoesNotMatch); } - #endregion + val.ThrowIfInvalid(); + } + + /// + /// Formats the date according to local format. + /// + private string FormatDate(string isoDate) + { + return DateTime.TryParse(isoDate, out var date) + ? new FuzzyDate(date).ToString() + : null; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Auth/ExternalLoginData.cs b/src/Bonsai/Areas/Front/Logic/Auth/ExternalLoginData.cs index ba504e97..cdf787f3 100644 --- a/src/Bonsai/Areas/Front/Logic/Auth/ExternalLoginData.cs +++ b/src/Bonsai/Areas/Front/Logic/Auth/ExternalLoginData.cs @@ -1,24 +1,23 @@ -namespace Bonsai.Areas.Front.Logic.Auth +namespace Bonsai.Areas.Front.Logic.Auth; + +/// +/// Details of an external provider's authentication. +/// +public class ExternalLoginData { - /// - /// Details of an external provider's authentication. - /// - public class ExternalLoginData + public ExternalLoginData(string loginProvider, string providerKey) { - public ExternalLoginData(string loginProvider, string providerKey) - { - LoginProvider = loginProvider; - ProviderKey = providerKey; - } + LoginProvider = loginProvider; + ProviderKey = providerKey; + } - /// - /// External provider: Facebook, Google, etc. - /// - public string LoginProvider { get; } + /// + /// External provider: Facebook, Google, etc. + /// + public string LoginProvider { get; } - /// - /// User's personal key returned by the provider. - /// - public string ProviderKey { get; } - } -} + /// + /// User's personal key returned by the provider. + /// + public string ProviderKey { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/CalendarPresenterService.cs b/src/Bonsai/Areas/Front/Logic/CalendarPresenterService.cs index 4e7fbe6b..7d9f9eca 100644 --- a/src/Bonsai/Areas/Front/Logic/CalendarPresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/CalendarPresenterService.cs @@ -14,353 +14,351 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Front.Logic +namespace Bonsai.Areas.Front.Logic; + +/// +/// The service that calculates events for the frontpage's calendar. +/// +public class CalendarPresenterService { + public CalendarPresenterService(AppDbContext db) + { + _db = db; + } + + private readonly AppDbContext _db; + /// - /// The service that calculates events for the frontpage's calendar. + /// Returns the events to display for the current month. /// - public class CalendarPresenterService + public async Task GetMonthEventsAsync(int year, int month) { - public CalendarPresenterService(AppDbContext db) - { - _db = db; - } - - private readonly AppDbContext _db; + var range = GetDisplayedRange(year, month); + var events = await GetMonthEventsInternalAsync(year, month); - /// - /// Returns the events to display for the current month. - /// - public async Task GetMonthEventsAsync(int year, int month) + return new CalendarMonthVM { - var range = GetDisplayedRange(year, month); - var events = await GetMonthEventsInternalAsync(year, month); + Month = month, + Year = year, + Title = new FuzzyDate(year, month, null).ReadableDate.Capitalize(), + Weeks = GetMonthGrid(range.from, range.to, month, events), + FuzzyEvents = events.Where(x => x.Day == null).ToList() + }; + } - return new CalendarMonthVM - { - Month = month, - Year = year, - Title = new FuzzyDate(year, month, null).ReadableDate.Capitalize(), - Weeks = GetMonthGrid(range.from, range.to, month, events), - FuzzyEvents = events.Where(x => x.Day == null).ToList() - }; - } + /// + /// Returns the events for a particular day (or fuzzy events for the month). + /// + public async Task GetDayEventsAsync(int year, int month, int? day) + { + var events = await GetMonthEventsInternalAsync(year, month); - /// - /// Returns the events for a particular day (or fuzzy events for the month). - /// - public async Task GetDayEventsAsync(int year, int month, int? day) + return new CalendarDayVM { - var events = await GetMonthEventsInternalAsync(year, month); + IsActive = true, + Day = day, + Date = new FuzzyDate(year, month, day), + Events = events.Where(x => x.Day == day).ToList() + }; + } - return new CalendarDayVM - { - IsActive = true, - Day = day, - Date = new FuzzyDate(year, month, day), - Events = events.Where(x => x.Day == day).ToList() - }; - } + #region Private helpers - #region Private helpers + /// + /// Returns all the events that happened in the specified month. + /// + private async Task> GetMonthEventsInternalAsync(int year, int month) + { + var context = await RelationContext.LoadContextAsync(_db); - /// - /// Returns all the events that happened in the specified month. - /// - private async Task> GetMonthEventsInternalAsync(int year, int month) - { - var context = await RelationContext.LoadContextAsync(_db); + var events = GetPageEvents(year, month, context).ToList(); + events.AddRange(GetRelationEvents(year, month, context)); + events.AddRange(await GetOneTimeEventsAsync(year, month)); - var events = GetPageEvents(year, month, context).ToList(); - events.AddRange(GetRelationEvents(year, month, context)); - events.AddRange(await GetOneTimeEventsAsync(year, month)); + return events; + } - return events; - } + /// + /// Infers page-related events for the current month. + /// + private IEnumerable GetPageEvents(int year, int month, RelationContext context) + { + var maxDate = new FuzzyDate(new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1)); - /// - /// Infers page-related events for the current month. - /// - private IEnumerable GetPageEvents(int year, int month, RelationContext context) + foreach (var page in context.Pages.Values) { - var maxDate = new FuzzyDate(new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1)); - - foreach (var page in context.Pages.Values) + if (page.BirthDate is FuzzyDate birth) { - if (page.BirthDate is FuzzyDate birth) + var showBirth = birth.Month == month + && (birth.Year == null || birth.Year <= year) + && (page.DeathDate == null || page.DeathDate >= maxDate); + + if (showBirth) { - var showBirth = birth.Month == month - && (birth.Year == null || birth.Year <= year) - && (page.DeathDate == null || page.DeathDate >= maxDate); + var title = (year == birth.Year && !birth.IsDecade) + ? Texts.CalendarPresenter_Birthday_Day + : (birth.Year == null || birth.IsDecade) + ? Texts.CalendarPresenter_Birthday_Anniversary + : string.Format(Texts.CalendarPresenter_Birthday_PreciseAnniversary, year - birth.Year.Value); - if (showBirth) + yield return new CalendarEventVM { - var title = (year == birth.Year && !birth.IsDecade) - ? Texts.CalendarPresenter_Birthday_Day - : (birth.Year == null || birth.IsDecade) - ? Texts.CalendarPresenter_Birthday_Anniversary - : string.Format(Texts.CalendarPresenter_Birthday_PreciseAnniversary, year - birth.Year.Value); - - yield return new CalendarEventVM - { - Day = birth.Day, - Title = title, - Type = CalendarEventType.Birth, - RelatedPage = Map(page) - }; - } + Day = birth.Day, + Title = title, + Type = CalendarEventType.Birth, + RelatedPage = Map(page) + }; } + } - if (page.DeathDate is FuzzyDate death) + if (page.DeathDate is FuzzyDate death) + { + var showDeath = death.Month == month + && (death.Year == null || death.Year <= year); + + if (showDeath) { - var showDeath = death.Month == month - && (death.Year == null || death.Year <= year); + var title = (year == death.Year && !death.IsDecade) + ? Texts.CalendarPresenter_Death_Day + : (death.Year == null || death.IsDecade) + ? Texts.CalendarPresenter_Death_Anniversary + : string.Format(Texts.CalendarPresenter_Death_PreciseAnniversary, year - death.Year.Value); - if (showDeath) + yield return new CalendarEventVM { - var title = (year == death.Year && !death.IsDecade) - ? Texts.CalendarPresenter_Death_Day - : (death.Year == null || death.IsDecade) - ? Texts.CalendarPresenter_Death_Anniversary - : string.Format(Texts.CalendarPresenter_Death_PreciseAnniversary, year - death.Year.Value); - - yield return new CalendarEventVM - { - Day = death.Day, - Title = title, - Type = CalendarEventType.Death, - RelatedPage = Map(page) - }; - } + Day = death.Day, + Title = title, + Type = CalendarEventType.Death, + RelatedPage = Map(page) + }; } } } + } - /// - /// Infers relation-based events for the current month. - /// - private IEnumerable GetRelationEvents(int year, int month, RelationContext context) - { - var visited = new HashSet(); - var maxDate = new FuzzyDate(CreateDate(year, month).AddMonths(1).AddSeconds(-1)); - - foreach (var rel in context.Relations.SelectMany(x => x.Value)) - { - if (rel.Duration is not FuzzyRange duration || duration.RangeStart is not FuzzyDate start || start.Month != month) - continue; - - if (duration.RangeEnd is FuzzyDate end && end <= maxDate) - continue; + /// + /// Infers relation-based events for the current month. + /// + private IEnumerable GetRelationEvents(int year, int month, RelationContext context) + { + var visited = new HashSet(); + var maxDate = new FuzzyDate(CreateDate(year, month).AddMonths(1).AddSeconds(-1)); - var hash = string.Concat(rel.SourceId.ToString(), rel.DestinationId.ToString(), duration.ToString()); - if (visited.Contains(hash)) - continue; + foreach (var rel in context.Relations.SelectMany(x => x.Value)) + { + if (rel.Duration is not FuzzyRange duration || duration.RangeStart is not FuzzyDate start || start.Month != month) + continue; - var inverseHash = string.Concat(rel.DestinationId.ToString(), rel.SourceId.ToString(), duration.ToString()); - visited.Add(hash); - visited.Add(inverseHash); + if (duration.RangeEnd is FuzzyDate end && end <= maxDate) + continue; - var evt = rel.Type switch - { - RelationType.Spouse => GetWeddingEvent(rel, start), - RelationType.Owner or RelationType.Pet => GetPetAdoptionEvent(start), - RelationType.StepChild or RelationType.StepParent => GetChildAdoptionEvent(rel, start), - _ => null - }; + var hash = string.Concat(rel.SourceId.ToString(), rel.DestinationId.ToString(), duration.ToString()); + if (visited.Contains(hash)) + continue; - if (evt != null) - { - evt.Day = start.Day; - evt.OtherPages = new[] - { - Map(context.Pages[rel.SourceId]), - Map(context.Pages[rel.DestinationId]) - }; - yield return evt; - } - } + var inverseHash = string.Concat(rel.DestinationId.ToString(), rel.SourceId.ToString(), duration.ToString()); + visited.Add(hash); + visited.Add(inverseHash); - CalendarEventVM GetWeddingEvent(RelationContext.RelationExcerpt rel, FuzzyDate start) + var evt = rel.Type switch { - var title = (year == start.Year && !start.IsDecade) - ? Texts.CalendarPresenter_Wedding_Day - : (start.Year == null || start.IsDecade) - ? Texts.CalendarPresenter_Wedding_Anniversary - : string.Format(Texts.CalendarPresenter_Wedding_PreciseAnniversary, year - start.Year.Value); + RelationType.Spouse => GetWeddingEvent(rel, start), + RelationType.Owner or RelationType.Pet => GetPetAdoptionEvent(start), + RelationType.StepChild or RelationType.StepParent => GetChildAdoptionEvent(rel, start), + _ => null + }; - return new CalendarEventVM + if (evt != null) + { + evt.Day = start.Day; + evt.OtherPages = new[] { - Title = title, - Type = CalendarEventType.Wedding, - RelatedPage = rel.EventId == null - ? new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Wedding_Title } - : Map(context.Pages[rel.EventId.Value]), + Map(context.Pages[rel.SourceId]), + Map(context.Pages[rel.DestinationId]) }; + yield return evt; } + } - CalendarEventVM GetPetAdoptionEvent(FuzzyDate start) + CalendarEventVM GetWeddingEvent(RelationContext.RelationExcerpt rel, FuzzyDate start) + { + var title = (year == start.Year && !start.IsDecade) + ? Texts.CalendarPresenter_Wedding_Day + : (start.Year == null || start.IsDecade) + ? Texts.CalendarPresenter_Wedding_Anniversary + : string.Format(Texts.CalendarPresenter_Wedding_PreciseAnniversary, year - start.Year.Value); + + return new CalendarEventVM { - if (year != start.Year || start.IsDecade) - return null; + Title = title, + Type = CalendarEventType.Wedding, + RelatedPage = rel.EventId == null + ? new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Wedding_Title } + : Map(context.Pages[rel.EventId.Value]), + }; + } - return new CalendarEventVM - { - Title = Texts.CalendarPresenter_PetAdoption_Title, - Type = CalendarEventType.PetAdoption, - RelatedPage = new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Event_Title } - }; - } + CalendarEventVM GetPetAdoptionEvent(FuzzyDate start) + { + if (year != start.Year || start.IsDecade) + return null; - CalendarEventVM GetChildAdoptionEvent(RelationContext.RelationExcerpt rel, FuzzyDate start) + return new CalendarEventVM { - if (year != start.Year || start.IsDecade) - return null; + Title = Texts.CalendarPresenter_PetAdoption_Title, + Type = CalendarEventType.PetAdoption, + RelatedPage = new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Event_Title } + }; + } - var child = context.Pages[ - rel.Type == RelationType.StepChild - ? rel.SourceId - : rel.DestinationId - ]; + CalendarEventVM GetChildAdoptionEvent(RelationContext.RelationExcerpt rel, FuzzyDate start) + { + if (year != start.Year || start.IsDecade) + return null; - var title = child.Gender == false - ? Texts.CalendarPresenter_ChildAdoptionF - : Texts.CalendarPresenter_ChildAdoptionM; + var child = context.Pages[ + rel.Type == RelationType.StepChild + ? rel.SourceId + : rel.DestinationId + ]; - return new CalendarEventVM - { - Title = title, - Type = CalendarEventType.ChildAdoption, - RelatedPage = new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Event_Title } - }; - } + var title = child.Gender == false + ? Texts.CalendarPresenter_ChildAdoptionF + : Texts.CalendarPresenter_ChildAdoptionM; + + return new CalendarEventVM + { + Title = title, + Type = CalendarEventType.ChildAdoption, + RelatedPage = new PageTitleExtendedVM { Title = Texts.CalendarPresenter_Event_Title } + }; } + } - /// - /// Gets the range for displaying the events. - /// - private (DateTime from, DateTime to) GetDisplayedRange(int year, int month) - { - var firstMonthDay = CreateDate(year, month); - var firstWeekDay = firstMonthDay.DayOfWeek; - var daysBeforeFirst = firstWeekDay != DayOfWeek.Sunday ? (int) firstWeekDay - 1 : 6; - var firstDay = firstMonthDay.AddDays(-daysBeforeFirst); + /// + /// Gets the range for displaying the events. + /// + private (DateTime from, DateTime to) GetDisplayedRange(int year, int month) + { + var firstMonthDay = CreateDate(year, month); + var firstWeekDay = firstMonthDay.DayOfWeek; + var daysBeforeFirst = firstWeekDay != DayOfWeek.Sunday ? (int) firstWeekDay - 1 : 6; + var firstDay = firstMonthDay.AddDays(-daysBeforeFirst); - var lastMonthDay = firstMonthDay.AddMonths(1).AddDays(-1); - var lastWeekDay = lastMonthDay.DayOfWeek; - var daysAfterLast = lastWeekDay != DayOfWeek.Sunday ? 7 - (int) lastWeekDay : 0; - var lastDay = lastMonthDay.AddDays(daysAfterLast); + var lastMonthDay = firstMonthDay.AddMonths(1).AddDays(-1); + var lastWeekDay = lastMonthDay.DayOfWeek; + var daysAfterLast = lastWeekDay != DayOfWeek.Sunday ? 7 - (int) lastWeekDay : 0; + var lastDay = lastMonthDay.AddDays(daysAfterLast); - return (firstDay, lastDay); - } + return (firstDay, lastDay); + } - /// - /// Returns a proper DateTime, falling back to current month. - /// - private DateTime CreateDate(int year, int month) + /// + /// Returns a proper DateTime, falling back to current month. + /// + private DateTime CreateDate(int year, int month) + { + if(month < 1 || month > 12) { - if(month < 1 || month > 12) - { - var now = DateTime.Now; - return new DateTime(now.Year, now.Month, 1); - } - - return new DateTime(year, month, 1); + var now = DateTime.Now; + return new DateTime(now.Year, now.Month, 1); } - /// - /// Gets the grid of days for current range. - /// - private IReadOnlyList> GetMonthGrid(DateTime from, DateTime to, int month, IEnumerable events) - { - var curr = from; - var weeks = new List>(); - var cache = events.Where(x => x.Day != null) - .GroupBy(x => x.Day.Value) - .ToDictionary(x => x.Key, x => x.ToList()); + return new DateTime(year, month, 1); + } + + /// + /// Gets the grid of days for current range. + /// + private IReadOnlyList> GetMonthGrid(DateTime from, DateTime to, int month, IEnumerable events) + { + var curr = from; + var weeks = new List>(); + var cache = events.Where(x => x.Day != null) + .GroupBy(x => x.Day.Value) + .ToDictionary(x => x.Key, x => x.ToList()); - while (curr < to) + while (curr < to) + { + var week = new List(); + for (var i = 0; i < 7; i++) { - var week = new List(); - for (var i = 0; i < 7; i++) + var day = new CalendarDayVM { Day = curr.Day }; + if (curr.Month == month) { - var day = new CalendarDayVM { Day = curr.Day }; - if (curr.Month == month) - { - day.IsActive = true; - - if (cache.TryGetValue(curr.Day, out var dayEvents)) - day.Events = dayEvents; - } + day.IsActive = true; - week.Add(day); - curr = curr.AddDays(1); + if (cache.TryGetValue(curr.Day, out var dayEvents)) + day.Events = dayEvents; } - weeks.Add(week); + week.Add(day); + curr = curr.AddDays(1); } - return weeks; + weeks.Add(week); } - /// - /// Maps a page excerpt to the page title. - /// - private PageTitleExtendedVM Map(RelationContext.PageExcerpt page) + return weeks; + } + + /// + /// Maps a page excerpt to the page title. + /// + private PageTitleExtendedVM Map(RelationContext.PageExcerpt page) + { + return new PageTitleExtendedVM { - return new PageTitleExtendedVM - { - Id = page.Id, - Key = page.Key, - Title = page.Title, - Type = page.Type, - MainPhotoPath = MediaPresenterService.GetSizedMediaPath(page.MainPhotoPath, MediaSize.Small) - }; - } + Id = page.Id, + Key = page.Key, + Title = page.Title, + Type = page.Type, + MainPhotoPath = MediaPresenterService.GetSizedMediaPath(page.MainPhotoPath, MediaSize.Small) + }; + } - /// - /// Returns the list of events that happened in the current month. - /// - private async Task> GetOneTimeEventsAsync(int year, int month) + /// + /// Returns the list of events that happened in the current month. + /// + private async Task> GetOneTimeEventsAsync(int year, int month) + { + var result = new List(); + var evtPages = await _db.Pages + .Where(x => x.Type == PageType.Event + && x.IsDeleted == false + && x.Facts.Contains("Main.Date")) + .Select(x => new {x.Id, x.Title, x.Key, MainPhotoPath = x.MainPhoto.FilePath, x.Facts}) + .ToListAsync(); + + foreach (var evtPage in evtPages) { - var result = new List(); - var evtPages = await _db.Pages - .Where(x => x.Type == PageType.Event - && x.IsDeleted == false - && x.Facts.Contains("Main.Date")) - .Select(x => new {x.Id, x.Title, x.Key, MainPhotoPath = x.MainPhoto.FilePath, x.Facts}) - .ToListAsync(); - - foreach (var evtPage in evtPages) - { - var facts = JObject.Parse(evtPage.Facts); - var rawDate = facts["Main.Date"]?["Value"]?.ToString(); - var date = FuzzyDate.TryParse(rawDate); - if(date is not { } d) - continue; + var facts = JObject.Parse(evtPage.Facts); + var rawDate = facts["Main.Date"]?["Value"]?.ToString(); + var date = FuzzyDate.TryParse(rawDate); + if(date is not { } d) + continue; - if (d.Year == year && d.Month == month) + if (d.Year == year && d.Month == month) + { + result.Add(new CalendarEventVM { - result.Add(new CalendarEventVM + Type = CalendarEventType.Event, + Title = Texts.Front_Calendar_Event, + Day = d.Day, + RelatedPage = new PageTitleExtendedVM { - Type = CalendarEventType.Event, - Title = Texts.Front_Calendar_Event, - Day = d.Day, - RelatedPage = new PageTitleExtendedVM - { - Type = PageType.Event, - Key = evtPage.Key, - Id = evtPage.Id, - Title = evtPage.Title, - MainPhotoPath = MediaPresenterService.GetSizedMediaPath(evtPage.MainPhotoPath, MediaSize.Small) - } - }); - } + Type = PageType.Event, + Key = evtPage.Key, + Id = evtPage.Id, + Title = evtPage.Title, + MainPhotoPath = MediaPresenterService.GetSizedMediaPath(evtPage.MainPhotoPath, MediaSize.Small) + } + }); } - - return result; } - #endregion + return result; } -} + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/MediaPresenterService.cs b/src/Bonsai/Areas/Front/Logic/MediaPresenterService.cs index 8016af5c..d9ba0b6a 100644 --- a/src/Bonsai/Areas/Front/Logic/MediaPresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/MediaPresenterService.cs @@ -15,179 +15,178 @@ using Bonsai.Data.Models; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Front.Logic +namespace Bonsai.Areas.Front.Logic; + +/// +/// Media displayer service. +/// +public class MediaPresenterService { + public MediaPresenterService(AppDbContext db, MarkdownService markdown) + { + _db = db; + _markdown = markdown; + } + + private readonly AppDbContext _db; + private readonly MarkdownService _markdown; + + #region Methods + /// - /// Media displayer service. + /// Returns the details for a media. /// - public class MediaPresenterService + public async Task GetMediaAsync(string key) { - public MediaPresenterService(AppDbContext db, MarkdownService markdown) - { - _db = db; - _markdown = markdown; - } + var id = PageHelper.GetMediaId(key); + var media = await _db.Media + .Include(x => x.Tags) + .ThenInclude(x => x.Object) + .Where(x => x.IsDeleted == false) + .FirstOrDefaultAsync(x => x.Id == id); - private readonly AppDbContext _db; - private readonly MarkdownService _markdown; + if (media == null) + throw new KeyNotFoundException(); - #region Methods + var descr = await _markdown.CompileAsync(media.Description); - /// - /// Returns the details for a media. - /// - public async Task GetMediaAsync(string key) + return new MediaVM { - var id = PageHelper.GetMediaId(key); - var media = await _db.Media - .Include(x => x.Tags) - .ThenInclude(x => x.Object) - .Where(x => x.IsDeleted == false) - .FirstOrDefaultAsync(x => x.Id == id); - - if (media == null) - throw new KeyNotFoundException(); + Id = media.Id, + Type = media.Type, + IsProcessed = media.IsProcessed, + Title = media.Title, + Description = descr, + Date = FuzzyDate.TryParse(media.Date), + Tags = GetMediaTagsVMs(media.Tags).ToList(), + Event = GetPageTitle(media.Tags.FirstOrDefault(x => x.Type == MediaTagType.Event)), + Location = GetPageTitle(media.Tags.FirstOrDefault(x => x.Type == MediaTagType.Location)), + OriginalPath = media.FilePath, + PreviewPath = GetSizedMediaPath(media.FilePath, MediaSize.Large) + }; + } - var descr = await _markdown.CompileAsync(media.Description); + /// + /// Returns the last N uploaded images. + /// + public async Task> GetLastUploadedMediaAsync(int count) + { + return await _db.Media + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.UploadDate) + .Take(count) + .Select(x => new MediaThumbnailVM + { + Key = x.Key, + Type = x.Type, + ThumbnailUrl = GetSizedMediaPath(x.FilePath, MediaSize.Small), + Date = FuzzyDate.TryParse(x.Date), + IsProcessed = x.IsProcessed + }) + .ToListAsync(); + } - return new MediaVM - { - Id = media.Id, - Type = media.Type, - IsProcessed = media.IsProcessed, - Title = media.Title, - Description = descr, - Date = FuzzyDate.TryParse(media.Date), - Tags = GetMediaTagsVMs(media.Tags).ToList(), - Event = GetPageTitle(media.Tags.FirstOrDefault(x => x.Type == MediaTagType.Event)), - Location = GetPageTitle(media.Tags.FirstOrDefault(x => x.Type == MediaTagType.Location)), - OriginalPath = media.FilePath, - PreviewPath = GetSizedMediaPath(media.FilePath, MediaSize.Large) - }; - } + #endregion - /// - /// Returns the last N uploaded images. - /// - public async Task> GetLastUploadedMediaAsync(int count) - { - return await _db.Media - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.UploadDate) - .Take(count) - .Select(x => new MediaThumbnailVM - { - Key = x.Key, - Type = x.Type, - ThumbnailUrl = GetSizedMediaPath(x.FilePath, MediaSize.Small), - Date = FuzzyDate.TryParse(x.Date), - IsProcessed = x.IsProcessed - }) - .ToListAsync(); - } + #region Private helpers - #endregion + /// + /// Maps a media tag to a page description. + /// + private PageTitleVM GetPageTitle(MediaTag tag) + { + if(tag == null) + return null; - #region Private helpers + return new PageTitleVM + { + Id = tag.ObjectId, + Title = tag.Object?.Title ?? tag.ObjectTitle, + Key = tag.Object?.Key, + Type = tag.Object?.Type ?? PageType.Other + }; + } - /// - /// Maps a media tag to a page description. - /// - private PageTitleVM GetPageTitle(MediaTag tag) + /// + /// Converts the media tag data objects to corresponding VMs. + /// + private IEnumerable GetMediaTagsVMs(IEnumerable tags) + { + RectangleF? ParseRectangle(string str) { - if(tag == null) + if (string.IsNullOrEmpty(str)) return null; - return new PageTitleVM - { - Id = tag.ObjectId, - Title = tag.Object?.Title ?? tag.ObjectTitle, - Key = tag.Object?.Key, - Type = tag.Object?.Type ?? PageType.Other - }; + var coords = str.Split(';') + .Select(x => float.Parse(x, CultureInfo.InvariantCulture)) + .ToList(); + + return new RectangleF( + coords[0], + coords[1], + coords[2], + coords[3] + ); } - /// - /// Converts the media tag data objects to corresponding VMs. - /// - private IEnumerable GetMediaTagsVMs(IEnumerable tags) + foreach (var tag in tags) { - RectangleF? ParseRectangle(string str) - { - if (string.IsNullOrEmpty(str)) - return null; - - var coords = str.Split(';') - .Select(x => float.Parse(x, CultureInfo.InvariantCulture)) - .ToList(); - - return new RectangleF( - coords[0], - coords[1], - coords[2], - coords[3] - ); - } - - foreach (var tag in tags) + if (tag.Type != MediaTagType.DepictedEntity) + continue; + + yield return new MediaTagVM { - if (tag.Type != MediaTagType.DepictedEntity) - continue; - - yield return new MediaTagVM - { - TagId = tag.Id, - Page = GetPageTitle(tag), - Rect = ParseRectangle(tag.Coordinates) - }; - } + TagId = tag.Id, + Page = GetPageTitle(tag), + Rect = ParseRectangle(tag.Coordinates) + }; } + } - #endregion + #endregion - #region Static helpers + #region Static helpers - /// - /// Gets the file path for a media frame of specified size. - /// - public static string GetSizedMediaPath(string fullPath, MediaSize size) - { - if (string.IsNullOrEmpty(fullPath)) - return fullPath; + /// + /// Gets the file path for a media frame of specified size. + /// + public static string GetSizedMediaPath(string fullPath, MediaSize size) + { + if (string.IsNullOrEmpty(fullPath)) + return fullPath; - if (size == MediaSize.Original) - return fullPath; + if (size == MediaSize.Original) + return fullPath; - if (size == MediaSize.Large) - return Path.ChangeExtension(fullPath, ".lg.jpg"); + if (size == MediaSize.Large) + return Path.ChangeExtension(fullPath, ".lg.jpg"); - if (size == MediaSize.Medium) - return Path.ChangeExtension(fullPath, ".md.jpg"); + if (size == MediaSize.Medium) + return Path.ChangeExtension(fullPath, ".md.jpg"); - if (size == MediaSize.Small) - return Path.ChangeExtension(fullPath, ".sm.jpg"); + if (size == MediaSize.Small) + return Path.ChangeExtension(fullPath, ".sm.jpg"); - throw new ArgumentOutOfRangeException(nameof(size), "Unexpected media size!"); - } - - /// - /// Returns the photo model. - /// - public static MediaThumbnailVM GetMediaThumbnail(Media media, MediaSize size = MediaSize.Small) - { - if (media == null) - return null; + throw new ArgumentOutOfRangeException(nameof(size), "Unexpected media size!"); + } - return new MediaThumbnailVM - { - Type = media.Type, - Key = media.Key, - ThumbnailUrl = GetSizedMediaPath(media.FilePath, size), - Date = FuzzyDate.TryParse(media.Date), - IsProcessed = media.IsProcessed - }; - } + /// + /// Returns the photo model. + /// + public static MediaThumbnailVM GetMediaThumbnail(Media media, MediaSize size = MediaSize.Small) + { + if (media == null) + return null; - #endregion + return new MediaThumbnailVM + { + Type = media.Type, + Key = media.Key, + ThumbnailUrl = GetSizedMediaPath(media.FilePath, size), + Date = FuzzyDate.TryParse(media.Date), + IsProcessed = media.IsProcessed + }; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/PagePresenterService.cs b/src/Bonsai/Areas/Front/Logic/PagePresenterService.cs index e0a59210..e78ec91b 100644 --- a/src/Bonsai/Areas/Front/Logic/PagePresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/PagePresenterService.cs @@ -18,256 +18,255 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Front.Logic +namespace Bonsai.Areas.Front.Logic; + +/// +/// Page displayer service. +/// +public class PagePresenterService { - /// - /// Page displayer service. - /// - public class PagePresenterService + public PagePresenterService(AppDbContext db, IMapper mapper, MarkdownService markdown, RelationsPresenterService relations, BonsaiConfigService config) { - public PagePresenterService(AppDbContext db, IMapper mapper, MarkdownService markdown, RelationsPresenterService relations, BonsaiConfigService config) - { - _db = db; - _mapper = mapper; - _markdown = markdown; - _relations = relations; - _config = config; - } - - private readonly AppDbContext _db; - private readonly IMapper _mapper; - private readonly MarkdownService _markdown; - private readonly RelationsPresenterService _relations; - private readonly BonsaiConfigService _config; + _db = db; + _mapper = mapper; + _markdown = markdown; + _relations = relations; + _config = config; + } - #region Public methods + private readonly AppDbContext _db; + private readonly IMapper _mapper; + private readonly MarkdownService _markdown; + private readonly RelationsPresenterService _relations; + private readonly BonsaiConfigService _config; - /// - /// Returns the description VM for a page. - /// - public async Task GetPageDescriptionAsync(string key) - { - var page = await FindPageAsync(key); - return await GetPageDescriptionAsync(page); - } + #region Public methods - /// - /// Returns the description for a constructed page. - /// - public async Task GetPageDescriptionAsync(Page page) - { - var descr = await _markdown.CompileAsync(page.Description); - return Configure(page, new PageDescriptionVM { Description = descr }); - } + /// + /// Returns the description VM for a page. + /// + public async Task GetPageDescriptionAsync(string key) + { + var page = await FindPageAsync(key); + return await GetPageDescriptionAsync(page); + } - /// - /// Returns the list of media files. - /// - public async Task GetPageMediaAsync(string key) - { - var page = await FindPageAsync(key, q => q.Include(p => p.MediaTags) - .ThenInclude(t => t.Media)); + /// + /// Returns the description for a constructed page. + /// + public async Task GetPageDescriptionAsync(Page page) + { + var descr = await _markdown.CompileAsync(page.Description); + return Configure(page, new PageDescriptionVM { Description = descr }); + } - var media = page.MediaTags - .Where(x => x.Media.IsDeleted == false) - .Select(x => MediaPresenterService.GetMediaThumbnail(x.Media, MediaSize.Small)) - .ToList(); + /// + /// Returns the list of media files. + /// + public async Task GetPageMediaAsync(string key) + { + var page = await FindPageAsync(key, q => q.Include(p => p.MediaTags) + .ThenInclude(t => t.Media)); - return Configure(page, new PageMediaVM { Media = media }); - } + var media = page.MediaTags + .Where(x => x.Media.IsDeleted == false) + .Select(x => MediaPresenterService.GetMediaThumbnail(x.Media, MediaSize.Small)) + .ToList(); - /// - /// Returns the tree VM (empty). - /// - public async Task GetPageTreeAsync(string key, TreeKind? kind = null) - { - var configValue = _config.GetDynamicConfig().TreeKinds; - var supportedKinds = Enum.GetValues() - .Where(x => configValue.HasFlag(x)) - .ToList(); + return Configure(page, new PageMediaVM { Media = media }); + } - if (kind == null) - kind = supportedKinds.Count > 0 ? supportedKinds.First() : throw new KeyNotFoundException(); - else if (!Enum.IsDefined(kind.Value)) - throw new KeyNotFoundException(); + /// + /// Returns the tree VM (empty). + /// + public async Task GetPageTreeAsync(string key, TreeKind? kind = null) + { + var configValue = _config.GetDynamicConfig().TreeKinds; + var supportedKinds = Enum.GetValues() + .Where(x => configValue.HasFlag(x)) + .ToList(); - var page = await FindPageAsync(key); - if(page.Type != PageType.Person) - throw new KeyNotFoundException(); + if (kind == null) + kind = supportedKinds.Count > 0 ? supportedKinds.First() : throw new KeyNotFoundException(); + else if (!Enum.IsDefined(kind.Value)) + throw new KeyNotFoundException(); - return Configure(page, new PageTreeVM - { - TreeKind = kind.Value, - SupportedKinds = supportedKinds - }); - } + var page = await FindPageAsync(key); + if(page.Type != PageType.Person) + throw new KeyNotFoundException(); - /// - /// Returns the list of references to current page. - /// - public async Task GetPageReferencesAsync(string key) + return Configure(page, new PageTreeVM { - var page = await FindPageAsync(key, q => q.Include(x => x.References).ThenInclude(x => x.Source)); - var refs = page.References - .Select(x => x.Source) - .Select(x => _mapper.Map(x)) - .OrderBy(x => x.Title) - .ToList(); + TreeKind = kind.Value, + SupportedKinds = supportedKinds + }); + } + + /// + /// Returns the list of references to current page. + /// + public async Task GetPageReferencesAsync(string key) + { + var page = await FindPageAsync(key, q => q.Include(x => x.References).ThenInclude(x => x.Source)); + var refs = page.References + .Select(x => x.Source) + .Select(x => _mapper.Map(x)) + .OrderBy(x => x.Title) + .ToList(); - return Configure(page, new PageReferencesVM {References = refs}); - } + return Configure(page, new PageReferencesVM {References = refs}); + } - /// - /// Returns the data for the page's side block. - /// - public async Task GetPageInfoBlockAsync(string key) - { - var keyLower = key?.ToLowerInvariant(); - var page = await _db.Pages - .AsNoTracking() - .Include(x => x.MainPhoto) - .FirstOrDefaultAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false); + /// + /// Returns the data for the page's side block. + /// + public async Task GetPageInfoBlockAsync(string key) + { + var keyLower = key?.ToLowerInvariant(); + var page = await _db.Pages + .AsNoTracking() + .Include(x => x.MainPhoto) + .FirstOrDefaultAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false); - if (page == null) - throw new KeyNotFoundException(); + if (page == null) + throw new KeyNotFoundException(); - return await GetPageInfoBlockAsync(page); - } + return await GetPageInfoBlockAsync(page); + } - /// - /// Returns the info block for a constructed page. - /// - public async Task GetPageInfoBlockAsync(Page page) + /// + /// Returns the info block for a constructed page. + /// + public async Task GetPageInfoBlockAsync(Page page) + { + var factGroups = GetPersonalFacts(page).ToList(); + var relations = await _relations.GetRelationsForPage(page.Id); + + return new InfoBlockVM { - var factGroups = GetPersonalFacts(page).ToList(); - var relations = await _relations.GetRelationsForPage(page.Id); + Photo = MediaPresenterService.GetMediaThumbnail(page.MainPhoto, MediaSize.Medium), + Facts = factGroups, + RelationGroups = relations, + }; + } - return new InfoBlockVM - { - Photo = MediaPresenterService.GetMediaThumbnail(page.MainPhoto, MediaSize.Medium), - Facts = factGroups, - RelationGroups = relations, - }; - } + /// + /// Returns the list of last N updated pages. + /// + public async Task> GetLastUpdatedPagesAsync(int count) + { + var list = await _db.Pages + .Where(x => !x.IsDeleted) + .OrderByDescending(x => x.LastUpdateDate) + .Take(count) + .ProjectToType(_mapper.Config) + .ToListAsync(); - /// - /// Returns the list of last N updated pages. - /// - public async Task> GetLastUpdatedPagesAsync(int count) - { - var list = await _db.Pages - .Where(x => !x.IsDeleted) - .OrderByDescending(x => x.LastUpdateDate) - .Take(count) - .ProjectToType(_mapper.Config) - .ToListAsync(); + foreach (var elem in list) + elem.MainPhotoPath = MediaPresenterService.GetSizedMediaPath(elem.MainPhotoPath, MediaSize.Small); - foreach (var elem in list) - elem.MainPhotoPath = MediaPresenterService.GetSizedMediaPath(elem.MainPhotoPath, MediaSize.Small); + return list; + } - return list; - } + #endregion - #endregion + #region Helper methods - #region Helper methods + /// + /// Sets additional properties on a page view model. + /// + private T Configure(Page page, T vm) where T : PageTitleVM + { + vm.Id = page.Id; + vm.Title = page.Title; + vm.Key = page.Key; + vm.Type = page.Type; - /// - /// Sets additional properties on a page view model. - /// - private T Configure(Page page, T vm) where T : PageTitleVM - { - vm.Id = page.Id; - vm.Title = page.Title; - vm.Key = page.Key; - vm.Type = page.Type; + return vm; + } - return vm; - } + /// + /// Returns the page by its key. + /// + private async Task FindPageAsync(string key, Func, IQueryable> config = null) + { + var query = _db.Pages + .AsNoTracking() + .Include(x => x.MainPhoto) as IQueryable; - /// - /// Returns the page by its key. - /// - private async Task FindPageAsync(string key, Func, IQueryable> config = null) - { - var query = _db.Pages - .AsNoTracking() - .Include(x => x.MainPhoto) as IQueryable; + if (config != null) + query = config(query); - if (config != null) - query = config(query); + var keyLower = key?.ToLowerInvariant(); + var page = await query.FirstOrDefaultAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false); - var keyLower = key?.ToLowerInvariant(); - var page = await query.FirstOrDefaultAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false); + if (page == null) + throw new KeyNotFoundException(); - if (page == null) - throw new KeyNotFoundException(); + if (page.Key != key) + throw new RedirectRequiredException(page.Key); - if (page.Key != key) - throw new RedirectRequiredException(page.Key); + return page; + } - return page; - } + /// + /// Returns the list of personal facts for a page. + /// + private IEnumerable GetPersonalFacts(Page page) + { + if (string.IsNullOrEmpty(page.Facts)) + yield break; - /// - /// Returns the list of personal facts for a page. - /// - private IEnumerable GetPersonalFacts(Page page) - { - if (string.IsNullOrEmpty(page.Facts)) - yield break; + var pageFacts = JObject.Parse(page.Facts); - var pageFacts = JObject.Parse(page.Facts); + foreach (var group in FactDefinitions.Groups[page.Type]) + { + var factsVms = new List(); - foreach (var group in FactDefinitions.Groups[page.Type]) + foreach (var fact in group.Defs) { - var factsVms = new List(); + var key = group.Id + "." + fact.Id; + var factInfo = pageFacts[key]; - foreach (var fact in group.Defs) - { - var key = group.Id + "." + fact.Id; - var factInfo = pageFacts[key]; + var vm = Deserialize(factInfo, fact.Kind); + if (vm == null) + continue; - var vm = Deserialize(factInfo, fact.Kind); - if (vm == null) - continue; + vm.Definition = fact; - vm.Definition = fact; + if (!vm.IsHidden) + factsVms.Add(vm); + } - if (!vm.IsHidden) - factsVms.Add(vm); - } - - if (factsVms.Count > 0) + if (factsVms.Count > 0) + { + yield return new FactGroupVM { - yield return new FactGroupVM - { - Definition = group, - Facts = factsVms - }; - } + Definition = group, + Facts = factsVms + }; } } + } - /// - /// Attempts to deserialize the fact. - /// Returns null on error. - /// - private FactModelBase Deserialize(JToken json, Type kind) - { - if (json == null) - return null; + /// + /// Attempts to deserialize the fact. + /// Returns null on error. + /// + private FactModelBase Deserialize(JToken json, Type kind) + { + if (json == null) + return null; - try - { - return (FactModelBase) json.ToObject(kind); - } - catch - { - return null; - } + try + { + return (FactModelBase) json.ToObject(kind); + } + catch + { + return null; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationDefinition.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationDefinition.cs index 747575a2..f8147963 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationDefinition.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationDefinition.cs @@ -2,56 +2,55 @@ using System.Linq; using System.Text.RegularExpressions; -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +/// +/// Definition of a single relationship between two pages. +/// +public class RelationDefinition { + public RelationDefinition(string rawPaths, string singularNames, string pluralName = null, RelationDurationDisplayMode? durationMode = null) + { + _singularNames = singularNames.Split('|'); + _pluralName = pluralName; + + RawPaths = rawPaths; + DurationDisplayMode = durationMode; + Paths = Regex.Split(rawPaths, "(?=[+-])").Select(x => new RelationPath(x)).ToList(); + } + + private readonly string[] _singularNames; + private readonly string _pluralName; + + /// + /// Path for current node. + /// + public string RawPaths { get; } + /// - /// Definition of a single relationship between two pages. + /// Parsed path. /// - public class RelationDefinition + public IReadOnlyList Paths { get; } + + /// + /// The mode for displaying a range. + /// + public RelationDurationDisplayMode? DurationDisplayMode { get; } + + /// + /// Returns the corresponding name for a possibly unspecified gender. + /// + public string GetName(int count, bool? isMale) { - public RelationDefinition(string rawPaths, string singularNames, string pluralName = null, RelationDurationDisplayMode? durationMode = null) - { - _singularNames = singularNames.Split('|'); - _pluralName = pluralName; - - RawPaths = rawPaths; - DurationDisplayMode = durationMode; - Paths = Regex.Split(rawPaths, "(?=[+-])").Select(x => new RelationPath(x)).ToList(); - } - - private readonly string[] _singularNames; - private readonly string _pluralName; - - /// - /// Path for current node. - /// - public string RawPaths { get; } - - /// - /// Parsed path. - /// - public IReadOnlyList Paths { get; } - - /// - /// The mode for displaying a range. - /// - public RelationDurationDisplayMode? DurationDisplayMode { get; } - - /// - /// Returns the corresponding name for a possibly unspecified gender. - /// - public string GetName(int count, bool? isMale) - { - if (count > 1) - return _pluralName; - - if (isMale == null && _singularNames.Length > 2) - return _singularNames[2]; - - if (isMale == false && _singularNames.Length > 1) - return _singularNames[1]; - - return _singularNames[0]; - } + if (count > 1) + return _pluralName; + + if (isMale == null && _singularNames.Length > 2) + return _singularNames[2]; + + if (isMale == false && _singularNames.Length > 1) + return _singularNames[1]; + + return _singularNames[0]; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationPath.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationPath.cs index 91f7dc07..2ac3c527 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationPath.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationPath.cs @@ -2,39 +2,38 @@ using System.Collections.Generic; using System.Linq; -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +/// +/// A single path in the definition relation. +/// +public class RelationPath { - /// - /// A single path in the definition relation. - /// - public class RelationPath + public RelationPath(string rawPath) { - public RelationPath(string rawPath) - { - IsExcluded = rawPath[0] == '-'; - rawPath = rawPath.TrimStart('+', '-'); + IsExcluded = rawPath[0] == '-'; + rawPath = rawPath.TrimStart('+', '-'); - IsBound = rawPath[0] == '!'; - rawPath = rawPath.TrimStart('!'); + IsBound = rawPath[0] == '!'; + rawPath = rawPath.TrimStart('!'); - Segments = rawPath.Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Select(x => new RelationPathSegment(x)) - .ToList(); - } + Segments = rawPath.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(x => new RelationPathSegment(x)) + .ToList(); + } - /// - /// Flag indicating that the current path segment must be excluded from the resulting selection. - /// - public bool IsExcluded { get; } + /// + /// Flag indicating that the current path segment must be excluded from the resulting selection. + /// + public bool IsExcluded { get; } - /// - /// Flag indicating that the path is bound by a particular pre-defined node (spouse-based grouping). - /// - public bool IsBound { get; } + /// + /// Flag indicating that the path is bound by a particular pre-defined node (spouse-based grouping). + /// + public bool IsBound { get; } - /// - /// Required path segments. - /// - public IReadOnlyList Segments; - } -} + /// + /// Required path segments. + /// + public IReadOnlyList Segments; +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationPathSegment.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationPathSegment.cs index 02c21a94..568b7619 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationPathSegment.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationPathSegment.cs @@ -1,35 +1,34 @@ using System; using Bonsai.Data.Models; -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +/// +/// A single segment of the path. +/// +public class RelationPathSegment { - /// - /// A single segment of the path. - /// - public class RelationPathSegment + public RelationPathSegment(string part) { - public RelationPathSegment(string part) + var sep = part.IndexOf(':'); + if (sep == -1) { - var sep = part.IndexOf(':'); - if (sep == -1) - { - Type = Enum.Parse(part, true); - } - else - { - Type = Enum.Parse(part.Substring(0, sep), true); - Gender = part[sep + 1] == 'm'; - } + Type = Enum.Parse(part, true); } + else + { + Type = Enum.Parse(part[..sep], true); + Gender = part[sep + 1] == 'm'; + } + } - /// - /// Expected type of the direct relation between pages. - /// - public readonly RelationType Type; + /// + /// Expected type of the direct relation between pages. + /// + public readonly RelationType Type; - /// - /// Flag indicating the expected gender (if it is required). - /// - public readonly bool? Gender; - } -} + /// + /// Flag indicating the expected gender (if it is required). + /// + public readonly bool? Gender; +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationRangeDisplayMode.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationRangeDisplayMode.cs index e6851aa2..daa5ff82 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationRangeDisplayMode.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationRangeDisplayMode.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +/// +/// The way of displaying a date next to a relation. +/// +public enum RelationDurationDisplayMode { /// - /// The way of displaying a date next to a relation. + /// Display the relation range (spouse). /// - public enum RelationDurationDisplayMode - { - /// - /// Display the relation range (spouse). - /// - RelationRange, + RelationRange, - /// - /// Display the birth of the related person (child). - /// - Birth, + /// + /// Display the birth of the related person (child). + /// + Birth, - /// - /// Display the life duration (pet). - /// - Life - } -} + /// + /// Display the life duration (pet). + /// + Life +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.Nested.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.Nested.cs index 196ff459..520815f6 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.Nested.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.Nested.cs @@ -2,33 +2,32 @@ using Bonsai.Code.DomainModel.Relations; using Bonsai.Code.Utils; -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +public partial class RelationsPresenterService { - public partial class RelationsPresenterService + /// + /// Information about a page matching a relation path segment. + /// + private class RelationTarget: IEquatable { - /// - /// Information about a page matching a relation path segment. - /// - private class RelationTarget: IEquatable - { - public readonly RelationContext.PageExcerpt Page; - public readonly RelationContext.RelationExcerpt Relation; - public readonly SinglyLinkedList VisitedPages; + public readonly RelationContext.PageExcerpt Page; + public readonly RelationContext.RelationExcerpt Relation; + public readonly SinglyLinkedList VisitedPages; - public RelationTarget(RelationContext.PageExcerpt page, RelationContext.RelationExcerpt relation, SinglyLinkedList visitedPages) - { - Page = page; - Relation = relation; - VisitedPages = visitedPages; - } + public RelationTarget(RelationContext.PageExcerpt page, RelationContext.RelationExcerpt relation, SinglyLinkedList visitedPages) + { + Page = page; + Relation = relation; + VisitedPages = visitedPages; + } - #region Equality members (auto-generated) + #region Equality members (auto-generated) - public bool Equals(RelationTarget other) => !ReferenceEquals(null, other) && (ReferenceEquals(this, other) || Page.Equals(other.Page)); - public override bool Equals(object obj) => !ReferenceEquals(null, obj) && (ReferenceEquals(this, obj) || obj.GetType() == GetType() && Equals((RelationTarget)obj)); - public override int GetHashCode() => Page.GetHashCode(); + public bool Equals(RelationTarget other) => !ReferenceEquals(null, other) && (ReferenceEquals(this, other) || Page.Equals(other.Page)); + public override bool Equals(object obj) => !ReferenceEquals(null, obj) && (ReferenceEquals(this, obj) || obj.GetType() == GetType() && Equals((RelationTarget)obj)); + public override int GetHashCode() => Page.GetHashCode(); - #endregion - } + #endregion } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.cs b/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.cs index 75733322..e1f0305f 100644 --- a/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/Relations/RelationsPresenterService.cs @@ -12,277 +12,275 @@ using Bonsai.Localization; using Impworks.Utils.Strings; -namespace Bonsai.Areas.Front.Logic.Relations +namespace Bonsai.Areas.Front.Logic.Relations; + +/// +/// The service for calculating relations between pages. +/// +public partial class RelationsPresenterService { - /// - /// The service for calculating relations between pages. - /// - public partial class RelationsPresenterService + public RelationsPresenterService(AppDbContext db) { - public RelationsPresenterService(AppDbContext db) - { - _db = db; - } + _db = db; + } - private readonly AppDbContext _db; + private readonly AppDbContext _db; - #region Relation definitions + #region Relation definitions - /// - /// Relations for parents/siblings group. - /// - public static RelationDefinition[] ParentRelations = - { - new RelationDefinition("Parent:m", Texts.Relations_ParentM), // отец - new RelationDefinition("Parent:f", Texts.Relations_ParentF), // мать - new RelationDefinition("Parent-Parent:m-Parent:f", Texts.Relations_Parent, Texts.Relations_Parent_Mult), // родители - new RelationDefinition("Parent Child:m", Texts.Relations_ParentChildM, Texts.Relations_ParentChildM_Mult), // брат - new RelationDefinition("Parent Child:f", Texts.Relations_ParentChildF, Texts.Relations_ParentChildF_Mult), // сестра - new RelationDefinition("Parent Parent:m", Texts.Relations_ParentParentM, Texts.Relations_ParentParentM_Mult), // бабушка - new RelationDefinition("Parent Parent:f", Texts.Relations_ParentParentF, Texts.Relations_ParentParentF_Mult) // дедушка - }; + /// + /// Relations for parents/siblings group. + /// + public static RelationDefinition[] ParentRelations = + [ + new RelationDefinition("Parent:m", Texts.Relations_ParentM), // отец + new RelationDefinition("Parent:f", Texts.Relations_ParentF), // мать + new RelationDefinition("Parent-Parent:m-Parent:f", Texts.Relations_Parent, Texts.Relations_Parent_Mult), // родители + new RelationDefinition("Parent Child:m", Texts.Relations_ParentChildM, Texts.Relations_ParentChildM_Mult), // брат + new RelationDefinition("Parent Child:f", Texts.Relations_ParentChildF, Texts.Relations_ParentChildF_Mult), // сестра + new RelationDefinition("Parent Parent:m", Texts.Relations_ParentParentM, Texts.Relations_ParentParentM_Mult), // бабушка + new RelationDefinition("Parent Parent:f", Texts.Relations_ParentParentF, Texts.Relations_ParentParentF_Mult) // дедушка + ]; - /// - /// Relations for a spouse-based group. - /// - public static RelationDefinition[] SpouseDefinitions = - { - new RelationDefinition("!Spouse:m", Texts.Relations_SpouseM, null, RelationDurationDisplayMode.RelationRange), // муж - new RelationDefinition("!Spouse:f", Texts.Relations_SpouseF, null, RelationDurationDisplayMode.RelationRange), // жена - new RelationDefinition("!Spouse Child+Child", Texts.Relations_SpouseChild, Texts.Relations_SpouseChild_Mult, RelationDurationDisplayMode.Birth), // родной ребенок с супругом - new RelationDefinition("!Spouse:m Parent:m", Texts.Relations_SpouseMParentM), // свекр - new RelationDefinition("!Spouse:m Parent:f", Texts.Relations_SpouseMParentF), // свекровь - new RelationDefinition("!Spouse:f Parent:m", Texts.Relations_SpouseFParentM), // тесть - new RelationDefinition("!Spouse:f Parent:f", Texts.Relations_SpouseFParentF), // теща - new RelationDefinition("!Spouse:m Parent Child:m", Texts.Relations_SpouseMParentChildM, Texts.Relations_SpouseMParentChildM_Mult), // деверь - new RelationDefinition("!Spouse:m Parent Child:f", Texts.Relations_SpouseMParentChildF, Texts.Relations_SpouseMParentChildF_Mult), // золовка - new RelationDefinition("!Spouse:f Parent Child:m", Texts.Relations_SpouseFParentChildM, Texts.Relations_SpouseFParentChildM_Mult), // шурин - new RelationDefinition("!Spouse:f Parent Child:f", Texts.Relations_SpouseFParentChildF, Texts.Relations_SpouseFParentChildF_Mult) // свояченица - }; + /// + /// Relations for a spouse-based group. + /// + public static RelationDefinition[] SpouseDefinitions = + [ + new RelationDefinition("!Spouse:m", Texts.Relations_SpouseM, null, RelationDurationDisplayMode.RelationRange), // муж + new RelationDefinition("!Spouse:f", Texts.Relations_SpouseF, null, RelationDurationDisplayMode.RelationRange), // жена + new RelationDefinition("!Spouse Child+Child", Texts.Relations_SpouseChild, Texts.Relations_SpouseChild_Mult, RelationDurationDisplayMode.Birth), // родной ребенок с супругом + new RelationDefinition("!Spouse:m Parent:m", Texts.Relations_SpouseMParentM), // свекр + new RelationDefinition("!Spouse:m Parent:f", Texts.Relations_SpouseMParentF), // свекровь + new RelationDefinition("!Spouse:f Parent:m", Texts.Relations_SpouseFParentM), // тесть + new RelationDefinition("!Spouse:f Parent:f", Texts.Relations_SpouseFParentF), // теща + new RelationDefinition("!Spouse:m Parent Child:m", Texts.Relations_SpouseMParentChildM, Texts.Relations_SpouseMParentChildM_Mult), // деверь + new RelationDefinition("!Spouse:m Parent Child:f", Texts.Relations_SpouseMParentChildF, Texts.Relations_SpouseMParentChildF_Mult), // золовка + new RelationDefinition("!Spouse:f Parent Child:m", Texts.Relations_SpouseFParentChildM, Texts.Relations_SpouseFParentChildM_Mult), // шурин + new RelationDefinition("!Spouse:f Parent Child:f", Texts.Relations_SpouseFParentChildF, Texts.Relations_SpouseFParentChildF_Mult) // свояченица + ]; - /// - /// Other relations for family members. - /// - public static RelationDefinition[] OtherRelativeRelations = - { - new RelationDefinition("Child-Spouse Child", Texts.Relations_SpouseOtherChild, Texts.Relations_SpouseOtherChild_Mult, RelationDurationDisplayMode.Birth), // ребенок не от супруга - new RelationDefinition("Child Child", Texts.Relations_ChildChild, Texts.Relations_ChildChild_Mult, RelationDurationDisplayMode.Birth), // внук - new RelationDefinition("Child:f Spouse:m", Texts.Relations_ChildFSpouseM, Texts.Relations_ChildFSpouseM_Mult), // зять - new RelationDefinition("Child:m Spouse:f", Texts.Relations_ChildMSpouseF, Texts.Relations_ChildMSpouseF_Mult), // невестка - new RelationDefinition("Pet", Texts.Relations_Pet, Texts.Relations_Pet_Mult), // питомец - }; + /// + /// Other relations for family members. + /// + public static RelationDefinition[] OtherRelativeRelations = + [ + new RelationDefinition("Child-Spouse Child", Texts.Relations_SpouseOtherChild, Texts.Relations_SpouseOtherChild_Mult, RelationDurationDisplayMode.Birth), // ребенок не от супруга + new RelationDefinition("Child Child", Texts.Relations_ChildChild, Texts.Relations_ChildChild_Mult, RelationDurationDisplayMode.Birth), // внук + new RelationDefinition("Child:f Spouse:m", Texts.Relations_ChildFSpouseM, Texts.Relations_ChildFSpouseM_Mult), // зять + new RelationDefinition("Child:m Spouse:f", Texts.Relations_ChildMSpouseF, Texts.Relations_ChildMSpouseF_Mult), // невестка + new RelationDefinition("Pet", Texts.Relations_Pet, Texts.Relations_Pet_Mult) // питомец + ]; - /// - /// Relations for other people. - /// - private static RelationDefinition[] NonRelativeRelations = - { - new RelationDefinition("Friend", Texts.Relations_Friend, Texts.Relations_Friend_Mult), // друг - new RelationDefinition("Colleague", Texts.Relations_Colleague, Texts.Relations_Colleague_Mult), // коллега - new RelationDefinition("Owner", Texts.Relations_Owner, Texts.Relations_Owner_Mult, RelationDurationDisplayMode.RelationRange), // владелец - new RelationDefinition("EventVisitor", Texts.Relations_EventVisitor, Texts.Relations_EventVisitor_Mult), // участник - new RelationDefinition("LocationInhabitant", Texts.Relations_LocationInhabitant, Texts.Relations_LocationInhabitant_Mult), // житель - }; + /// + /// Relations for other people. + /// + private static RelationDefinition[] NonRelativeRelations = + [ + new RelationDefinition("Friend", Texts.Relations_Friend, Texts.Relations_Friend_Mult), // друг + new RelationDefinition("Colleague", Texts.Relations_Colleague, Texts.Relations_Colleague_Mult), // коллега + new RelationDefinition("Owner", Texts.Relations_Owner, Texts.Relations_Owner_Mult, RelationDurationDisplayMode.RelationRange), // владелец + new RelationDefinition("EventVisitor", Texts.Relations_EventVisitor, Texts.Relations_EventVisitor_Mult), // участник + new RelationDefinition("LocationInhabitant", Texts.Relations_LocationInhabitant, Texts.Relations_LocationInhabitant_Mult) // житель + ]; - /// - /// Relations for non-human pages. - /// - private static RelationDefinition[] NonHumanRelations = - { - new RelationDefinition("Location", Texts.Relations_Location, Texts.Relations_Location_Mult), // место - new RelationDefinition("Event", Texts.Relations_Event, Texts.Relations_Event_Mult), // событие - }; + /// + /// Relations for non-human pages. + /// + private static RelationDefinition[] NonHumanRelations = + [ + new RelationDefinition("Location", Texts.Relations_Location, Texts.Relations_Location_Mult), // место + new RelationDefinition("Event", Texts.Relations_Event, Texts.Relations_Event_Mult) // событие + ]; - #endregion + #endregion - #region Public methods + #region Public methods - /// - /// Returns the list of all inferred relation groups for the page. - /// - public async Task> GetRelationsForPage(Guid pageId) - { - var ctx = await RelationContext.LoadContextAsync(_db); + /// + /// Returns the list of all inferred relation groups for the page. + /// + public async Task> GetRelationsForPage(Guid pageId) + { + var ctx = await RelationContext.LoadContextAsync(_db); - var cats = new [] + var cats = new [] + { + new RelationCategoryVM { - new RelationCategoryVM - { - Title = Texts.Relations_Group_Relatives, - IsMain = true, - Groups = GetGroups(ctx, pageId, ParentRelations) - .Concat(GetSpouseGroups(ctx, pageId)) - .Concat(GetGroups(ctx, pageId, OtherRelativeRelations)) - .ToList() - }, - new RelationCategoryVM - { - Title = Texts.Relations_Group_People, - Groups = GetGroups(ctx, pageId, NonRelativeRelations).ToList(), - }, - new RelationCategoryVM - { - Title = Texts.Relations_Group_Pages, - Groups = GetGroups(ctx, pageId, NonHumanRelations).ToList(), - } - }; + Title = Texts.Relations_Group_Relatives, + IsMain = true, + Groups = GetGroups(ctx, pageId, ParentRelations) + .Concat(GetSpouseGroups(ctx, pageId)) + .Concat(GetGroups(ctx, pageId, OtherRelativeRelations)) + .ToList() + }, + new RelationCategoryVM + { + Title = Texts.Relations_Group_People, + Groups = GetGroups(ctx, pageId, NonRelativeRelations).ToList(), + }, + new RelationCategoryVM + { + Title = Texts.Relations_Group_Pages, + Groups = GetGroups(ctx, pageId, NonHumanRelations).ToList(), + } + }; - return cats.Where(x => x.Groups.Any()).ToList(); - } + return cats.Where(x => x.Groups.Any()).ToList(); + } - #endregion + #endregion - #region Private helpers + #region Private helpers - /// - /// Returns the relation groups with parents and siblings. - /// - private IEnumerable GetGroups(RelationContext ctx, Guid pageId, RelationDefinition[] defs) - { - var ids = new[] {pageId}; - var relations = defs.Select(x => GetRelationVM(ctx, x, ids)) - .Where(x => x != null) - .ToList(); + /// + /// Returns the relation groups with parents and siblings. + /// + private IEnumerable GetGroups(RelationContext ctx, Guid pageId, RelationDefinition[] defs) + { + var ids = new[] {pageId}; + var relations = defs.Select(x => GetRelationVM(ctx, x, ids)) + .Where(x => x != null) + .ToList(); - if (relations.Any()) - yield return new RelationGroupVM {Relations = relations}; - } + if (relations.Any()) + yield return new RelationGroupVM {Relations = relations}; + } - /// - /// Returns the groups for each spouse-based family. - /// - private IEnumerable GetSpouseGroups(RelationContext ctx, Guid pageId) - { - if (!ctx.Pages.TryGetValue(pageId, out var page)) - yield break; + /// + /// Returns the groups for each spouse-based family. + /// + private IEnumerable GetSpouseGroups(RelationContext ctx, Guid pageId) + { + if (!ctx.Pages.TryGetValue(pageId, out var page)) + yield break; - if (page.Type != PageType.Person && page.Type != PageType.Pet) - yield break; + if (page.Type != PageType.Person && page.Type != PageType.Pet) + yield break; - if (!ctx.Relations.ContainsKey(pageId)) - yield break; + if (!ctx.Relations.ContainsKey(pageId)) + yield break; - var spouses = ctx.Relations[pageId] - .Where(x => x.Type == RelationType.Spouse) - .OrderBy(x => x.Duration); + var spouses = ctx.Relations[pageId] + .Where(x => x.Type == RelationType.Spouse) + .OrderBy(x => x.Duration); - foreach (var spouse in spouses) - { - var ids = new[] {pageId, spouse.DestinationId}; - var relations = SpouseDefinitions.Select(x => GetRelationVM(ctx, x, ids)) - .Where(x => x != null) - .ToList(); + foreach (var spouse in spouses) + { + var ids = new[] {pageId, spouse.DestinationId}; + var relations = SpouseDefinitions.Select(x => GetRelationVM(ctx, x, ids)) + .Where(x => x != null) + .ToList(); - if (relations.Any()) - yield return new RelationGroupVM {Relations = relations}; - } + if (relations.Any()) + yield return new RelationGroupVM {Relations = relations}; } + } - /// - /// Returns a relation for all pages matching the definition. - /// - private RelationVM GetRelationVM(RelationContext ctx, RelationDefinition def, params Guid[] guids) + /// + /// Returns a relation for all pages matching the definition. + /// + private RelationVM GetRelationVM(RelationContext ctx, RelationDefinition def, params Guid[] guids) + { + // Performs one step from the current page along the relation path and returns matching pages + IEnumerable Step(RelationTarget elem, RelationPathSegment segment, Guid? guidFilter) { - // Performs one step from the current page along the relation path and returns matching pages - IEnumerable Step(RelationTarget elem, RelationPathSegment segment, Guid? guidFilter) - { - if(!ctx.Relations.TryGetValue(elem.Page.Id, out var rels)) - return Enumerable.Empty(); - - return from rel in rels - where rel.Type == segment.Type - where guidFilter == null || rel.DestinationId == guidFilter - let page = ctx.Pages[rel.DestinationId] - where segment.Gender == null || segment.Gender == page.Gender - where !elem.VisitedPages.Contains(page) - select new RelationTarget(page, rel, elem.VisitedPages.Append(page)); - } + if(!ctx.Relations.TryGetValue(elem.Page.Id, out var rels)) + return Enumerable.Empty(); + + return from rel in rels + where rel.Type == segment.Type + where guidFilter == null || rel.DestinationId == guidFilter + let page = ctx.Pages[rel.DestinationId] + where segment.Gender == null || segment.Gender == page.Gender + where !elem.VisitedPages.Contains(page) + select new RelationTarget(page, rel, elem.VisitedPages.Append(page)); + } - // Finds pages matching the entire path from current page - IEnumerable GetMatchingPages(RelationPath path) - { - if (!ctx.Pages.TryGetValue(guids[0], out var root)) - return Array.Empty(); - - var currents = new List {new RelationTarget(root, null, new SinglyLinkedList(root))}; + // Finds pages matching the entire path from current page + IEnumerable GetMatchingPages(RelationPath path) + { + if (!ctx.Pages.TryGetValue(guids[0], out var root)) + return Array.Empty(); - for (var depth = 0; depth < path.Segments.Count; depth++) - { - if (currents.Count == 0) - break; + var currents = new List {new RelationTarget(root, null, new SinglyLinkedList(root))}; - var segment = path.Segments[depth]; - var guidFilter = path.IsBound && (depth + 1) < guids.Length - ? guids[depth + 1] - : (Guid?) null; + for (var depth = 0; depth < path.Segments.Count; depth++) + { + if (currents.Count == 0) + break; - currents = currents.Select(x => Step(x, segment, guidFilter)) - .SelectMany(x => x) - .ToList(); - } + var segment = path.Segments[depth]; + var guidFilter = path.IsBound && (depth + 1) < guids.Length + ? guids[depth + 1] + : (Guid?) null; - return currents; + currents = currents.Select(x => Step(x, segment, guidFilter)) + .SelectMany(x => x) + .ToList(); } - - // Gets the range to display alongside the relation - FuzzyRange? GetRange(RelationTarget elem) - { - if (def.DurationDisplayMode == RelationDurationDisplayMode.RelationRange) - return elem.Relation.Duration; - if (def.DurationDisplayMode == RelationDurationDisplayMode.Birth) - if (elem.Page.BirthDate != null) - return new FuzzyRange(elem.Page.BirthDate, null); + return currents; + } + + // Gets the range to display alongside the relation + FuzzyRange? GetRange(RelationTarget elem) + { + if (def.DurationDisplayMode == RelationDurationDisplayMode.RelationRange) + return elem.Relation.Duration; - if (def.DurationDisplayMode == RelationDurationDisplayMode.Life) - if(elem.Page.BirthDate != null || elem.Page.DeathDate != null) - return new FuzzyRange(elem.Page.BirthDate, elem.Page.DeathDate); + if (def.DurationDisplayMode == RelationDurationDisplayMode.Birth) + if (elem.Page.BirthDate != null) + return new FuzzyRange(elem.Page.BirthDate, null); - return null; - } + if (def.DurationDisplayMode == RelationDurationDisplayMode.Life) + if(elem.Page.BirthDate != null || elem.Page.DeathDate != null) + return new FuzzyRange(elem.Page.BirthDate, elem.Page.DeathDate); - PageTitleVM GetEventPageTitle(Guid? eventId) - { - if (eventId == null) - return null; - - var page = ctx.Pages[eventId.Value]; - return new PageTitleVM - { - Title = page.Title, - Key = page.Key - }; - } - - var posPaths = def.Paths.Where(x => !x.IsExcluded); - var negPaths = def.Paths.Where(x => x.IsExcluded); + return null; + } - // A+B-C means: all pages matching both paths A & B, but not matching path C - var results = posPaths.Select(GetMatchingPages) - .Aggregate((a, b) => a.Intersect(b)) - .Except(negPaths.Select(GetMatchingPages) - .SelectMany(x => x)) - .ToList(); - - if(!results.Any()) + PageTitleVM GetEventPageTitle(Guid? eventId) + { + if (eventId == null) return null; - return new RelationVM + var page = ctx.Pages[eventId.Value]; + return new PageTitleVM { - Title = def.GetName(results.Count, results[0].Page.Gender), - Pages = results.OrderBy(x => x.Page.BirthDate) - .Select(elem => new RelatedPageVM - { - Title = StringHelper.Coalesce(elem.Page.ShortName, elem.Page.Title), - Key = elem.Page.Key, - Duration = GetRange(elem), - RelationEvent = GetEventPageTitle(elem.Relation.EventId) - }) - .ToList() + Title = page.Title, + Key = page.Key }; } - #endregion + var posPaths = def.Paths.Where(x => !x.IsExcluded); + var negPaths = def.Paths.Where(x => x.IsExcluded); + + // A+B-C means: all pages matching both paths A & B, but not matching path C + var results = posPaths.Select(GetMatchingPages) + .Aggregate((a, b) => a.Intersect(b)) + .Except(negPaths.Select(GetMatchingPages) + .SelectMany(x => x)) + .ToList(); + + if(!results.Any()) + return null; + + return new RelationVM + { + Title = def.GetName(results.Count, results[0].Page.Gender), + Pages = results.OrderBy(x => x.Page.BirthDate) + .Select(elem => new RelatedPageVM + { + Title = StringHelper.Coalesce(elem.Page.ShortName, elem.Page.Title), + Key = elem.Page.Key, + Duration = GetRange(elem), + RelationEvent = GetEventPageTitle(elem.Relation.EventId) + }) + .ToList() + }; } -} + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/SearchPresenterService.cs b/src/Bonsai/Areas/Front/Logic/SearchPresenterService.cs index a5f904af..086cc046 100644 --- a/src/Bonsai/Areas/Front/Logic/SearchPresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/SearchPresenterService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bonsai.Areas.Front.ViewModels.Page; @@ -9,91 +10,90 @@ using MapsterMapper; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Areas.Front.Logic +namespace Bonsai.Areas.Front.Logic; + +/// +/// The presenter for search results. +/// +public class SearchPresenterService { - /// - /// The presenter for search results. - /// - public class SearchPresenterService + public SearchPresenterService(AppDbContext db, ISearchEngine search, IMapper mapper) { - public SearchPresenterService(AppDbContext db, ISearchEngine search, IMapper mapper) - { - _db = db; - _search = search; - _mapper = mapper; - } + _db = db; + _search = search; + _mapper = mapper; + } - private readonly AppDbContext _db; - private readonly ISearchEngine _search; - private readonly IMapper _mapper; + private readonly AppDbContext _db; + private readonly ISearchEngine _search; + private readonly IMapper _mapper; - private const int MIN_QUERY_LENGTH = 3; + private const int MIN_QUERY_LENGTH = 3; - /// - /// Returns the exact match if one exists. - /// - public async Task FindExactAsync(string query) - { - return await _db.Pages - .Where(x => x.IsDeleted == false && x.Aliases.Any(y => y.Title == query)) - .ProjectToType(_mapper.Config) - .FirstOrDefaultAsync(); - } + /// + /// Returns the exact match if one exists. + /// + public async Task FindExactAsync(string query) + { + return await _db.Pages + .Where(x => x.IsDeleted == false && x.Aliases.Any(y => y.Title == query)) + .ProjectToType(_mapper.Config) + .FirstOrDefaultAsync(); + } - /// - /// Find pages matching the query. - /// - public async Task> SearchAsync(string query, int page = 0) - { - var q = (query ?? "").Trim(); - if(q.Length < MIN_QUERY_LENGTH) - return new SearchResultVM[0]; + /// + /// Find pages matching the query. + /// + public async Task> SearchAsync(string query, int page = 0) + { + var q = (query ?? "").Trim(); + if(q.Length < MIN_QUERY_LENGTH) + return Array.Empty(); - var matches = await _search.SearchAsync(q, page); - var ids = matches.Select(x => x.Id); + var matches = await _search.SearchAsync(q, page); + var ids = matches.Select(x => x.Id); - var details = await _db.Pages - .Where(x => ids.Contains(x.Id) && x.IsDeleted == false) - .Select(x => new { x.Id, x.MainPhoto.FilePath, x.LastUpdateDate, PageType = x.Type }) - .ToDictionaryAsync(x => x.Id, x => x); + var details = await _db.Pages + .Where(x => ids.Contains(x.Id) && x.IsDeleted == false) + .Select(x => new { x.Id, x.MainPhoto.FilePath, x.LastUpdateDate, PageType = x.Type }) + .ToDictionaryAsync(x => x.Id, x => x); - var results = matches - .Where(x => details.ContainsKey(x.Id)) - .Select(x => new SearchResultVM - { - Id = x.Id, - Key = x.Key, - Title = x.Title, - HighlightedTitle = x.HighlightedTitle, - Type = details[x.Id].PageType, - DescriptionExcerpt = x.HighlightedDescription, - MainPhotoPath = details[x.Id].FilePath, - LastUpdateDate = details[x.Id].LastUpdateDate, - }); + var results = matches + .Where(x => details.ContainsKey(x.Id)) + .Select(x => new SearchResultVM + { + Id = x.Id, + Key = x.Key, + Title = x.Title, + HighlightedTitle = x.HighlightedTitle, + Type = details[x.Id].PageType, + DescriptionExcerpt = x.HighlightedDescription, + MainPhotoPath = details[x.Id].FilePath, + LastUpdateDate = details[x.Id].LastUpdateDate, + }); - return results.ToList(); - } + return results.ToList(); + } - /// - /// Shows autocomplete suggestions for the search box. - /// - public async Task> SuggestAsync(string query) - { - var q = (query ?? "").Trim(); - if(q.Length < MIN_QUERY_LENGTH) - return new PageTitleVM[0]; + /// + /// Shows autocomplete suggestions for the search box. + /// + public async Task> SuggestAsync(string query) + { + var q = (query ?? "").Trim(); + if(q.Length < MIN_QUERY_LENGTH) + return Array.Empty(); - var results = await _search.SuggestAsync(q, maxCount: 10); + var results = await _search.SuggestAsync(q, maxCount: 10); - return results.Select(x => new SearchResultVM - { - Id = x.Id, - Title = x.Title, - HighlightedTitle = x.HighlightedTitle, - Key = x.Key, - Type = x.PageType - }) - .ToList(); - } + return results.Select(x => new SearchResultVM + { + Id = x.Id, + Title = x.Title, + HighlightedTitle = x.HighlightedTitle, + Key = x.Key, + Type = x.PageType + }) + .ToList(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Logic/TreePresenterService.cs b/src/Bonsai/Areas/Front/Logic/TreePresenterService.cs index 8997890d..f4fd37ea 100644 --- a/src/Bonsai/Areas/Front/Logic/TreePresenterService.cs +++ b/src/Bonsai/Areas/Front/Logic/TreePresenterService.cs @@ -10,73 +10,72 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Front.Logic +namespace Bonsai.Areas.Front.Logic; + +/// +/// The presenter for tree elements. +/// +public class TreePresenterService { - /// - /// The presenter for tree elements. - /// - public class TreePresenterService - { - #region Constructor + #region Constructor - public TreePresenterService(AppDbContext db, IUrlHelper url) - { - _db = db; - _url = url; - } + public TreePresenterService(AppDbContext db, IUrlHelper url) + { + _db = db; + _url = url; + } - #endregion + #endregion - #region Fields + #region Fields - private readonly AppDbContext _db; - private readonly IUrlHelper _url; + private readonly AppDbContext _db; + private readonly IUrlHelper _url; - #endregion + #endregion - #region Public methods + #region Public methods - /// - /// Returns the entire tree. - /// - public async Task GetTreeAsync(string key, TreeKind kind) - { - var keyLower = key?.ToLowerInvariant(); + /// + /// Returns the entire tree. + /// + public async Task GetTreeAsync(string key, TreeKind kind) + { + var keyLower = key?.ToLowerInvariant(); - var page = await _db.Pages - .AsNoTracking() - .Include(x => x.TreeLayout) - .GetAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false, Texts.Global_Error_PageNotFound); + var page = await _db.Pages + .AsNoTracking() + .Include(x => x.TreeLayout) + .GetAsync(x => x.Aliases.Any(y => y.Key == keyLower) && x.IsDeleted == false, Texts.Global_Error_PageNotFound); - var result = new TreeVM {RootId = page.Id}; - var json = await GetLayoutJsonAsync(); - if (!string.IsNullOrEmpty(json)) + var result = new TreeVM {RootId = page.Id}; + var json = await GetLayoutJsonAsync(); + if (!string.IsNullOrEmpty(json)) + { + result.Content = JObject.Parse(json); + foreach (var child in result.Content["children"]) { - result.Content = JObject.Parse(json); - foreach (var child in result.Content["children"]) - { - var info = child["info"]; - if (info == null) - continue; + var info = child["info"]; + if (info == null) + continue; - info["Photo"] = _url.Content(info["Photo"].Value()); - info["Url"] = _url.Action("Description", "Page", new {area = "Front", key = info["Url"].Value()}); - } + info["Photo"] = _url.Content(info["Photo"].Value()); + info["Url"] = _url.Action("Description", "Page", new {area = "Front", key = info["Url"].Value()}); } + } - return result; + return result; - async Task GetLayoutJsonAsync() - { - if (kind == TreeKind.FullTree) - return page.TreeLayout?.LayoutJson; + async Task GetLayoutJsonAsync() + { + if (kind == TreeKind.FullTree) + return page.TreeLayout?.LayoutJson; - var layout = await _db.TreeLayouts - .FirstOrDefaultAsync(x => x.Kind == kind && x.PageId == page.Id); - return layout?.LayoutJson ?? throw new OperationException(Texts.Global_Error_PageNotFound); - } + var layout = await _db.TreeLayouts + .FirstOrDefaultAsync(x => x.Kind == kind && x.PageId == page.Id); + return layout?.LayoutJson ?? throw new OperationException(Texts.Global_Error_PageNotFound); } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/AuthProviderVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/AuthProviderVM.cs index 0fb62f85..0a24635e 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/AuthProviderVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/AuthProviderVM.cs @@ -2,31 +2,30 @@ using Bonsai.Code.Services.Config; using Microsoft.AspNetCore.Authentication; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Information about an authorization provider. +/// +public class AuthProviderVM { /// - /// Information about an authorization provider. + /// Internal name of the provider. /// - public class AuthProviderVM - { - /// - /// Internal name of the provider. - /// - public string Key { get; set; } + public string Key { get; set; } - /// - /// CSS name of the class for the button. - /// - public string IconClass { get; set; } + /// + /// CSS name of the class for the button. + /// + public string IconClass { get; set; } - /// - /// Readable name of the caption. - /// - public string Caption { get; set; } + /// + /// Readable name of the caption. + /// + public string Caption { get; set; } - /// - /// The handler for activating current provider. - /// - public Func TryActivate { get; set; } - } -} + /// + /// The handler for activating current provider. + /// + public Func TryActivate { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/LocalLoginVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/LocalLoginVM.cs index e6c38f42..95bb59f3 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/LocalLoginVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/LocalLoginVM.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Credentials for local authorization. +/// +public class LocalLoginVM { /// - /// Credentials for local authorization. + /// Account login. /// - public class LocalLoginVM - { - /// - /// Account login. - /// - public string Login { get; set; } + public string Login { get; set; } - /// - /// Account password. - /// - public string Password { get; set; } + /// + /// Account password. + /// + public string Password { get; set; } - /// - /// URL to redirect to after logging in. - /// - public string ReturnUrl { get; set; } - } -} + /// + /// URL to redirect to after logging in. + /// + public string ReturnUrl { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginDataVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginDataVM.cs index 4fd8a2e6..9e93ce80 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginDataVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginDataVM.cs @@ -1,45 +1,44 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Information about the login state. +/// +public class LoginDataVM { /// - /// Information about the login state. + /// URL to return to after a successful authorization. + /// + public string ReturnUrl { get; set; } + + /// + /// Current status. + /// + public LoginStatus? Status { get; set; } + + /// + /// Flag indicating that there are no users in the database and current user will register as first. + /// + public bool IsFirstUser { get; set; } + + /// + /// Flag indicating that unauthorized visitors can view page contents. + /// + public bool AllowGuests { get; set; } + + /// + /// Flag indicating that login-password auth is enabled. + /// + public bool AllowPasswordAuth { get; set; } + + /// + /// Flag indicating that new registrations are allowed. + /// + public bool AllowRegistration { get; set; } + + /// + /// List of enabled authentication providers. /// - public class LoginDataVM - { - /// - /// URL to return to after a successful authorization. - /// - public string ReturnUrl { get; set; } - - /// - /// Current status. - /// - public LoginStatus? Status { get; set; } - - /// - /// Flag indicating that there are no users in the database and current user will register as first. - /// - public bool IsFirstUser { get; set; } - - /// - /// Flag indicating that unauthorized visitors can view page contents. - /// - public bool AllowGuests { get; set; } - - /// - /// Flag indicating that login-password auth is enabled. - /// - public bool AllowPasswordAuth { get; set; } - - /// - /// Flag indicating that new registrations are allowed. - /// - public bool AllowRegistration { get; set; } - - /// - /// List of enabled authentication providers. - /// - public IEnumerable Providers { get; set; } - } -} + public IEnumerable Providers { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginResultVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginResultVM.cs index a64a9bb8..b08e38fb 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginResultVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginResultVM.cs @@ -2,37 +2,36 @@ using Bonsai.Areas.Front.Logic.Auth; using Microsoft.AspNetCore.Identity; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Result of an authentication attempt. +/// +public class LoginResultVM { - /// - /// Result of an authentication attempt. - /// - public class LoginResultVM + public LoginResultVM(LoginStatus status, ExternalLoginInfo extLogin = null) { - public LoginResultVM(LoginStatus status, ExternalLoginInfo extLogin = null) - { - Status = status; + Status = status; - if (extLogin != null) - { - Principal = extLogin.Principal; - ExternalLogin = new ExternalLoginData(extLogin.LoginProvider, extLogin.ProviderKey); - } + if (extLogin != null) + { + Principal = extLogin.Principal; + ExternalLogin = new ExternalLoginData(extLogin.LoginProvider, extLogin.ProviderKey); } + } - /// - /// Status of the operation. - /// - public LoginStatus Status { get; } + /// + /// Status of the operation. + /// + public LoginStatus Status { get; } - /// - /// Newly authenticated user (if the log-in was successful). - /// - public ClaimsPrincipal Principal { get; } + /// + /// Newly authenticated user (if the log-in was successful). + /// + public ClaimsPrincipal Principal { get; } - /// - /// Information about the external login. - /// - public ExternalLoginData ExternalLogin { get; } - } -} + /// + /// Information about the external login. + /// + public ExternalLoginData ExternalLogin { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginStatus.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginStatus.cs index 44c006b5..c6692493 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/LoginStatus.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/LoginStatus.cs @@ -1,33 +1,32 @@ -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Types of authorization result. +/// +public enum LoginStatus { /// - /// Types of authorization result. + /// The auth credentials are invalid. /// - public enum LoginStatus - { - /// - /// The auth credentials are invalid. - /// - Failed, + Failed, - /// - /// User has been authorized successfully. - /// - Succeeded, + /// + /// User has been authorized successfully. + /// + Succeeded, - /// - /// Administrator has blocked this account. - /// - LockedOut, + /// + /// Administrator has blocked this account. + /// + LockedOut, - /// - /// The user account is not yet validated by the admin. - /// - Unvalidated, + /// + /// The user account is not yet validated by the admin. + /// + Unvalidated, - /// - /// The user account has just been created, but validation is pending. - /// - NewUser - } -} + /// + /// The user account has just been created, but validation is pending. + /// + NewUser +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserDataVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserDataVM.cs index 67349e43..81430bad 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserDataVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserDataVM.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Additional options for the user registration form. +/// +public class RegisterUserDataVM { /// - /// Additional options for the user registration form. + /// Flag indicating that this created user is the first one (and will be made an admin). /// - public class RegisterUserDataVM - { - /// - /// Flag indicating that this created user is the first one (and will be made an admin). - /// - public bool IsFirstUser { get; set; } + public bool IsFirstUser { get; set; } - /// - /// Flag indicating that local auth has been chosen and the user must provide a login and password. - /// - public bool UsePasswordAuth { get; set; } - } -} + /// + /// Flag indicating that local auth has been chosen and the user must provide a login and password. + /// + public bool UsePasswordAuth { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserResultVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserResultVM.cs index 47214ffc..35a7e412 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserResultVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserResultVM.cs @@ -1,27 +1,26 @@ using System.Security.Claims; using Bonsai.Data.Models; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Result of the user's creation request. +/// +public class RegisterUserResultVM { /// - /// Result of the user's creation request. + /// Flag indicating the user's validation state. + /// The first user is automatically validated. /// - public class RegisterUserResultVM - { - /// - /// Flag indicating the user's validation state. - /// The first user is automatically validated. - /// - public bool IsValidated { get; set; } + public bool IsValidated { get; set; } - /// - /// The created user profile. - /// - public AppUser User { get; set; } + /// + /// The created user profile. + /// + public AppUser User { get; set; } - /// - /// The principal for newly created user. - /// - public ClaimsPrincipal Principal { get; set; } - } -} + /// + /// The principal for newly created user. + /// + public ClaimsPrincipal Principal { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserVM.cs index b60c84a6..c2ab1325 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/RegisterUserVM.cs @@ -5,77 +5,76 @@ using Bonsai.Localization; using Mapster; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Information for registering a new user. +/// +public class RegisterUserVM: IMapped { /// - /// Information for registering a new user. + /// The email address. /// - public class RegisterUserVM: IMapped - { - /// - /// The email address. - /// - [StringLength(255)] - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_EmailEmpty")] - [EmailAddress(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_EmailInvalid")] - public string Email { get; set; } + [StringLength(255)] + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_EmailEmpty")] + [EmailAddress(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_EmailInvalid")] + public string Email { get; set; } - /// - /// First name. - /// - [StringLength(256)] - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_FirstNameEmpty")] - public string FirstName { get; set; } + /// + /// First name. + /// + [StringLength(256)] + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_FirstNameEmpty")] + public string FirstName { get; set; } - /// - /// Last name. - /// - [StringLength(256)] - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_LastNameEmpty")] - public string LastName { get; set; } + /// + /// Last name. + /// + [StringLength(256)] + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_LastNameEmpty")] + public string LastName { get; set; } - /// - /// Middle name. - /// - [StringLength(256)] - public string MiddleName { get; set; } + /// + /// Middle name. + /// + [StringLength(256)] + public string MiddleName { get; set; } - /// - /// Birthday. - /// - [StringLength(10)] - [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_BirthdayEmpty")] - [RegularExpression("[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}", ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_BirthdayInvalid")] - public string Birthday { get; set; } + /// + /// Birthday. + /// + [StringLength(10)] + [Required(ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_BirthdayEmpty")] + [RegularExpression("[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}", ErrorMessageResourceType = typeof(Texts), ErrorMessageResourceName = "Front_Register_Validation_BirthdayInvalid")] + public string Birthday { get; set; } - /// - /// Password to use when registering without external auth. - /// - public string Password { get; set; } + /// + /// Password to use when registering without external auth. + /// + public string Password { get; set; } - /// - /// Copy of the password. - /// - public string PasswordCopy { get; set; } + /// + /// Copy of the password. + /// + public string PasswordCopy { get; set; } - /// - /// Flag indicating that the user must be granted a page. - /// - public bool CreatePersonalPage { get; set; } + /// + /// Flag indicating that the user must be granted a page. + /// + public bool CreatePersonalPage { get; set; } - /// - /// Configures Automapper maps. - /// - public virtual void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Birthday, x => x.Birthday) - .Map(x => x.FirstName, x => x.FirstName) - .Map(x => x.MiddleName, x => x.MiddleName) - .Map(x => x.LastName, x => x.LastName) - .Map(x => x.Email, x => x.Email) - .Map(x => x.UserName, x => Regex.Replace(x.Email, "[^a-z0-9]", "")) - .IgnoreNonMapped(true); - } + /// + /// Configures Automapper maps. + /// + public virtual void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Birthday, x => x.Birthday) + .Map(x => x.FirstName, x => x.FirstName) + .Map(x => x.MiddleName, x => x.MiddleName) + .Map(x => x.LastName, x => x.LastName) + .Map(x => x.Email, x => x.Email) + .Map(x => x.UserName, x => Regex.Replace(x.Email, "[^a-z0-9]", "")) + .IgnoreNonMapped(true); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/RegistrationInfo.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/RegistrationInfo.cs index 1e53525f..ff8bbf44 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/RegistrationInfo.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/RegistrationInfo.cs @@ -1,20 +1,19 @@ using Bonsai.Areas.Front.Logic.Auth; -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Information about the user authorized via external provider. +/// +public class RegistrationInfo { /// - /// Information about the user authorized via external provider. + /// Default values for the registration form. /// - public class RegistrationInfo - { - /// - /// Default values for the registration form. - /// - public RegisterUserVM FormData { get; set; } + public RegisterUserVM FormData { get; set; } - /// - /// External login credentials. - /// - public ExternalLoginData Login { get; set; } - } -} + /// + /// External login credentials. + /// + public ExternalLoginData Login { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Auth/UserVM.cs b/src/Bonsai/Areas/Front/ViewModels/Auth/UserVM.cs index eea1d38a..f977199c 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Auth/UserVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Auth/UserVM.cs @@ -1,33 +1,32 @@ -namespace Bonsai.Areas.Front.ViewModels.Auth +namespace Bonsai.Areas.Front.ViewModels.Auth; + +/// +/// Information about the currently authorized user. +/// +public class UserVM { /// - /// Information about the currently authorized user. + /// Readable name. /// - public class UserVM - { - /// - /// Readable name. - /// - public string Name { get; set; } + public string Name { get; set; } - /// - /// User's registered email. - /// - public string Email { get; set; } + /// + /// User's registered email. + /// + public string Email { get; set; } - /// - /// Flag indicating that this user has access to admin panel. - /// - public bool IsAdministrator { get; set; } + /// + /// Flag indicating that this user has access to admin panel. + /// + public bool IsAdministrator { get; set; } - /// - /// Flag indicating that the current user has been validated by administrators. - /// - public bool IsValidated { get; set; } + /// + /// Flag indicating that the current user has been validated by administrators. + /// + public bool IsValidated { get; set; } - /// - /// Reference to the user's own page. - /// - public string PageKey { get; set; } - } -} + /// + /// Reference to the user's own page. + /// + public string PageKey { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarDayVM.cs b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarDayVM.cs index 95558782..a456fe57 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarDayVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarDayVM.cs @@ -1,29 +1,28 @@ using System.Collections.Generic; using Bonsai.Code.Utils.Date; -namespace Bonsai.Areas.Front.ViewModels.Calendar +namespace Bonsai.Areas.Front.ViewModels.Calendar; + +public class CalendarDayVM { - public class CalendarDayVM - { - /// - /// Day number (1-based). - /// - public int? Day { get; set; } + /// + /// Day number (1-based). + /// + public int? Day { get; set; } - /// - /// Date of the day. - /// - public FuzzyDate Date { get; set; } + /// + /// Date of the day. + /// + public FuzzyDate Date { get; set; } - /// - /// Flag indicating that the day belongs to currently displayed month. - /// Otherwise, the day is a placeholder. - /// - public bool IsActive { get; set; } + /// + /// Flag indicating that the day belongs to currently displayed month. + /// Otherwise, the day is a placeholder. + /// + public bool IsActive { get; set; } - /// - /// List of events in the current day. - /// - public IReadOnlyList Events { get; set; } - } -} + /// + /// List of events in the current day. + /// + public IReadOnlyList Events { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventType.cs b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventType.cs index 7efe531e..32cd7a51 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventType.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventType.cs @@ -1,15 +1,14 @@ -namespace Bonsai.Areas.Front.ViewModels.Calendar +namespace Bonsai.Areas.Front.ViewModels.Calendar; + +/// +/// Type of calendar events. +/// +public enum CalendarEventType { - /// - /// Type of calendar events. - /// - public enum CalendarEventType - { - Birth, - Death, - Wedding, - PetAdoption, - ChildAdoption, - Event - } -} + Birth, + Death, + Wedding, + PetAdoption, + ChildAdoption, + Event +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventVM.cs b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventVM.cs index 3fd5c7ac..557c6692 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarEventVM.cs @@ -1,36 +1,35 @@ using System.Collections.Generic; using Bonsai.Areas.Front.ViewModels.Page; -namespace Bonsai.Areas.Front.ViewModels.Calendar +namespace Bonsai.Areas.Front.ViewModels.Calendar; + +/// +/// Information about a calendar event. +/// +public class CalendarEventVM { /// - /// Information about a calendar event. + /// Number of the day. /// - public class CalendarEventVM - { - /// - /// Number of the day. - /// - public int? Day { get; set; } + public int? Day { get; set; } - /// - /// Title of the event. - /// - public string Title { get; set; } + /// + /// Title of the event. + /// + public string Title { get; set; } - /// - /// Event type. - /// - public CalendarEventType Type { get; set; } + /// + /// Event type. + /// + public CalendarEventType Type { get; set; } - /// - /// Link to the main page of the relation. - /// - public PageTitleExtendedVM RelatedPage { get; set; } + /// + /// Link to the main page of the relation. + /// + public PageTitleExtendedVM RelatedPage { get; set; } - /// - /// Links to other pages (if available). - /// - public IReadOnlyList OtherPages { get; set; } - } -} + /// + /// Links to other pages (if available). + /// + public IReadOnlyList OtherPages { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarMonthVM.cs b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarMonthVM.cs index 231b5301..0fb629bc 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarMonthVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Calendar/CalendarMonthVM.cs @@ -1,40 +1,39 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Calendar +namespace Bonsai.Areas.Front.ViewModels.Calendar; + +/// +/// The month of events. +/// +public class CalendarMonthVM { /// - /// The month of events. + /// Current year. /// - public class CalendarMonthVM - { - /// - /// Current year. - /// - public int Year { get; set; } + public int Year { get; set; } - /// - /// Current month's number (1-based). - /// - public int Month { get; set; } + /// + /// Current month's number (1-based). + /// + public int Month { get; set; } - /// - /// Title (month + year). - /// - public string Title { get; set; } + /// + /// Title (month + year). + /// + public string Title { get; set; } - /// - /// Current day's number (1-based). - /// - public int Day { get; set; } + /// + /// Current day's number (1-based). + /// + public int Day { get; set; } - /// - /// List of calendar days / weeks. - /// - public IReadOnlyList> Weeks { get; set; } + /// + /// List of calendar days / weeks. + /// + public IReadOnlyList> Weeks { get; set; } - /// - /// Events without a certain date. - /// - public IReadOnlyList FuzzyEvents { get; set; } - } -} + /// + /// Events without a certain date. + /// + public IReadOnlyList FuzzyEvents { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Home/HomeVM.cs b/src/Bonsai/Areas/Front/ViewModels/Home/HomeVM.cs index c89d10e6..87770bd6 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Home/HomeVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Home/HomeVM.cs @@ -2,21 +2,20 @@ using Bonsai.Areas.Front.ViewModels.Media; using Bonsai.Areas.Front.ViewModels.Page; -namespace Bonsai.Areas.Front.ViewModels.Home +namespace Bonsai.Areas.Front.ViewModels.Home; + +/// +/// VM for the home page. +/// +public class HomeVM { /// - /// VM for the home page. + /// List of the pages with latest updates. /// - public class HomeVM - { - /// - /// List of the pages with latest updates. - /// - public IReadOnlyList LastUpdatedPages { get; set; } + public IReadOnlyList LastUpdatedPages { get; set; } - /// - /// List of the latest uploaded media. - /// - public IReadOnlyList LastUploadedMedia { get; set; } - } -} + /// + /// List of the latest uploaded media. + /// + public IReadOnlyList LastUploadedMedia { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Media/MediaTagVM.cs b/src/Bonsai/Areas/Front/ViewModels/Media/MediaTagVM.cs index e90868ab..1d04125a 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Media/MediaTagVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Media/MediaTagVM.cs @@ -2,26 +2,25 @@ using System.Drawing; using Bonsai.Areas.Front.ViewModels.Page; -namespace Bonsai.Areas.Front.ViewModels.Media +namespace Bonsai.Areas.Front.ViewModels.Media; + +/// +/// A single tag on the photo. +/// +public class MediaTagVM { /// - /// A single tag on the photo. + /// Unique ID of the tag (for cross-highlighting). /// - public class MediaTagVM - { - /// - /// Unique ID of the tag (for cross-highlighting). - /// - public Guid TagId { get; set; } + public Guid TagId { get; set; } - /// - /// Tagged entity. - /// - public PageTitleVM Page { get; set; } + /// + /// Tagged entity. + /// + public PageTitleVM Page { get; set; } - /// - /// Tag section in photo coordinates (or null for other media types). - /// - public RectangleF? Rect { get; set; } - } -} + /// + /// Tag section in photo coordinates (or null for other media types). + /// + public RectangleF? Rect { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Media/MediaThumbnailVM.cs b/src/Bonsai/Areas/Front/ViewModels/Media/MediaThumbnailVM.cs index c74b5f1b..bb278ca6 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Media/MediaThumbnailVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Media/MediaThumbnailVM.cs @@ -1,36 +1,35 @@ using Bonsai.Code.Utils.Date; using Bonsai.Data.Models; -namespace Bonsai.Areas.Front.ViewModels.Media +namespace Bonsai.Areas.Front.ViewModels.Media; + +/// +/// Details about a media item's thumbnail. +/// +public class MediaThumbnailVM { /// - /// Details about a media item's thumbnail. + /// Full URL of the media's display page. /// - public class MediaThumbnailVM - { - /// - /// Full URL of the media's display page. - /// - public string Key { get; set; } + public string Key { get; set; } - /// - /// URL of the media's thumbnail. - /// - public string ThumbnailUrl { get; set; } + /// + /// URL of the media's thumbnail. + /// + public string ThumbnailUrl { get; set; } - /// - /// Type of the media. - /// - public MediaType Type { get; set; } + /// + /// Type of the media. + /// + public MediaType Type { get; set; } - /// - /// Date of the media's origin. - /// - public FuzzyDate? Date { get; set; } + /// + /// Date of the media's origin. + /// + public FuzzyDate? Date { get; set; } - /// - /// Flag indicating that the media file is ready for viewing. - /// - public bool IsProcessed { get; set; } - } -} + /// + /// Flag indicating that the media file is ready for viewing. + /// + public bool IsProcessed { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Media/MediaVM.cs b/src/Bonsai/Areas/Front/ViewModels/Media/MediaVM.cs index 28145213..ff0ef2c5 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Media/MediaVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Media/MediaVM.cs @@ -4,66 +4,65 @@ using Bonsai.Code.Utils.Date; using Bonsai.Data.Models; -namespace Bonsai.Areas.Front.ViewModels.Media +namespace Bonsai.Areas.Front.ViewModels.Media; + +/// +/// Details of a single media file. +/// +public class MediaVM { /// - /// Details of a single media file. + /// Surrogate ID. /// - public class MediaVM - { - /// - /// Surrogate ID. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// URL of the media file to display inline. - /// - public string PreviewPath { get; set; } + /// + /// URL of the media file to display inline. + /// + public string PreviewPath { get; set; } - /// - /// URL of the full-sized media (for photos). - /// - public string OriginalPath { get; set; } + /// + /// URL of the full-sized media (for photos). + /// + public string OriginalPath { get; set; } - /// - /// Title of the document (for documents). - /// - public string Title { get; set; } + /// + /// Title of the document (for documents). + /// + public string Title { get; set; } - /// - /// Type of the media (photo, video, etc.). - /// - public MediaType Type { get; set; } + /// + /// Type of the media (photo, video, etc.). + /// + public MediaType Type { get; set; } - /// - /// Flag indicating that the media has been successfully encoded. - /// - public bool IsProcessed { get; set; } + /// + /// Flag indicating that the media has been successfully encoded. + /// + public bool IsProcessed { get; set; } - /// - /// Description of the media (HTML). - /// - public string Description { get; set; } + /// + /// Description of the media (HTML). + /// + public string Description { get; set; } - /// - /// Tagged entities on the photo. - /// - public IReadOnlyList Tags { get; set; } + /// + /// Tagged entities on the photo. + /// + public IReadOnlyList Tags { get; set; } - /// - /// Date of the media file's creation. - /// - public FuzzyDate? Date { get; set; } + /// + /// Date of the media file's creation. + /// + public FuzzyDate? Date { get; set; } - /// - /// Related location. - /// - public PageTitleVM Location { get; set; } + /// + /// Related location. + /// + public PageTitleVM Location { get; set; } - /// - /// Related event. - /// - public PageTitleVM Event { get; set; } - } -} + /// + /// Related event. + /// + public PageTitleVM Event { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/FactGroupVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/FactGroupVM.cs index 577c3515..ec2a2dfe 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/FactGroupVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/FactGroupVM.cs @@ -2,21 +2,20 @@ using Bonsai.Code.DomainModel.Facts; using Bonsai.Code.DomainModel.Facts.Models; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// Group of related facts. +/// +public class FactGroupVM { /// - /// Group of related facts. + /// Definition of the group. /// - public class FactGroupVM - { - /// - /// Definition of the group. - /// - public FactDefinitionGroup Definition { get; set; } + public FactDefinitionGroup Definition { get; set; } - /// - /// The set of fact data. - /// - public ICollection Facts { get; set; } - } -} + /// + /// The set of fact data. + /// + public ICollection Facts { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/InfoBlockVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/InfoBlockVM.cs index 10d05456..98075c03 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/InfoBlockVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/InfoBlockVM.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; using Bonsai.Areas.Front.ViewModels.Media; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// The list of facts related to a particular page. +/// +public class InfoBlockVM { /// - /// The list of facts related to a particular page. + /// The photo (if specified). /// - public class InfoBlockVM - { - /// - /// The photo (if specified). - /// - public MediaThumbnailVM Photo { get; set; } + public MediaThumbnailVM Photo { get; set; } - /// - /// Facts about the person. - /// - public IReadOnlyCollection Facts { get; set; } + /// + /// Facts about the person. + /// + public IReadOnlyCollection Facts { get; set; } - /// - /// Groups of inferred relations. - /// - public IReadOnlyCollection RelationGroups { get; set; } - } -} + /// + /// Groups of inferred relations. + /// + public IReadOnlyCollection RelationGroups { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelatedPageVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelatedPageVM.cs index 024fc168..820cf624 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelatedPageVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelatedPageVM.cs @@ -1,20 +1,19 @@ using Bonsai.Code.Utils.Date; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// A single related page. +/// +public class RelatedPageVM: PageTitleVM { /// - /// A single related page. + /// Range of the relation (if applicable). /// - public class RelatedPageVM: PageTitleVM - { - /// - /// Range of the relation (if applicable). - /// - public FuzzyRange? Duration { get; set; } + public FuzzyRange? Duration { get; set; } - /// - /// Page of the event bound to the relation. - /// - public PageTitleVM RelationEvent { get; set; } - } -} + /// + /// Page of the event bound to the relation. + /// + public PageTitleVM RelationEvent { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationCategoryVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationCategoryVM.cs index 3761cc42..fe647aa2 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationCategoryVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationCategoryVM.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// Information about a list of relation groups. +/// +public class RelationCategoryVM { /// - /// Information about a list of relation groups. + /// Title of the relations category. /// - public class RelationCategoryVM - { - /// - /// Title of the relations category. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// Flag indicating that the category must be displayed at the top of the page. - /// - public bool IsMain { get; set; } + /// + /// Flag indicating that the category must be displayed at the top of the page. + /// + public bool IsMain { get; set; } - /// - /// List of nested groups. - /// - public IReadOnlyList Groups { get; set; } - } -} + /// + /// List of nested groups. + /// + public IReadOnlyList Groups { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationGroupVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationGroupVM.cs index c8bc6280..a4b9ac7e 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationGroupVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationGroupVM.cs @@ -1,15 +1,14 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// Information about a group of relations. +/// +public class RelationGroupVM { /// - /// Information about a group of relations. + /// Relations in the group. /// - public class RelationGroupVM - { - /// - /// Relations in the group. - /// - public ICollection Relations { get; set; } - } -} + public ICollection Relations { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationVM.cs index b88a7af7..76478adf 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/InfoBlock/RelationVM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock +namespace Bonsai.Areas.Front.ViewModels.Page.InfoBlock; + +/// +/// Information about a particular relation. +/// +public class RelationVM { /// - /// Information about a particular relation. + /// Readable title of the relation. /// - public class RelationVM - { - /// - /// Readable title of the relation. - /// - public string Title { get; set; } + public string Title { get; set; } - /// - /// List of related pages. - /// - public ICollection Pages { get; set; } - } -} + /// + /// List of related pages. + /// + public ICollection Pages { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageDescriptionVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageDescriptionVM.cs index aaeb251f..9eb301eb 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageDescriptionVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageDescriptionVM.cs @@ -1,13 +1,12 @@ -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// The description section of a page. +/// +public class PageDescriptionVM: PageTitleVM { /// - /// The description section of a page. + /// Main description (in HTML format). /// - public class PageDescriptionVM: PageTitleVM - { - /// - /// Main description (in HTML format). - /// - public string Description { get; set; } - } -} + public string Description { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageMediaVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageMediaVM.cs index fe8a0ede..f0a5d6e7 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageMediaVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageMediaVM.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; using Bonsai.Areas.Front.ViewModels.Media; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// List of the page's media files. +/// +public class PageMediaVM: PageTitleVM { /// - /// List of the page's media files. + /// The list of media item thumbnails for current page. /// - public class PageMediaVM: PageTitleVM - { - /// - /// The list of media item thumbnails for current page. - /// - public IReadOnlyList Media { get; set; } - } -} + public IReadOnlyList Media { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageReferencesVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageReferencesVM.cs index 8992484a..dca72c1b 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageReferencesVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageReferencesVM.cs @@ -1,15 +1,14 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// Page with references to current page. +/// +public class PageReferencesVM : PageTitleVM { /// - /// Page with references to current page. + /// List of other pages referencing the current one. /// - public class PageReferencesVM : PageTitleVM - { - /// - /// List of other pages referencing the current one. - /// - public IReadOnlyList References { get; set; } - } + public IReadOnlyList References { get; set; } } \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleExtendedVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleExtendedVM.cs index 1ac15c56..bd3f34f7 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleExtendedVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleExtendedVM.cs @@ -2,43 +2,42 @@ using Bonsai.Code.Infrastructure; using Mapster; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// Information about the page with a tiny preview image, if any. +/// +public class PageTitleExtendedVM: PageTitleVM, IMapped { /// - /// Information about the page with a tiny preview image, if any. + /// Unique ID. /// - public class PageTitleExtendedVM: PageTitleVM, IMapped - { - /// - /// Unique ID. - /// - public new Guid Id { get; set; } + public new Guid Id { get; set; } - /// - /// Page's main image. - /// - public string MainPhotoPath { get; set; } + /// + /// Page's main image. + /// + public string MainPhotoPath { get; set; } - /// - /// Date of the page's last update. - /// - public DateTimeOffset LastUpdateDate { get; set; } + /// + /// Date of the page's last update. + /// + public DateTimeOffset LastUpdateDate { get; set; } - /// - /// Date of the page's creation. - /// - public DateTimeOffset CreationDate { get; set; } + /// + /// Date of the page's creation. + /// + public DateTimeOffset CreationDate { get; set; } - public override void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Key, x => x.Key) - .Map(x => x.Type, x => x.Type) - .Map(x => x.MainPhotoPath, x => x.MainPhoto.FilePath) - .Map(x => x.LastUpdateDate, x => x.LastUpdateDate) - .Map(x => x.CreationDate, x => x.CreationDate); - } + public override void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Key, x => x.Key) + .Map(x => x.Type, x => x.Type) + .Map(x => x.MainPhotoPath, x => x.MainPhoto.FilePath) + .Map(x => x.LastUpdateDate, x => x.LastUpdateDate) + .Map(x => x.CreationDate, x => x.CreationDate); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleVM.cs index eba1a0e5..b068365b 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageTitleVM.cs @@ -3,40 +3,39 @@ using Bonsai.Data.Models; using Mapster; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// Base view model for all page sections. +/// +public class PageTitleVM: IMapped { /// - /// Base view model for all page sections. + /// Surrogate ID of the page. /// - public class PageTitleVM: IMapped - { - /// - /// Surrogate ID of the page. - /// - public Guid? Id { get; set; } + public Guid? Id { get; set; } - /// - /// Title of the page (displayed in the header). - /// - public string Title { get; set; } + /// + /// Title of the page (displayed in the header). + /// + public string Title { get; set; } - /// - /// Key of the page (url-friendly version of the title). - /// - public string Key { get; set; } + /// + /// Key of the page (url-friendly version of the title). + /// + public string Key { get; set; } - /// - /// Type of the entity described by this page. - /// - public PageType Type { get; set; } + /// + /// Type of the entity described by this page. + /// + public PageType Type { get; set; } - public virtual void Configure(TypeAdapterConfig config) - { - config.NewConfig() - .Map(x => x.Id, x => x.Id) - .Map(x => x.Title, x => x.Title) - .Map(x => x.Key, x => x.Key) - .Map(x => x.Type, x => x.Type); - } + public virtual void Configure(TypeAdapterConfig config) + { + config.NewConfig() + .Map(x => x.Id, x => x.Id) + .Map(x => x.Title, x => x.Title) + .Map(x => x.Key, x => x.Key) + .Map(x => x.Type, x => x.Type); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageTreeVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageTreeVM.cs index 81cd051b..674dcb9d 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageTreeVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageTreeVM.cs @@ -1,21 +1,20 @@ using System.Collections.Generic; using Bonsai.Data.Models; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// Details of the page tree. +/// +public class PageTreeVM: PageTitleVM { /// - /// Details of the page tree. + /// List of tree kinds which are enabled in the settings. /// - public class PageTreeVM: PageTitleVM - { - /// - /// List of tree kinds which are enabled in the settings. - /// - public IReadOnlyList SupportedKinds { get; set; } + public IReadOnlyList SupportedKinds { get; set; } - /// - /// Kind of the tree being displayed. - /// - public TreeKind TreeKind { get; set; } - } -} + /// + /// Kind of the tree being displayed. + /// + public TreeKind TreeKind { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Page/PageVM.cs b/src/Bonsai/Areas/Front/ViewModels/Page/PageVM.cs index bb814110..775b6198 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Page/PageVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Page/PageVM.cs @@ -1,20 +1,19 @@ using Bonsai.Areas.Front.ViewModels.Page.InfoBlock; -namespace Bonsai.Areas.Front.ViewModels.Page +namespace Bonsai.Areas.Front.ViewModels.Page; + +/// +/// Details of the page. +/// +public class PageVM where T: PageTitleVM { /// - /// Details of the page. + /// Main page contents. /// - public class PageVM where T: PageTitleVM - { - /// - /// Main page contents. - /// - public T Body { get; set; } + public T Body { get; set; } - /// - /// Additional info in the sidebar. - /// - public InfoBlockVM InfoBlock { get; set; } - } -} + /// + /// Additional info in the sidebar. + /// + public InfoBlockVM InfoBlock { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Search/SearchResultVM.cs b/src/Bonsai/Areas/Front/ViewModels/Search/SearchResultVM.cs index ecd6dffe..1a6a386f 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Search/SearchResultVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Search/SearchResultVM.cs @@ -1,20 +1,19 @@ using Bonsai.Areas.Front.ViewModels.Page; -namespace Bonsai.Areas.Front.ViewModels.Search +namespace Bonsai.Areas.Front.ViewModels.Search; + +/// +/// Information about a page found in the search. +/// +public class SearchResultVM: PageTitleExtendedVM { /// - /// Information about a page found in the search. + /// Page title with matched elements highlighted. /// - public class SearchResultVM: PageTitleExtendedVM - { - /// - /// Page title with matched elements highlighted. - /// - public string HighlightedTitle { get; set; } + public string HighlightedTitle { get; set; } - /// - /// A portion of the page's description that matches the query. - /// - public string DescriptionExcerpt { get; set; } - } -} + /// + /// A portion of the page's description that matches the query. + /// + public string DescriptionExcerpt { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Search/SearchVM.cs b/src/Bonsai/Areas/Front/ViewModels/Search/SearchVM.cs index 8eb0f62d..b73a4398 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Search/SearchVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Search/SearchVM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; -namespace Bonsai.Areas.Front.ViewModels.Search +namespace Bonsai.Areas.Front.ViewModels.Search; + +/// +/// The results of a search. +/// +public class SearchVM { /// - /// The results of a search. + /// Search query. /// - public class SearchVM - { - /// - /// Search query. - /// - public string Query { get; set; } + public string Query { get; set; } - /// - /// Found pages. - /// - public IReadOnlyList Results { get; set; } - } -} + /// + /// Found pages. + /// + public IReadOnlyList Results { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/ViewModels/Tree/TreeVM.cs b/src/Bonsai/Areas/Front/ViewModels/Tree/TreeVM.cs index b4a0e28e..4611cd36 100644 --- a/src/Bonsai/Areas/Front/ViewModels/Tree/TreeVM.cs +++ b/src/Bonsai/Areas/Front/ViewModels/Tree/TreeVM.cs @@ -1,21 +1,20 @@ using System; using Newtonsoft.Json.Linq; -namespace Bonsai.Areas.Front.ViewModels.Tree +namespace Bonsai.Areas.Front.ViewModels.Tree; + +/// +/// Information about a rendered tree. +/// +public class TreeVM { /// - /// Information about a rendered tree. + /// ID of the root element. /// - public class TreeVM - { - /// - /// ID of the root element. - /// - public Guid RootId { get; set; } + public Guid RootId { get; set; } - /// - /// Complete layout. - /// - public JObject Content { get; set; } - } -} + /// + /// Complete layout. + /// + public JObject Content { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Areas/Front/Views/Auth/RegisterDisabled.cshtml.cs b/src/Bonsai/Areas/Front/Views/Auth/RegisterDisabled.cshtml.cs index dc5fbc71..320bcea2 100644 --- a/src/Bonsai/Areas/Front/Views/Auth/RegisterDisabled.cshtml.cs +++ b/src/Bonsai/Areas/Front/Views/Auth/RegisterDisabled.cshtml.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Bonsai.Areas.Front.Views.Auth +namespace Bonsai.Areas.Front.Views.Auth; + +public class RegisterDisabledModel : PageModel { - public class RegisterDisabledModel : PageModel + public void OnGet() { - public void OnGet() - { - } } } \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Auth.cs b/src/Bonsai/Code/Config/Startup.Auth.cs index fce042f5..a2e1bbcb 100644 --- a/src/Bonsai/Code/Config/Startup.Auth.cs +++ b/src/Bonsai/Code/Config/Startup.Auth.cs @@ -4,35 +4,34 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Configures the auth-related sessions. + /// + private void ConfigureAuthServices(IServiceCollection services) { - /// - /// Configures the auth-related sessions. - /// - private void ConfigureAuthServices(IServiceCollection services) + services.AddAuthorization(opts => { - services.AddAuthorization(opts => - { - opts.AddPolicy(AuthRequirement.Name, p => p.Requirements.Add(new AuthRequirement())); - opts.AddPolicy(AdminAuthRequirement.Name, p => p.Requirements.Add(new AdminAuthRequirement())); - }); + opts.AddPolicy(AuthRequirement.Name, p => p.Requirements.Add(new AuthRequirement())); + opts.AddPolicy(AdminAuthRequirement.Name, p => p.Requirements.Add(new AdminAuthRequirement())); + }); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - var auth = services.AddAuthentication(IdentityConstants.ApplicationScheme); - var authProvider = new AuthProviderService(); - authProvider.Initialize(Configuration, auth); - services.AddSingleton(authProvider); + var auth = services.AddAuthentication(IdentityConstants.ApplicationScheme); + var authProvider = new AuthProviderService(); + authProvider.Initialize(Configuration, auth); + services.AddSingleton(authProvider); - services.ConfigureApplicationCookie(opts => - { - opts.LoginPath = "/auth/login"; - opts.AccessDeniedPath = "/auth/login"; - opts.ReturnUrlParameter = "returnUrl"; - }); - } + services.ConfigureApplicationCookie(opts => + { + opts.LoginPath = "/auth/login"; + opts.AccessDeniedPath = "/auth/login"; + opts.ReturnUrlParameter = "returnUrl"; + }); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Database.cs b/src/Bonsai/Code/Config/Startup.Database.cs index dedc7755..7703fa6a 100644 --- a/src/Bonsai/Code/Config/Startup.Database.cs +++ b/src/Bonsai/Code/Config/Startup.Database.cs @@ -20,202 +20,201 @@ using Newtonsoft.Json; using Serilog; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Configures and registers database-related services. + /// + private void ConfigureDatabaseServices(IServiceCollection services) { - /// - /// Configures and registers database-related services. - /// - private void ConfigureDatabaseServices(IServiceCollection services) + services.AddDbContext(opts => { - services.AddDbContext(opts => - { - var cfg = Configuration.ConnectionStrings; - if (cfg.UseEmbeddedDatabase) - opts.UseSqlite(cfg.EmbeddedDatabase); - else - opts.UseNpgsql(cfg.Database); - }); - - services.AddIdentity(o => - { - o.Password.RequireDigit = false; - o.Password.RequireLowercase = false; - o.Password.RequireUppercase = false; - o.Password.RequireNonAlphanumeric = false; - o.Password.RequiredLength = 6; - o.Password.RequiredUniqueChars = 1; - }) - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - } + var cfg = Configuration.ConnectionStrings; + if (cfg.UseEmbeddedDatabase) + opts.UseSqlite(cfg.EmbeddedDatabase); + else + opts.UseNpgsql(cfg.Database); + }); - /// - /// Applies database migrations and seeds data. - /// - private void InitDatabase(IApplicationBuilder app) - { - var startupService = app.ApplicationServices.GetService(); + services.AddIdentity(o => + { + o.Password.RequireDigit = false; + o.Password.RequireLowercase = false; + o.Password.RequireUppercase = false; + o.Password.RequireNonAlphanumeric = false; + o.Password.RequiredLength = 6; + o.Password.RequiredUniqueChars = 1; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } + + /// + /// Applies database migrations and seeds data. + /// + private void InitDatabase(IApplicationBuilder app) + { + var startupService = app.ApplicationServices.GetService(); - var scope = app.ApplicationServices.GetRequiredService().CreateScope(); - var sp = scope.ServiceProvider; - var demoCfg = Configuration.DemoMode ?? new DemoModeConfig(); // all false + var scope = app.ApplicationServices.GetRequiredService().CreateScope(); + var sp = scope.ServiceProvider; + var demoCfg = Configuration.DemoMode ?? new DemoModeConfig(); // all false - if (DatabaseReplicator.IsReplicationPending(Configuration.ConnectionStrings)) - { - startupService.AddTask( - "DatabaseReplicate", - Texts.Startup_Task_DatabaseReplication, - () => DatabaseReplicator.ReplicateAsync(Configuration.ConnectionStrings) - ); - } - else + if (DatabaseReplicator.IsReplicationPending(Configuration.ConnectionStrings)) + { + startupService.AddTask( + "DatabaseReplicate", + Texts.Startup_Task_DatabaseReplication, + () => DatabaseReplicator.ReplicateAsync(Configuration.ConnectionStrings) + ); + } + else + { + startupService.AddTask( + "DatabaseMigrate", + Texts.Startup_Task_DatabaseMigration, + async () => + { + var db = sp.GetService(); + await AppDbContextHelper.UpdateDatabaseAsync(db); + } + ); + } + + if (demoCfg.Enabled) + { + if (demoCfg.ClearOnStartup) { startupService.AddTask( - "DatabaseMigrate", - Texts.Startup_Task_DatabaseMigration, - async () => - { - var db = sp.GetService(); - await AppDbContextHelper.UpdateDatabaseAsync(db); - } + "DatabaseClear", + Texts.Startup_Task_DatabaseCleanup, + () => SeedData.ClearPreviousDataAsync(sp.GetService()) ); } - if (demoCfg.Enabled) + if (demoCfg.CreateDefaultPages || demoCfg.CreateDefaultAdmin) { - if (demoCfg.ClearOnStartup) - { - startupService.AddTask( - "DatabaseClear", - Texts.Startup_Task_DatabaseCleanup, - () => SeedData.ClearPreviousDataAsync(sp.GetService()) - ); - } - - if (demoCfg.CreateDefaultPages || demoCfg.CreateDefaultAdmin) - { - startupService.AddTask( - "DatabaseSeed", - Texts.Startup_Task_TestDataSeeding, - async () => - { - if (demoCfg.CreateDefaultPages) - await SeedData.EnsureSampleDataSeededAsync(sp.GetService()); - - if (demoCfg.CreateDefaultAdmin) - await SeedData.EnsureDefaultUserCreatedAsync(sp.GetService>()); - }); - } - startupService.AddTask( - "PrepareConfig", - Texts.Startup_Task_Configuration, + "DatabaseSeed", + Texts.Startup_Task_TestDataSeeding, async () => { - var db = sp.GetRequiredService(); - var wrapper = await db.DynamicConfig.FirstAsync(); - var cfg = JsonConvert.DeserializeObject(wrapper.Value); - cfg.TreeKinds = TreeKind.FullTree | TreeKind.CloseFamily | TreeKind.Ancestors | TreeKind.Descendants; - wrapper.Value = JsonConvert.SerializeObject(cfg); - await db.SaveChangesAsync(); - } - ); + if (demoCfg.CreateDefaultPages) + await SeedData.EnsureSampleDataSeededAsync(sp.GetService()); + + if (demoCfg.CreateDefaultAdmin) + await SeedData.EnsureDefaultUserCreatedAsync(sp.GetService>()); + }); } startupService.AddTask( - "FullTextIndexInit", - Texts.Startup_Task_SearchIndexPreparation, + "PrepareConfig", + Texts.Startup_Task_Configuration, async () => { - var search = sp.GetService(); - var db = sp.GetService(); - - await search.InitializeAsync(); - - var pages = await db.Pages.Include(x => x.Aliases).ToListAsync(); - foreach (var page in pages) - await search.AddPageAsync(page); + var db = sp.GetRequiredService(); + var wrapper = await db.DynamicConfig.FirstAsync(); + var cfg = JsonConvert.DeserializeObject(wrapper.Value); + cfg.TreeKinds = TreeKind.FullTree | TreeKind.CloseFamily | TreeKind.Ancestors | TreeKind.Descendants; + wrapper.Value = JsonConvert.SerializeObject(cfg); + await db.SaveChangesAsync(); } ); - - startupService.AddTask( - "BuildPageReferences", - Texts.Startup_Task_ReferenceDetection, - () => BuildPageReferences(sp) - ); + } - startupService.AddTask( - "InitTree", - Texts.Startup_Task_TreeBuilding, - async () => - { - var db = sp.GetService(); - if (await db.TreeLayouts.AnyAsync()) - return; + startupService.AddTask( + "FullTextIndexInit", + Texts.Startup_Task_SearchIndexPreparation, + async () => + { + var search = sp.GetService(); + var db = sp.GetService(); - var jobs = sp.GetService(); - await jobs.RunAsync(JobBuilder.For().SupersedeAll()); - } - ); + await search.InitializeAsync(); - startupService.AddTask("CheckMissingMedia", "", () => CheckMissingMediaAsync(sp)); + var pages = await db.Pages.Include(x => x.Aliases).ToListAsync(); + foreach (var page in pages) + await search.AddPageAsync(page); + } + ); - startupService.AddTask("Finalize", "", async () => scope.Dispose()); + startupService.AddTask( + "BuildPageReferences", + Texts.Startup_Task_ReferenceDetection, + () => BuildPageReferences(sp) + ); + + startupService.AddTask( + "InitTree", + Texts.Startup_Task_TreeBuilding, + async () => + { + var db = sp.GetService(); + if (await db.TreeLayouts.AnyAsync()) + return; - startupService.RunStartupTasks(); - } + var jobs = sp.GetService(); + await jobs.RunAsync(JobBuilder.For().SupersedeAll()); + } + ); - /// - /// Checks if the media folder is not mounted correctly. - /// - private async Task CheckMissingMediaAsync(IServiceProvider sp) - { - var db = sp.GetService(); - var env = sp.GetService(); + startupService.AddTask("CheckMissingMedia", "", () => CheckMissingMediaAsync(sp)); - if (await db.Media.AnyAsync() == false) - return; + startupService.AddTask("Finalize", "", async () => scope.Dispose()); - var path = Path.Combine(env.WebRootPath, "media"); - if (!Directory.Exists(path) || !Directory.EnumerateFiles(path).Any()) - { - var logger = sp.GetService(); - logger.Error("The 'media' directory is missing. Make sure it is mounted properly."); - } - } + startupService.RunStartupTasks(); + } + + /// + /// Checks if the media folder is not mounted correctly. + /// + private async Task CheckMissingMediaAsync(IServiceProvider sp) + { + var db = sp.GetService(); + var env = sp.GetService(); + + if (await db.Media.AnyAsync() == false) + return; - /// - /// Parses all pages, finding cross links in descriptions. - /// - private async Task BuildPageReferences(IServiceProvider sp) + var path = Path.Combine(env.WebRootPath, "media"); + if (!Directory.Exists(path) || !Directory.EnumerateFiles(path).Any()) { - var db = sp.GetService(); - if (await db.PageReferences.AnyAsync()) - return; + var logger = sp.GetService(); + logger.Error("The 'media' directory is missing. Make sure it is mounted properly."); + } + } - var pages = await db.Pages.Select(x => new {x.Id, x.Key, x.Description}) - .ToDictionaryAsync(x => x.Key, x => x); + /// + /// Parses all pages, finding cross links in descriptions. + /// + private async Task BuildPageReferences(IServiceProvider sp) + { + var db = sp.GetService(); + if (await db.PageReferences.AnyAsync()) + return; - foreach (var p in pages.Values) + var pages = await db.Pages.Select(x => new {x.Id, x.Key, x.Description}) + .ToDictionaryAsync(x => x.Key, x => x); + + foreach (var p in pages.Values) + { + var links = MarkdownService.GetPageReferences(p.Description); + foreach (var link in links) { - var links = MarkdownService.GetPageReferences(p.Description); - foreach (var link in links) - { - if (!pages.TryGetValue(link, out var linkRef)) - continue; + if (!pages.TryGetValue(link, out var linkRef)) + continue; - db.PageReferences.Add(new PageReference - { - Id = Guid.NewGuid(), - SourceId = p.Id, - DestinationId = linkRef.Id - }); - } + db.PageReferences.Add(new PageReference + { + Id = Guid.NewGuid(), + SourceId = p.Id, + DestinationId = linkRef.Id + }); } - - await db.SaveChangesAsync(); } + + await db.SaveChangesAsync(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Mapster.cs b/src/Bonsai/Code/Config/Startup.Mapster.cs index 4c6e45cd..bbd07aba 100644 --- a/src/Bonsai/Code/Config/Startup.Mapster.cs +++ b/src/Bonsai/Code/Config/Startup.Mapster.cs @@ -3,27 +3,26 @@ using MapsterMapper; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Registers Mapster. + /// + private void ConfigureMapster(IServiceCollection services) { - /// - /// Registers Mapster. - /// - private void ConfigureMapster(IServiceCollection services) + var config = new TypeAdapterConfig { - var config = new TypeAdapterConfig - { - RequireExplicitMapping = true, - }; + RequireExplicitMapping = true, + }; - foreach (var map in RuntimeHelper.GetAllInstances()) - map.Configure(config); + foreach (var map in RuntimeHelper.GetAllInstances()) + map.Configure(config); - config.Compile(); - var mapper = new Mapper(config); + config.Compile(); + var mapper = new Mapper(config); - services.AddSingleton(mapper); - } + services.AddSingleton(mapper); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Mvc.cs b/src/Bonsai/Code/Config/Startup.Mvc.cs index 1915c8b3..3cef6f5b 100644 --- a/src/Bonsai/Code/Config/Startup.Mvc.cs +++ b/src/Bonsai/Code/Config/Startup.Mvc.cs @@ -13,67 +13,66 @@ using Newtonsoft.Json; using Serilog; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Configures and registers MVC-related services. + /// + private void ConfigureMvcServices(IServiceCollection services) { - /// - /// Configures and registers MVC-related services. - /// - private void ConfigureMvcServices(IServiceCollection services) - { - services.AddSingleton(p => Configuration); - services.AddSingleton(p => Log.Logger); + services.AddSingleton(p => Configuration); + services.AddSingleton(p => Log.Logger); - services.AddMvc() - .AddControllersAsServices() - .AddSessionStateTempDataProvider() - .AddRazorRuntimeCompilation() - .AddNewtonsoftJson(opts => + services.AddMvc() + .AddControllersAsServices() + .AddSessionStateTempDataProvider() + .AddRazorRuntimeCompilation() + .AddNewtonsoftJson(opts => + { + var convs = new List { - var convs = new List - { - new FuzzyDate.FuzzyDateJsonConverter(), - new FuzzyRange.FuzzyRangeJsonConverter() - }; + new FuzzyDate.FuzzyDateJsonConverter(), + new FuzzyRange.FuzzyRangeJsonConverter() + }; - convs.ForEach(x => opts.SerializerSettings.Converters.Add(x)); + convs.ForEach(x => opts.SerializerSettings.Converters.Add(x)); - JsonConvert.DefaultSettings = () => - { - var settings = new JsonSerializerSettings(); - convs.ForEach(settings.Converters.Add); - return settings; - }; - }); + JsonConvert.DefaultSettings = () => + { + var settings = new JsonSerializerSettings(); + convs.ForEach(settings.Converters.Add); + return settings; + }; + }); - services.AddRouting(opts => - { - opts.AppendTrailingSlash = false; - opts.LowercaseUrls = false; - }); + services.AddRouting(opts => + { + opts.AppendTrailingSlash = false; + opts.LowercaseUrls = false; + }); - services.AddSession(); + services.AddSession(); - services.AddSingleton(); - services.AddScoped(x => - { - var httpAcc = x.GetService(); - var urlFactory = x.GetService(); - var actionCtx = new ActionContext(httpAcc.HttpContext, httpAcc.HttpContext.GetRouteData(), new ActionDescriptor()); - return urlFactory.GetUrlHelper(actionCtx); - }); - services.AddScoped(); + services.AddSingleton(); + services.AddScoped(x => + { + var httpAcc = x.GetService(); + var urlFactory = x.GetService(); + var actionCtx = new ActionContext(httpAcc.HttpContext, httpAcc.HttpContext.GetRouteData(), new ActionDescriptor()); + return urlFactory.GetUrlHelper(actionCtx); + }); + services.AddScoped(); - services.AddScoped(); + services.AddScoped(); - if(Configuration.WebServer.RequireHttps) + if(Configuration.WebServer.RequireHttps) + { + services.Configure(opts => { - services.Configure(opts => - { - opts.Filters.Add(new RequireHttpsAttribute()); - }); - } + opts.Filters.Add(new RequireHttpsAttribute()); + }); } } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Search.cs b/src/Bonsai/Code/Config/Startup.Search.cs index d04ee4f4..2201003f 100644 --- a/src/Bonsai/Code/Config/Startup.Search.cs +++ b/src/Bonsai/Code/Config/Startup.Search.cs @@ -1,16 +1,15 @@ using Bonsai.Code.Services.Search; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Registers fulltext search-related services. + /// + private void ConfigureSearchServices(IServiceCollection services) { - /// - /// Registers fulltext search-related services. - /// - private void ConfigureSearchServices(IServiceCollection services) - { - services.AddSingleton(); - } + services.AddSingleton(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.Services.cs b/src/Bonsai/Code/Config/Startup.Services.cs index 94a476eb..26e0ecb5 100644 --- a/src/Bonsai/Code/Config/Startup.Services.cs +++ b/src/Bonsai/Code/Config/Startup.Services.cs @@ -12,63 +12,62 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +public partial class Startup { - public partial class Startup + /// + /// Register application-level services. + /// + private void ConfigureAppServices(IServiceCollection services) { - /// - /// Register application-level services. - /// - private void ConfigureAppServices(IServiceCollection services) + services.Configure(x => { - services.Configure(x => - { - // actual value set in config via ConfigurableRequestSizeLimitFilter - x.MemoryBufferThreshold = int.MaxValue; - x.ValueLengthLimit = int.MaxValue; - x.MultipartBodyLengthLimit = int.MaxValue; - }); + // actual value set in config via ConfigurableRequestSizeLimitFilter + x.MemoryBufferThreshold = int.MaxValue; + x.ValueLengthLimit = int.MaxValue; + x.MultipartBodyLengthLimit = int.MaxValue; + }); - services.AddNodeJS(); - services.Configure(opts => opts.InvocationTimeoutMS = -1); - services.Configure(opts => opts.Version = HttpVersion.Version20); + services.AddNodeJS(); + services.Configure(opts => opts.InvocationTimeoutMS = -1); + services.Configure(opts => opts.Version = HttpVersion.Version20); - // common - services.AddScoped(); - services.AddSingleton(); - services.AddTransient(); + // common + services.AddScoped(); + services.AddSingleton(); + services.AddTransient(); - services.AddSingleton(); + services.AddSingleton(); - // frontend - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + // frontend + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - // admin - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + // admin + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - } + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Config/Startup.cs b/src/Bonsai/Code/Config/Startup.cs index 3afff8f9..8265db6c 100644 --- a/src/Bonsai/Code/Config/Startup.cs +++ b/src/Bonsai/Code/Config/Startup.cs @@ -10,97 +10,96 @@ using Microsoft.Extensions.Hosting; using Serilog; -namespace Bonsai.Code.Config +namespace Bonsai.Code.Config; + +[UsedImplicitly] +public partial class Startup { - [UsedImplicitly] - public partial class Startup + public Startup(IWebHostEnvironment env) { - public Startup(IWebHostEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); - Configuration = builder.Build().Get(); - Environment = env; + Configuration = builder.Build().Get(); + Environment = env; - ConfigValidator.EnsureValid(Configuration); - LocaleProvider.SetLocale(Configuration.Locale); - } + ConfigValidator.EnsureValid(Configuration); + LocaleProvider.SetLocale(Configuration.Locale); + } - private StaticConfig Configuration { get; } - private IWebHostEnvironment Environment { get; } + private StaticConfig Configuration { get; } + private IWebHostEnvironment Environment { get; } - /// - /// Registers all required services in the dependency injection container. - /// - public void ConfigureServices(IServiceCollection services) - { - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); - AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true); - AppContext.SetSwitch("System.Drawing.EnableUnixSupport", true); + /// + /// Registers all required services in the dependency injection container. + /// + public void ConfigureServices(IServiceCollection services) + { + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true); + AppContext.SetSwitch("System.Drawing.EnableUnixSupport", true); - // order is crucial - ConfigureMvcServices(services); - ConfigureDatabaseServices(services); - ConfigureAuthServices(services); - ConfigureSearchServices(services); - ConfigureMapster(services); - ConfigureAppServices(services); - } + // order is crucial + ConfigureMvcServices(services); + ConfigureDatabaseServices(services); + ConfigureAuthServices(services); + ConfigureSearchServices(services); + ConfigureMapster(services); + ConfigureAppServices(services); + } - /// - /// Configures the web framework pipeline. - /// - public void Configure(IApplicationBuilder app) - { - var startupService = app.ApplicationServices.GetService(); + /// + /// Configures the web framework pipeline. + /// + public void Configure(IApplicationBuilder app) + { + var startupService = app.ApplicationServices.GetService(); - if (Environment.IsDevelopment()) - { - app.UseBrowserLink(); - app.UseSerilogRequestLogging(); - } + if (Environment.IsDevelopment()) + { + app.UseBrowserLink(); + app.UseSerilogRequestLogging(); + } - if (Configuration.WebServer.RequireHttps) - app.UseHttpsRedirection(); + if (Configuration.WebServer.RequireHttps) + app.UseHttpsRedirection(); - if (Configuration.Debug.DetailedExceptions) - app.UseDeveloperExceptionPage(); + if (Configuration.Debug.DetailedExceptions) + app.UseDeveloperExceptionPage(); - InitDatabase(app); + InitDatabase(app); - app.UseForwardedHeaders(GetforwardedHeadersOptions()) - .UseStatusCodePagesWithReExecute("/error/{0}") - .UseStaticFiles() - .UseRequestLocalization(LocaleProvider.GetLocaleCode()) - .Use(startupService.RenderLoadingPage) - .UseRouting() - .UseAuthentication() - .UseAuthorization() - .UseSession() - .UseCookiePolicy() - .UseEndpoints(x => - { - x.MapAreaControllerRoute("admin", "Admin", "admin/{controller}/{action}/{id?}"); - x.MapControllers(); - }); - } + app.UseForwardedHeaders(GetforwardedHeadersOptions()) + .UseStatusCodePagesWithReExecute("/error/{0}") + .UseStaticFiles() + .UseRequestLocalization(LocaleProvider.GetLocaleCode()) + .Use(startupService.RenderLoadingPage) + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseSession() + .UseCookiePolicy() + .UseEndpoints(x => + { + x.MapAreaControllerRoute("admin", "Admin", "admin/{controller}/{action}/{id?}"); + x.MapControllers(); + }); + } - /// - /// Configures the options for header forwarding. - /// - private ForwardedHeadersOptions GetforwardedHeadersOptions() + /// + /// Configures the options for header forwarding. + /// + private ForwardedHeadersOptions GetforwardedHeadersOptions() + { + var opts = new ForwardedHeadersOptions { - var opts = new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.All, - }; - opts.KnownProxies.Clear(); - opts.KnownNetworks.Clear(); - return opts; - } + ForwardedHeaders = ForwardedHeaders.All, + }; + opts.KnownProxies.Clear(); + opts.KnownNetworks.Clear(); + return opts; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/FactDefinition.cs b/src/Bonsai/Code/DomainModel/Facts/FactDefinition.cs index 7ab063d4..f950688a 100644 --- a/src/Bonsai/Code/DomainModel/Facts/FactDefinition.cs +++ b/src/Bonsai/Code/DomainModel/Facts/FactDefinition.cs @@ -2,86 +2,85 @@ using System.Linq; using Bonsai.Code.DomainModel.Facts.Models; -namespace Bonsai.Code.DomainModel.Facts +namespace Bonsai.Code.DomainModel.Facts; + +/// +/// Blueprint of a fact's template and editor. +/// +public class FactDefinition : IFactDefinition + where T: FactModelBase { - /// - /// Blueprint of a fact's template and editor. - /// - public class FactDefinition : IFactDefinition - where T: FactModelBase + public FactDefinition(string id, string title, string shortTitle = null) { - public FactDefinition(string id, string title, string shortTitle = null) - { - Id = id; - Title = title; - Kind = typeof(T); + Id = id; + Title = title; + Kind = typeof(T); - var parts = (shortTitle ?? title).Split('|'); - ShortTitle = shortTitle; - ShortTitleSingle = parts.First(); - ShortTitleMultiple = parts.Last(); - } + var parts = (shortTitle ?? title).Split('|'); + ShortTitle = shortTitle; + ShortTitleSingle = parts.First(); + ShortTitleMultiple = parts.Last(); + } - /// - /// Unique ID for referencing the fact. - /// - public string Id { get; } + /// + /// Unique ID for referencing the fact. + /// + public string Id { get; } - /// - /// Readable title. - /// - public string Title { get; } + /// + /// Readable title. + /// + public string Title { get; } - /// - /// The original short title. - /// Required for json deserialization. - /// - public string ShortTitle { get; } + /// + /// The original short title. + /// Required for json deserialization. + /// + public string ShortTitle { get; } - /// - /// Short title for displaying in the info block (with single value). - /// - public string ShortTitleSingle { get; } + /// + /// Short title for displaying in the info block (with single value). + /// + public string ShortTitleSingle { get; } - /// - /// Short title for displaying in the info block (with multiple values). - /// - public string ShortTitleMultiple { get; } + /// + /// Short title for displaying in the info block (with multiple values). + /// + public string ShortTitleMultiple { get; } - /// - /// Type of the fact's kind. - /// - public Type Kind { get; } - } + /// + /// Type of the fact's kind. + /// + public Type Kind { get; } +} +/// +/// Shared interface for untyped fact definitions. +/// +public interface IFactDefinition +{ /// - /// Shared interface for untyped fact definitions. + /// Unique ID for referencing the fact. /// - public interface IFactDefinition - { - /// - /// Unique ID for referencing the fact. - /// - string Id { get; } + string Id { get; } - /// - /// Readable title. - /// - string Title { get; } + /// + /// Readable title. + /// + string Title { get; } - /// - /// Short title for displaying in the info block. - /// - string ShortTitleSingle { get; } + /// + /// Short title for displaying in the info block. + /// + string ShortTitleSingle { get; } - /// - /// Short title for displaying in the info block (with multiple values). - /// - string ShortTitleMultiple { get; } + /// + /// Short title for displaying in the info block (with multiple values). + /// + string ShortTitleMultiple { get; } - /// - /// Type of the fact's kind. - /// - Type Kind { get; } - } -} + /// + /// Type of the fact's kind. + /// + Type Kind { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/FactDefinitionGroup.cs b/src/Bonsai/Code/DomainModel/Facts/FactDefinitionGroup.cs index 40272144..fddf32db 100644 --- a/src/Bonsai/Code/DomainModel/Facts/FactDefinitionGroup.cs +++ b/src/Bonsai/Code/DomainModel/Facts/FactDefinitionGroup.cs @@ -1,40 +1,39 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace Bonsai.Code.DomainModel.Facts +namespace Bonsai.Code.DomainModel.Facts; + +/// +/// A group of correlated facts. +/// +public class FactDefinitionGroup { - /// - /// A group of correlated facts. - /// - public class FactDefinitionGroup + public FactDefinitionGroup(string id, string title, bool isMain, params IFactDefinition[] defs) { - public FactDefinitionGroup(string id, string title, bool isMain, params IFactDefinition[] defs) - { - Id = id; - Title = title; - IsMain = isMain; - Defs = defs; - } + Id = id; + Title = title; + IsMain = isMain; + Defs = defs; + } - /// - /// Unique ID of the group. - /// - public string Id { get; } + /// + /// Unique ID of the group. + /// + public string Id { get; } - /// - /// Readable title of the group. - /// - public string Title { get; } + /// + /// Readable title of the group. + /// + public string Title { get; } - /// - /// Flag indicating that this group should be shown at the top. - /// - public bool IsMain { get; } + /// + /// Flag indicating that this group should be shown at the top. + /// + public bool IsMain { get; } - /// - /// Nested fact definitions. - /// - [JsonIgnore] - public IReadOnlyList Defs { get; } - } -} + /// + /// Nested fact definitions. + /// + [JsonIgnore] + public IReadOnlyList Defs { get; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/FactDefinitions.cs b/src/Bonsai/Code/DomainModel/Facts/FactDefinitions.cs index d778007b..f98ecd86 100644 --- a/src/Bonsai/Code/DomainModel/Facts/FactDefinitions.cs +++ b/src/Bonsai/Code/DomainModel/Facts/FactDefinitions.cs @@ -1,156 +1,156 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Bonsai.Code.DomainModel.Facts.Models; using Bonsai.Data.Models; using Bonsai.Localization; -namespace Bonsai.Code.DomainModel.Facts +namespace Bonsai.Code.DomainModel.Facts; + +/// +/// The mapping between facts and their corresponding templates. +/// +public static class FactDefinitions { - /// - /// The mapping between facts and their corresponding templates. - /// - public static class FactDefinitions + static FactDefinitions() { - static FactDefinitions() + Groups = new Dictionary { - Groups = new Dictionary - { - [PageType.Person] = new[] - { - new FactDefinitionGroup( - "Main", - Texts.Facts_Group_Main, - true, - new FactDefinition("Name", Texts.Facts_Person_Name, Texts.Facts_Person_NameS) - ), - new FactDefinitionGroup( - "Birth", - Texts.Facts_Group_Birth, - true, - new FactDefinition("Date", Texts.Facts_Birth_Day, Texts.Facts_Birth_DayS), - new FactDefinition("Place", Texts.Facts_Birth_Place, Texts.Facts_Birth_PlaceS) - ), - new FactDefinitionGroup( - "Death", - Texts.Facts_Group_Death, - true, - new FactDefinition("Date", Texts.Facts_Death_Date, Texts.Facts_Death_DateS), - new FactDefinition("Place", Texts.Facts_Death_Place, Texts.Facts_Death_PlaceS), - new FactDefinition("Cause", Texts.Facts_Death_Cause, Texts.Facts_Death_CauseS), - new FactDefinition("Burial", Texts.Facts_Death_Burial, Texts.Facts_Death_BurialS) - ), - new FactDefinitionGroup( - "Bio", - Texts.Facts_Group_Bio, - false, - new FactDefinition("Gender", Texts.Facts_Person_Gender), - new FactDefinition("Blood", Texts.Facts_Person_BloodType, Texts.Facts_Person_BloodTypeS), - new FactDefinition("Eyes", Texts.Facts_Person_EyeColor, Texts.Facts_Person_EyeColorS), - new FactDefinition("Hair", Texts.Facts_Person_HairColor, Texts.Facts_Person_HairColorS) - ), - new FactDefinitionGroup( - "Person", - Texts.Facts_Group_Person, - false, - new FactDefinition("Language", Texts.Facts_Person_Language, Texts.Facts_Person_LanguageS), - new FactDefinition("Skill", Texts.Facts_Person_Skill), - new FactDefinition("Profession", Texts.Facts_Person_Profession, Texts.Facts_Person_ProfessionS), - new FactDefinition("Religion", Texts.Facts_Person_Religion, Texts.Facts_Person_ReligionS) - ), - new FactDefinitionGroup( - "Meta", - Texts.Facts_Group_Meta, - false, - new FactDefinition("SocialProfiles", Texts.Facts_Person_SocialProfiles) - ) - }, + [PageType.Person] = + [ + new FactDefinitionGroup( + "Main", + Texts.Facts_Group_Main, + true, + new FactDefinition("Name", Texts.Facts_Person_Name, Texts.Facts_Person_NameS) + ), + new FactDefinitionGroup( + "Birth", + Texts.Facts_Group_Birth, + true, + new FactDefinition("Date", Texts.Facts_Birth_Day, Texts.Facts_Birth_DayS), + new FactDefinition("Place", Texts.Facts_Birth_Place, Texts.Facts_Birth_PlaceS) + ), + new FactDefinitionGroup( + "Death", + Texts.Facts_Group_Death, + true, + new FactDefinition("Date", Texts.Facts_Death_Date, Texts.Facts_Death_DateS), + new FactDefinition("Place", Texts.Facts_Death_Place, Texts.Facts_Death_PlaceS), + new FactDefinition("Cause", Texts.Facts_Death_Cause, Texts.Facts_Death_CauseS), + new FactDefinition("Burial", Texts.Facts_Death_Burial, Texts.Facts_Death_BurialS) + ), + new FactDefinitionGroup( + "Bio", + Texts.Facts_Group_Bio, + false, + new FactDefinition("Gender", Texts.Facts_Person_Gender), + new FactDefinition("Blood", Texts.Facts_Person_BloodType, Texts.Facts_Person_BloodTypeS), + new FactDefinition("Eyes", Texts.Facts_Person_EyeColor, Texts.Facts_Person_EyeColorS), + new FactDefinition("Hair", Texts.Facts_Person_HairColor, Texts.Facts_Person_HairColorS) + ), + new FactDefinitionGroup( + "Person", + Texts.Facts_Group_Person, + false, + new FactDefinition("Language", Texts.Facts_Person_Language, Texts.Facts_Person_LanguageS), + new FactDefinition("Skill", Texts.Facts_Person_Skill), + new FactDefinition("Profession", Texts.Facts_Person_Profession, Texts.Facts_Person_ProfessionS), + new FactDefinition("Religion", Texts.Facts_Person_Religion, Texts.Facts_Person_ReligionS) + ), + new FactDefinitionGroup( + "Meta", + Texts.Facts_Group_Meta, + false, + new FactDefinition("SocialProfiles", Texts.Facts_Person_SocialProfiles) + ) + ], - [PageType.Pet] = new[] - { - new FactDefinitionGroup( - "Main", - Texts.Facts_Group_Main, - true, - new FactDefinition("Name", Texts.Facts_Pet_Name) - ), - new FactDefinitionGroup( - "Birth", - Texts.Facts_Group_Birth, - true, - new FactDefinition("Date", Texts.Facts_Birth_Day, Texts.Facts_Birth_DayS), - new FactDefinition("Place", Texts.Facts_Birth_Place, Texts.Facts_Birth_PlaceS) - ), - new FactDefinitionGroup( - "Death", - Texts.Facts_Group_Death, - true, - new FactDefinition("Date", Texts.Facts_Death_Date, Texts.Facts_Death_DateS), - new FactDefinition("Place", Texts.Facts_Death_Place, Texts.Facts_Death_PlaceS), - new FactDefinition("Cause", Texts.Facts_Death_Cause, Texts.Facts_Death_CauseS), - new FactDefinition("Burial", Texts.Facts_Death_Burial, Texts.Facts_Death_BurialS) - ), - new FactDefinitionGroup( - "Bio", - Texts.Facts_Group_Bio, - true, - new FactDefinition("Gender", Texts.Facts_Pet_Gender), - new FactDefinition("Species", Texts.Facts_Pet_Species), - new FactDefinition("Breed", Texts.Facts_Pet_Breed), - new FactDefinition("Color", Texts.Facts_Pet_Color) - ) - }, + [PageType.Pet] = + [ + new FactDefinitionGroup( + "Main", + Texts.Facts_Group_Main, + true, + new FactDefinition("Name", Texts.Facts_Pet_Name) + ), + new FactDefinitionGroup( + "Birth", + Texts.Facts_Group_Birth, + true, + new FactDefinition("Date", Texts.Facts_Birth_Day, Texts.Facts_Birth_DayS), + new FactDefinition("Place", Texts.Facts_Birth_Place, Texts.Facts_Birth_PlaceS) + ), + new FactDefinitionGroup( + "Death", + Texts.Facts_Group_Death, + true, + new FactDefinition("Date", Texts.Facts_Death_Date, Texts.Facts_Death_DateS), + new FactDefinition("Place", Texts.Facts_Death_Place, Texts.Facts_Death_PlaceS), + new FactDefinition("Cause", Texts.Facts_Death_Cause, Texts.Facts_Death_CauseS), + new FactDefinition("Burial", Texts.Facts_Death_Burial, Texts.Facts_Death_BurialS) + ), + new FactDefinitionGroup( + "Bio", + Texts.Facts_Group_Bio, + true, + new FactDefinition("Gender", Texts.Facts_Pet_Gender), + new FactDefinition("Species", Texts.Facts_Pet_Species), + new FactDefinition("Breed", Texts.Facts_Pet_Breed), + new FactDefinition("Color", Texts.Facts_Pet_Color) + ) + ], - [PageType.Location] = new[] - { - new FactDefinitionGroup( - "Main", - Texts.Facts_Group_Main, - true, - new FactDefinition("Location", Texts.Facts_Location_Address), - new FactDefinition("Opening", Texts.Facts_Location_Opening), - new FactDefinition("Shutdown", Texts.Facts_Location_Shutdown) - ) - }, + [PageType.Location] = + [ + new FactDefinitionGroup( + "Main", + Texts.Facts_Group_Main, + true, + new FactDefinition("Location", Texts.Facts_Location_Address), + new FactDefinition("Opening", Texts.Facts_Location_Opening), + new FactDefinition("Shutdown", Texts.Facts_Location_Shutdown) + ) + ], - [PageType.Event] = new[] - { - new FactDefinitionGroup( - "Main", - Texts.Facts_Group_Main, - true, - new FactDefinition("Date", Texts.Facts_Event_Date) - ) - }, + [PageType.Event] = + [ + new FactDefinitionGroup( + "Main", + Texts.Facts_Group_Main, + true, + new FactDefinition("Date", Texts.Facts_Event_Date) + ) + ], - [PageType.Other] = new FactDefinitionGroup[0] - }; + [PageType.Other] = Array.Empty() + }; - Definitions = Groups.ToDictionary( - x => x.Key, - x => x.Value.SelectMany(y => y.Defs.Select(z => new { Key = y.Id + "." + z.Id, Fact = z })) - .ToDictionary(y => y.Key, y => y.Fact) - ); - } + Definitions = Groups.ToDictionary( + x => x.Key, + x => x.Value.SelectMany(y => y.Defs.Select(z => new { Key = y.Id + "." + z.Id, Fact = z })) + .ToDictionary(y => y.Key, y => y.Fact) + ); + } - /// - /// Available groups of fact definitions. - /// - public static readonly Dictionary Groups; + /// + /// Available groups of fact definitions. + /// + public static readonly Dictionary Groups; - /// - /// Lookup for fact definitions. - /// - public static readonly Dictionary> Definitions; + /// + /// Lookup for fact definitions. + /// + public static readonly Dictionary> Definitions; - /// - /// Finds a definition. - /// - public static IFactDefinition TryGetDefinition(PageType type, string key) - { - return Definitions.TryGetValue(type, out var pageLookup) - && pageLookup.TryGetValue(key, out var def) - ? def - : null; - } + /// + /// Finds a definition. + /// + public static IFactDefinition TryGetDefinition(PageType type, string key) + { + return Definitions.TryGetValue(type, out var pageLookup) + && pageLookup.TryGetValue(key, out var def) + ? def + : null; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/AddressFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/AddressFactModel.cs index 8fa533a7..c5d802f0 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/AddressFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/AddressFactModel.cs @@ -1,9 +1,6 @@ -namespace Bonsai.Code.DomainModel.Facts.Models -{ - /// - /// Display for address-related facts. - /// - public class AddressFactModel: StringFactModel - { - } -} +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// Display for address-related facts. +/// +public class AddressFactModel: StringFactModel; \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/BirthDateFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/BirthDateFactModel.cs index 208205ad..33e1621c 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/BirthDateFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/BirthDateFactModel.cs @@ -1,9 +1,6 @@ -namespace Bonsai.Code.DomainModel.Facts.Models -{ - /// - /// Special logic for displaying a birthday. - /// - public class BirthDateFactModel: DateFactModel - { - } -} +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// Special logic for displaying a birthday. +/// +public class BirthDateFactModel: DateFactModel; \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/BloodTypeFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/BloodTypeFactModel.cs index fc5e55a4..928d50c0 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/BloodTypeFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/BloodTypeFactModel.cs @@ -1,26 +1,31 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +using JetBrains.Annotations; + +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying blood type. +/// +public class BloodTypeFactModel: FactModelBase { /// - /// The template for specifying blood type. + /// Blood type. /// - public class BloodTypeFactModel: FactModelBase - { - /// - /// Blood type. - /// - public BloodType Type { get; set; } + public BloodType Type { get; set; } - /// - /// Rhesus factor (if known). - /// - public bool? RhesusFactor { get; set; } - } - - public enum BloodType - { - O, - A, - B, - AB - } + /// + /// Rhesus factor (if known). + /// + public bool? RhesusFactor { get; set; } } + +public enum BloodType +{ + [UsedImplicitly] + O, + [UsedImplicitly] + A, + [UsedImplicitly] + B, + [UsedImplicitly] + AB +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/ContactsFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/ContactsFactModel.cs index c035b6b6..dc6ea0b2 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/ContactsFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/ContactsFactModel.cs @@ -4,90 +4,89 @@ using Bonsai.Data.Models; using Bonsai.Localization; -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// Template for specifying known social profile links. +/// +public class ContactsFactModel: FactListModelBase { - /// - /// Template for specifying known social profile links. - /// - public class ContactsFactModel: FactListModelBase + public override void Validate() { - public override void Validate() + for (var i = 1; i <= Values.Length; i++) { - for (var i = 1; i <= Values.Length; i++) - { - var item = Values[i-1]; - if (!Enum.IsDefined(item.Type)) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_UnknownType, i)); + var item = Values[i-1]; + if (!Enum.IsDefined(item.Type)) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_UnknownType, i)); - if(string.IsNullOrEmpty(item.Value)) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_UnknownValue, i)); + if(string.IsNullOrEmpty(item.Value)) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_UnknownValue, i)); - if (item.Type == ContactType.Email) - { - if(!Regex.IsMatch(item.Value, ".+@.+\\..+")) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidEmail, i)); - } - else if (item.Type == ContactType.Phone) - { - if (!Regex.IsMatch(item.Value, "\\+[0-9]+")) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidPhone, i)); - } - else if (item.Type == ContactType.Telegram || item.Type == ContactType.Twitter) - { - if(!item.Value.StartsWith("https://")) - if(!Regex.IsMatch(item.Value, "@[a-z0-9_-]+")) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidHandle, i)); - } - else if (!item.Value.StartsWith("https://")) - throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidLink, i)); + if (item.Type == ContactType.Email) + { + if(!Regex.IsMatch(item.Value, ".+@.+\\..+")) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidEmail, i)); } + else if (item.Type == ContactType.Phone) + { + if (!Regex.IsMatch(item.Value, "\\+[0-9]+")) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidPhone, i)); + } + else if (item.Type == ContactType.Telegram || item.Type == ContactType.Twitter) + { + if(!item.Value.StartsWith("https://")) + if(!Regex.IsMatch(item.Value, "@[a-z0-9_-]+")) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidHandle, i)); + } + else if (!item.Value.StartsWith("https://")) + throw new ValidationException(nameof(Page.Facts), string.Format(Texts.Admin_Page_Contacts_Validation_InvalidLink, i)); } } +} +/// +/// Single social profile link. +/// +public class ContactFactItem +{ /// - /// Single social profile link. + /// Type of the profile. /// - public class ContactFactItem - { - /// - /// Type of the profile. - /// - public ContactType Type { get; set; } - - /// - /// Link to the profile. - /// - public string Value { get; set; } + public ContactType Type { get; set; } - /// - /// Returns the public hyperlink to this contact. - /// - public string GetLink() - { - return Type switch - { - ContactType.Phone => "tel:" + Value, - ContactType.Email => "mailto:" + Value, - ContactType.Telegram => Value.StartsWith('@') ? "https://t.me/" + Value.Substring(1) : Value, - ContactType.Twitter => Value.StartsWith('@') ? "https://twitter.com/" + Value.Substring(1) : Value, - _ => Value - }; - } - } + /// + /// Link to the profile. + /// + public string Value { get; set; } /// - /// Known social profile links (to display icons properly). + /// Returns the public hyperlink to this contact. /// - public enum ContactType + public string GetLink() { - Facebook, - Twitter, - Odnoklassniki, - Vkontakte, - Telegram, - Youtube, - Github, - Email, - Phone, + return Type switch + { + ContactType.Phone => "tel:" + Value, + ContactType.Email => "mailto:" + Value, + ContactType.Telegram => Value.StartsWith('@') ? "https://t.me/" + Value[1..] : Value, + ContactType.Twitter => Value.StartsWith('@') ? "https://twitter.com/" + Value[1..] : Value, + _ => Value + }; } } + +/// +/// Known social profile links (to display icons properly). +/// +public enum ContactType +{ + Facebook, + Twitter, + Odnoklassniki, + Vkontakte, + Telegram, + Youtube, + Github, + Email, + Phone, +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/DateFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/DateFactModel.cs index ad35a574..eae6166c 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/DateFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/DateFactModel.cs @@ -1,15 +1,14 @@ using Bonsai.Code.Utils.Date; -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying a date. +/// +public class DateFactModel: FactModelBase { /// - /// The template for specifying a date. + /// Arbitrary date value. /// - public class DateFactModel: FactModelBase - { - /// - /// Arbitrary date value. - /// - public FuzzyDate Value { get; set; } - } -} + public FuzzyDate Value { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/DeathDateFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/DeathDateFactModel.cs index ab74d888..543bca46 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/DeathDateFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/DeathDateFactModel.cs @@ -3,30 +3,29 @@ using Bonsai.Data.Models; using Bonsai.Localization; -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// Special logic for displaying a death date. +/// +public class DeathDateFactModel: FactModelBase { /// - /// Special logic for displaying a death date. + /// Arbitrary date value (optional). /// - public class DeathDateFactModel: FactModelBase - { - /// - /// Arbitrary date value (optional). - /// - public FuzzyDate? Value { get; set; } + public FuzzyDate? Value { get; set; } - /// - /// Flag indicating that the person is dead, but the exact date is unknown. - /// - public bool IsUnknown { get; set; } + /// + /// Flag indicating that the person is dead, but the exact date is unknown. + /// + public bool IsUnknown { get; set; } - /// - /// Inner check. - /// - public override void Validate() - { - if (IsUnknown == Value.HasValue) - throw new ValidationException(nameof(Page.Facts), Texts.Admin_Page_Facts_Validation_DeathDate); - } + /// + /// Inner check. + /// + public override void Validate() + { + if (IsUnknown == Value.HasValue) + throw new ValidationException(nameof(Page.Facts), Texts.Admin_Page_Facts_Validation_DeathDate); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/DurationFactItem.cs b/src/Bonsai/Code/DomainModel/Facts/Models/DurationFactItem.cs index cf440053..04222958 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/DurationFactItem.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/DurationFactItem.cs @@ -1,15 +1,14 @@ using Bonsai.Code.Utils.Date; -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// A template item that has a duration period. +/// +public class DurationFactItem { /// - /// A template item that has a duration period. + /// Range of the item's actuality. /// - public class DurationFactItem - { - /// - /// Range of the item's actuality. - /// - public FuzzyRange? Duration { get; set; } - } -} + public FuzzyRange? Duration { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/FactListModelBase.cs b/src/Bonsai/Code/DomainModel/Facts/Models/FactListModelBase.cs index e9df52d7..78fc87dd 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/FactListModelBase.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/FactListModelBase.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying a list of values. +/// +public class FactListModelBase: FactModelBase { /// - /// The template for specifying a list of values. + /// List of values. /// - public class FactListModelBase: FactModelBase - { - /// - /// List of values. - /// - public T[] Values { get; set; } + public T[] Values { get; set; } - /// - /// Flag indicating that this fact does not contain any data. - /// - public override bool IsHidden => Values == null || Values.Length == 0; + /// + /// Flag indicating that this fact does not contain any data. + /// + public override bool IsHidden => Values == null || Values.Length == 0; - /// - /// Returns the appropriate short title depending on the number of values. - /// - public override string ShortTitle => Values.Length == 1 ? Definition.ShortTitleSingle : Definition.ShortTitleMultiple; - } -} + /// + /// Returns the appropriate short title depending on the number of values. + /// + public override string ShortTitle => Values.Length == 1 ? Definition.ShortTitleSingle : Definition.ShortTitleMultiple; +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/FactModelBase.cs b/src/Bonsai/Code/DomainModel/Facts/Models/FactModelBase.cs index 5ade5d41..c060796e 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/FactModelBase.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/FactModelBase.cs @@ -1,32 +1,31 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// Base class for all fact templates. +/// +public abstract class FactModelBase { /// - /// Base class for all fact templates. + /// Definition of the current fact. /// - public abstract class FactModelBase - { - /// - /// Definition of the current fact. - /// - public IFactDefinition Definition { get; set; } + public IFactDefinition Definition { get; set; } - /// - /// Flag indicating that the current fact does not contain valuable information - /// and must be omitted from the side bar. - /// - public virtual bool IsHidden => false; + /// + /// Flag indicating that the current fact does not contain valuable information + /// and must be omitted from the side bar. + /// + public virtual bool IsHidden => false; - /// - /// Title. - /// - public virtual string ShortTitle => Definition.ShortTitleSingle; + /// + /// Title. + /// + public virtual string ShortTitle => Definition.ShortTitleSingle; - /// - /// Ensures that the fact contains correct data. - /// Throws an error otherwise. - /// - public virtual void Validate() - { - } + /// + /// Ensures that the fact contains correct data. + /// Throws an error otherwise. + /// + public virtual void Validate() + { } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/GenderFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/GenderFactModel.cs index a75b8468..aaba54c8 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/GenderFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/GenderFactModel.cs @@ -1,13 +1,12 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// A template for gender specification. +/// +public class GenderFactModel: FactModelBase { /// - /// A template for gender specification. + /// Flag indicating the gender. /// - public class GenderFactModel: FactModelBase - { - /// - /// Flag indicating the gender. - /// - public bool IsMale { get; set; } - } -} + public bool IsMale { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/HumanNameFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/HumanNameFactModel.cs index d47611bf..bd2d3c96 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/HumanNameFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/HumanNameFactModel.cs @@ -1,35 +1,34 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template definition for a person's name. +/// +public class HumanNameFactModel: FactListModelBase { /// - /// The template definition for a person's name. + /// Hides the names list from the side bar unless there are at least two. + /// The name is always displayed at the page's title. /// - public class HumanNameFactModel: FactListModelBase - { - /// - /// Hides the names list from the side bar unless there are at least two. - /// The name is always displayed at the page's title. - /// - public override bool IsHidden => Values.Length < 2; - } + public override bool IsHidden => Values.Length < 2; +} +/// +/// A single recorded name with date ranges. +/// +public class HumanNameFactItem: DurationFactItem +{ /// - /// A single recorded name with date ranges. + /// First name. /// - public class HumanNameFactItem: DurationFactItem - { - /// - /// First name. - /// - public string FirstName { get; set; } + public string FirstName { get; set; } - /// - /// Middle name. - /// - public string MiddleName { get; set; } + /// + /// Middle name. + /// + public string MiddleName { get; set; } - /// - /// Last name. - /// - public string LastName { get; set; } - } -} + /// + /// Last name. + /// + public string LastName { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/LanguageFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/LanguageFactModel.cs index 5d4b6d69..3c5018e7 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/LanguageFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/LanguageFactModel.cs @@ -1,37 +1,34 @@ -namespace Bonsai.Code.DomainModel.Facts.Models -{ - /// - /// The template for specifying known languages. - /// - public class LanguageFactModel: FactListModelBase - { - } +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying known languages. +/// +public class LanguageFactModel: FactListModelBase; +/// +/// Information about a single known language. +/// +public class LanguageFactItem : DurationFactItem +{ /// - /// Information about a single known language. + /// Name of the language. /// - public class LanguageFactItem : DurationFactItem - { - /// - /// Name of the language. - /// - public string Name { get; set; } - - /// - /// Language proficiency. - /// - public LanguageProficiency? Proficiency { get; set; } - } + public string Name { get; set; } /// - /// The proficiency in the specified language. + /// Language proficiency. /// - // ReSharper disable UnusedMember.Global - public enum LanguageProficiency - { - Beginner, - Intermediate, - Profound, - Native - } + public LanguageProficiency? Proficiency { get; set; } } + +/// +/// The proficiency in the specified language. +/// +// ReSharper disable UnusedMember.Global +public enum LanguageProficiency +{ + Beginner, + Intermediate, + Profound, + Native +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/NameFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/NameFactModel.cs index 72134727..c742f36c 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/NameFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/NameFactModel.cs @@ -1,18 +1,17 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying a name of a pet, event or location. +/// +public class NameFactModel: FactModelBase { /// - /// The template for specifying a name of a pet, event or location. + /// Name as a string. /// - public class NameFactModel: FactModelBase - { - /// - /// Name as a string. - /// - public string Value { get; set; } + public string Value { get; set; } - /// - /// Should not be displayed. - /// - public override bool IsHidden => true; - } -} + /// + /// Should not be displayed. + /// + public override bool IsHidden => true; +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/SkillFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/SkillFactModel.cs index 98c8b015..a5a28b30 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/SkillFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/SkillFactModel.cs @@ -1,36 +1,33 @@ -namespace Bonsai.Code.DomainModel.Facts.Models -{ - /// - /// The template for specifying posessed skills. - /// - public class SkillFactModel : FactListModelBase - { - } +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying posessed skills. +/// +public class SkillFactModel : FactListModelBase; +/// +/// Information about a single skill. +/// +public class SkillFactItem : DurationFactItem +{ /// - /// Information about a single skill. + /// Name of the skill. /// - public class SkillFactItem : DurationFactItem - { - /// - /// Name of the skill. - /// - public string Name { get; set; } - - /// - /// Proficiency of the skill. - /// - public SkillProficiency? Proficiency { get; set; } - } + public string Name { get; set; } /// - /// The proficiency in current skill or hobby. + /// Proficiency of the skill. /// - // ReSharper disable UnusedMember.Global - public enum SkillProficiency - { - Beginner, - Intermediate, - Profound - } + public SkillProficiency? Proficiency { get; set; } } + +/// +/// The proficiency in current skill or hobby. +/// +// ReSharper disable UnusedMember.Global +public enum SkillProficiency +{ + Beginner, + Intermediate, + Profound +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/StringFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/StringFactModel.cs index 0d748664..1af2b48c 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/StringFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/StringFactModel.cs @@ -1,13 +1,12 @@ -namespace Bonsai.Code.DomainModel.Facts.Models +namespace Bonsai.Code.DomainModel.Facts.Models; + +/// +/// The template for specifying arbitrary string-based facts. +/// +public class StringFactModel: FactModelBase { /// - /// The template for specifying arbitrary string-based facts. + /// Arbitrary value. /// - public class StringFactModel: FactModelBase - { - /// - /// Arbitrary value. - /// - public string Value { get; set; } - } -} + public string Value { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Facts/Models/StringListFactModel.cs b/src/Bonsai/Code/DomainModel/Facts/Models/StringListFactModel.cs index 2ce51f6b..4b47bdf5 100644 --- a/src/Bonsai/Code/DomainModel/Facts/Models/StringListFactModel.cs +++ b/src/Bonsai/Code/DomainModel/Facts/Models/StringListFactModel.cs @@ -1,20 +1,17 @@ -namespace Bonsai.Code.DomainModel.Facts.Models -{ - /// - /// A list of string values, defined by date. - /// - public class StringListFactModel: FactListModelBase - { - } +namespace Bonsai.Code.DomainModel.Facts.Models; +/// +/// A list of string values, defined by date. +/// +public class StringListFactModel: FactListModelBase; + +/// +/// Current value & duration wrapper. +/// +public class StringListFactItem : DurationFactItem +{ /// - /// Current value & duration wrapper. + /// Current value. /// - public class StringListFactItem : DurationFactItem - { - /// - /// Current value. - /// - public string Value { get; set; } - } -} + public string Value { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Media/MediaSize.cs b/src/Bonsai/Code/DomainModel/Media/MediaSize.cs index 9fdeabbb..855e0d8f 100644 --- a/src/Bonsai/Code/DomainModel/Media/MediaSize.cs +++ b/src/Bonsai/Code/DomainModel/Media/MediaSize.cs @@ -1,25 +1,24 @@ -namespace Bonsai.Code.DomainModel.Media +namespace Bonsai.Code.DomainModel.Media; + +public enum MediaSize { - public enum MediaSize - { - /// - /// Original dimensions of the uploaded photo. - /// - Original, + /// + /// Original dimensions of the uploaded photo. + /// + Original, - /// - /// Photo for viewing in the embedded viewer. - /// - Large, + /// + /// Photo for viewing in the embedded viewer. + /// + Large, - /// - /// Photo for info block. - /// - Medium, + /// + /// Photo for info block. + /// + Medium, - /// - /// Photo for media tumbnail. - /// - Small, - } -} + /// + /// Photo for media tumbnail. + /// + Small, +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Relations/RelationBinding.cs b/src/Bonsai/Code/DomainModel/Relations/RelationBinding.cs index b02491c7..a4d2018e 100644 --- a/src/Bonsai/Code/DomainModel/Relations/RelationBinding.cs +++ b/src/Bonsai/Code/DomainModel/Relations/RelationBinding.cs @@ -1,32 +1,11 @@ using Bonsai.Data.Models; -namespace Bonsai.Code.DomainModel.Relations -{ - /// - /// A list of allowed relation types between two types of pages. - /// - public class RelationBinding - { - public RelationBinding(PageType sourceType, PageType destinationType, RelationType[] relTypes) - { - SourceType = sourceType; - DestinationType = destinationType; - RelationTypes = relTypes; - } - - /// - /// Type of the source page. - /// - public PageType SourceType { get; } - - /// - /// Type of the destination page. - /// - public PageType DestinationType { get; } - - /// - /// Type of the relation. - /// - public RelationType[] RelationTypes { get; } - } -} +namespace Bonsai.Code.DomainModel.Relations; + +/// +/// A list of allowed relation types between two types of pages. +/// Type of the source page. +/// Type of the destination page. +/// Types of the relation. +/// +public record RelationBinding(PageType SourceType, PageType DestinationType, RelationType[] RelationTypes); \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Relations/RelationContext.cs b/src/Bonsai/Code/DomainModel/Relations/RelationContext.cs index 3dc6a06b..6c393525 100644 --- a/src/Bonsai/Code/DomainModel/Relations/RelationContext.cs +++ b/src/Bonsai/Code/DomainModel/Relations/RelationContext.cs @@ -9,142 +9,142 @@ using Bonsai.Data.Utils; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Code.DomainModel.Relations +namespace Bonsai.Code.DomainModel.Relations; + +/// +/// Information about all known pages and relations. +/// +public class RelationContext { + #region Properties + /// - /// Information about all known pages and relations. + /// List of all pages currently available. /// - public class RelationContext - { - #region Properties + public IReadOnlyDictionary Pages { get; private set; } - /// - /// List of all pages currently available. - /// - public IReadOnlyDictionary Pages { get; private set; } + /// + /// List of all relations currently available. + /// + public IReadOnlyDictionary> Relations { get; private set; } - /// - /// List of all relations currently available. - /// - public IReadOnlyDictionary> Relations { get; private set; } + #endregion - #endregion + #region Methods - #region Methods + /// + /// Adds extra information about a page. + /// + public void Augment(PageExcerpt page) + { + var pages = (Dictionary) Pages; + pages[page.Id] = page; + } - /// - /// Adds extra information about a page. - /// - public void Augment(PageExcerpt page) + /// + /// Adds extra information about a relation. + /// + public void Augment(RelationExcerpt rel) + { + var rels = (Dictionary>) Relations; + if (rels.TryGetValue(rel.SourceId, out var rList)) { - var pages = (Dictionary) Pages; - pages[page.Id] = page; - } + var list = (List) rList; + var existing = list.FirstOrDefault(x => x.DestinationId == rel.DestinationId && x.Type == rel.Type); + if (existing != null) + list.Remove(existing); - /// - /// Adds extra information about a relation. - /// - public void Augment(RelationExcerpt rel) + list.Add(rel); + } + else { - var rels = (Dictionary>) Relations; - if (rels.TryGetValue(rel.SourceId, out var rList)) - { - var list = (List) rList; - var existing = list.FirstOrDefault(x => x.DestinationId == rel.DestinationId && x.Type == rel.Type); - if (existing != null) - list.Remove(existing); - - list.Add(rel); - } - else - { - rels[rel.SourceId] = new List {rel}; - } + rels[rel.SourceId] = new List {rel}; } + } - #endregion + #endregion + + #region Static constructor + + /// + /// Loads basic information about all pages. + /// + public static async Task LoadContextAsync(AppDbContext db, RelationContextOptions opts = null) + { + if(opts == null) + opts = new RelationContextOptions(); - #region Static constructor + var pages = await LoadPagesAsync(db, opts); + var rels = await LoadRelationsAsync(db, opts); - /// - /// Loads basic information about all pages. - /// - public static async Task LoadContextAsync(AppDbContext db, RelationContextOptions opts = null) + return new RelationContext { - if(opts == null) - opts = new RelationContextOptions(); + Pages = pages.ToDictionary(x => x.Id, x => x), + Relations = rels + }; + } - var pages = await LoadPagesAsync(db, opts); - var rels = await LoadRelationsAsync(db, opts); + /// + /// Loads the pages from the database. + /// + private static async Task> LoadPagesAsync(AppDbContext db, RelationContextOptions opts) + { + var query = db.Pages + .AsNoTracking() + .Include(x => x.LivingBeingOverview) + .Include(x => x.MainPhoto) + .Where(x => x.IsDeleted == false); - return new RelationContext - { - Pages = pages.ToDictionary(x => x.Id, x => x), - Relations = rels - }; - } + if (opts.PeopleOnly) + query = query.Where(x => x.Type == PageType.Person); + + var pageData = await query.Select(x => new {x.Id, x.Title, x.Key, x.Type, x.LivingBeingOverview, x.MainPhoto.FilePath}) + .ToListAsync(); - /// - /// Loads the pages from the database. - /// - private static async Task> LoadPagesAsync(AppDbContext db, RelationContextOptions opts) + var excerpts = pageData.Select(x => new PageExcerpt { - var query = db.Pages - .AsNoTracking() - .Include(x => x.LivingBeingOverview) - .Include(x => x.MainPhoto) - .Where(x => x.IsDeleted == false); - - if (opts.PeopleOnly) - query = query.Where(x => x.Type == PageType.Person); - - var pageData = await query.Select(x => new {x.Id, x.Title, x.Key, x.Type, x.LivingBeingOverview, x.MainPhoto.FilePath}) - .ToListAsync(); - - var excerpts = pageData.Select(x => new PageExcerpt - { - Id = x.Id, - Title = x.Title, - Type = x.Type, - Key = x.Key, - MainPhotoPath = x.FilePath, - BirthDate = FuzzyDate.TryParse(x.LivingBeingOverview?.BirthDate), - DeathDate = FuzzyDate.TryParse(x.LivingBeingOverview?.DeathDate), - IsDead = x.LivingBeingOverview?.IsDead ?? false, - Gender = x.LivingBeingOverview?.Gender, - ShortName = x.LivingBeingOverview?.ShortName, - MaidenName = x.LivingBeingOverview?.MaidenName - }); - - return excerpts.ToList(); + Id = x.Id, + Title = x.Title, + Type = x.Type, + Key = x.Key, + MainPhotoPath = x.FilePath, + BirthDate = FuzzyDate.TryParse(x.LivingBeingOverview?.BirthDate), + DeathDate = FuzzyDate.TryParse(x.LivingBeingOverview?.DeathDate), + IsDead = x.LivingBeingOverview?.IsDead ?? false, + Gender = x.LivingBeingOverview?.Gender, + ShortName = x.LivingBeingOverview?.ShortName, + MaidenName = x.LivingBeingOverview?.MaidenName + }); + + return excerpts.ToList(); + } + + /// + /// Loads the relations from the database. + /// + private static async Task>> LoadRelationsAsync(AppDbContext db, RelationContextOptions opts) + { + if (opts.PagesOnly) + return null; + + var query = db.Relations + .Where(x => x.Source.IsDeleted == false + && x.Destination.IsDeleted == false + && x.IsDeleted == false); + if (opts.PeopleOnly) + { + query = query.Where(x => x.Source.Type == PageType.Person + && x.Destination.Type == PageType.Person); } - /// - /// Loads the relations from the database. - /// - private static async Task>> LoadRelationsAsync(AppDbContext db, RelationContextOptions opts) + if (opts.TreeRelationsOnly) { - if (opts.PagesOnly) - return null; - - var query = db.Relations - .Where(x => x.Source.IsDeleted == false - && x.Destination.IsDeleted == false - && x.IsDeleted == false); - if (opts.PeopleOnly) - { - query = query.Where(x => x.Source.Type == PageType.Person - && x.Destination.Type == PageType.Person); - } - - if (opts.TreeRelationsOnly) - { - query = query.Where(x => x.Type == RelationType.Child - || x.Type == RelationType.Parent - || x.Type == RelationType.Spouse); - } - - var data = await query.Select(x => new RelationExcerpt + query = query.Where(x => x.Type == RelationType.Child + || x.Type == RelationType.Parent + || x.Type == RelationType.Spouse); + } + + var data = await query.Select(x => new RelationExcerpt { Id = x.Id, SourceId = x.SourceId, @@ -156,83 +156,82 @@ private static async Task x.SourceId).ToDictionary(x => x.Key, x => (IReadOnlyList) x.ToList()); + return data.GroupBy(x => x.SourceId).ToDictionary(x => x.Key, x => (IReadOnlyList) x.ToList()); + } + + #endregion + + #region Nested classes + + /// + /// Basic information about a page. + /// + [DebuggerDisplay("{Title} ({Id})")] + public class PageExcerpt : IEquatable + { + public Guid Id { get; set; } + + public string Title { get; set; } + public string Key { get; set; } + public PageType Type { get; set; } + public bool? Gender { get; set; } + public FuzzyDate? BirthDate { get; set; } + public FuzzyDate? DeathDate { get; set; } + public bool IsDead { get; set; } + public string ShortName { get; set; } + public string MaidenName { get; set; } + public string MainPhotoPath { get; set; } + + #region Equality members (auto-generated) + + public bool Equals(PageExcerpt other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); } - #endregion + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((PageExcerpt)obj); + } - #region Nested classes + public override int GetHashCode() + { + return Id.GetHashCode(); + } - /// - /// Basic information about a page. - /// - [DebuggerDisplay("{Title} ({Id})")] - public class PageExcerpt : IEquatable + public static bool operator ==(PageExcerpt left, PageExcerpt right) { - public Guid Id { get; set; } - - public string Title { get; set; } - public string Key { get; set; } - public PageType Type { get; set; } - public bool? Gender { get; set; } - public FuzzyDate? BirthDate { get; set; } - public FuzzyDate? DeathDate { get; set; } - public bool IsDead { get; set; } - public string ShortName { get; set; } - public string MaidenName { get; set; } - public string MainPhotoPath { get; set; } - - #region Equality members (auto-generated) - - public bool Equals(PageExcerpt other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((PageExcerpt)obj); - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public static bool operator ==(PageExcerpt left, PageExcerpt right) - { - return Equals(left, right); - } - - public static bool operator !=(PageExcerpt left, PageExcerpt right) - { - return !Equals(left, right); - } - - #endregion + return Equals(left, right); } - /// - /// Basic information about a relation between two pages. - /// - [DebuggerDisplay("{Type}: {SourceId} -> {DestinationId} ({Duration})")] - public class RelationExcerpt + public static bool operator !=(PageExcerpt left, PageExcerpt right) { - public Guid Id { get; set; } - - public Guid SourceId { get; set; } - public Guid DestinationId { get; set; } - public Guid? EventId { get; set; } - public RelationType Type { get; set; } - public FuzzyRange? Duration { get; set; } - public bool IsComplementary { get; set; } + return !Equals(left, right); } #endregion } -} + + /// + /// Basic information about a relation between two pages. + /// + [DebuggerDisplay("{Type}: {SourceId} -> {DestinationId} ({Duration})")] + public class RelationExcerpt + { + public Guid Id { get; set; } + + public Guid SourceId { get; set; } + public Guid DestinationId { get; set; } + public Guid? EventId { get; set; } + public RelationType Type { get; set; } + public FuzzyRange? Duration { get; set; } + public bool IsComplementary { get; set; } + } + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Relations/RelationContextOptions.cs b/src/Bonsai/Code/DomainModel/Relations/RelationContextOptions.cs index eeaafcc3..a116d5dd 100644 --- a/src/Bonsai/Code/DomainModel/Relations/RelationContextOptions.cs +++ b/src/Bonsai/Code/DomainModel/Relations/RelationContextOptions.cs @@ -1,23 +1,22 @@ -namespace Bonsai.Code.DomainModel.Relations +namespace Bonsai.Code.DomainModel.Relations; + +/// +/// Options for loading the RelationContext. +/// +public class RelationContextOptions { /// - /// Options for loading the RelationContext. + /// Loads only pages of type "Person" and their relations. /// - public class RelationContextOptions - { - /// - /// Loads only pages of type "Person" and their relations. - /// - public bool PeopleOnly { get; set; } + public bool PeopleOnly { get; init; } - /// - /// Omits loading relations. - /// - public bool PagesOnly { get; set; } + /// + /// Omits loading relations. + /// + public bool PagesOnly { get; init; } - /// - /// Omits loading relations except for "Parent", "Child" and "Spouse" which are required for the tree. - /// - public bool TreeRelationsOnly { get; set; } - } -} + /// + /// Omits loading relations except for "Parent", "Child" and "Spouse" which are required for the tree. + /// + public bool TreeRelationsOnly { get; init; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/DomainModel/Relations/RelationHelper.cs b/src/Bonsai/Code/DomainModel/Relations/RelationHelper.cs index 353d9d98..9c2d8d9b 100644 --- a/src/Bonsai/Code/DomainModel/Relations/RelationHelper.cs +++ b/src/Bonsai/Code/DomainModel/Relations/RelationHelper.cs @@ -2,114 +2,112 @@ using System.Linq; using Bonsai.Data.Models; -namespace Bonsai.Code.DomainModel.Relations +namespace Bonsai.Code.DomainModel.Relations; + +/// +/// A collection of helpful utilities for working with relations. +/// +public static class RelationHelper { /// - /// A collection of helpful utilities for working with relations. + /// Complementary relation bindings. /// - public static class RelationHelper + public static readonly IReadOnlyDictionary ComplementaryRelations = new Dictionary { - /// - /// Complementary relation bindings. - /// - public static readonly IReadOnlyDictionary ComplementaryRelations = new Dictionary - { - [RelationType.Parent] = RelationType.Child, - [RelationType.Child] = RelationType.Parent, - [RelationType.StepParent] = RelationType.StepChild, - [RelationType.StepChild] = RelationType.StepParent, + [RelationType.Parent] = RelationType.Child, + [RelationType.Child] = RelationType.Parent, + [RelationType.StepParent] = RelationType.StepChild, + [RelationType.StepChild] = RelationType.StepParent, - [RelationType.Pet] = RelationType.Owner, - [RelationType.Owner] = RelationType.Pet, + [RelationType.Pet] = RelationType.Owner, + [RelationType.Owner] = RelationType.Pet, - [RelationType.Spouse] = RelationType.Spouse, - [RelationType.Friend] = RelationType.Friend, - [RelationType.Colleague] = RelationType.Colleague, + [RelationType.Spouse] = RelationType.Spouse, + [RelationType.Friend] = RelationType.Friend, + [RelationType.Colleague] = RelationType.Colleague, - [RelationType.Location] = RelationType.LocationInhabitant, - [RelationType.LocationInhabitant] = RelationType.Location, - [RelationType.Event] = RelationType.EventVisitor, - [RelationType.EventVisitor] = RelationType.Event, + [RelationType.Location] = RelationType.LocationInhabitant, + [RelationType.LocationInhabitant] = RelationType.Location, + [RelationType.Event] = RelationType.EventVisitor, + [RelationType.EventVisitor] = RelationType.Event, - [RelationType.Other] = RelationType.Other - }; + [RelationType.Other] = RelationType.Other + }; - /// - /// List of allowed relations. - /// - public static readonly RelationBinding[] RelationBindingsMap = - { - new RelationBinding(PageType.Person, PageType.Person, new[] - { - RelationType.Parent, - RelationType.Child, - RelationType.Spouse, - RelationType.Friend, - RelationType.Colleague, - RelationType.StepParent, - RelationType.StepChild - }), - new RelationBinding(PageType.Person, PageType.Pet, new[] {RelationType.Pet}), - new RelationBinding(PageType.Pet, PageType.Person, new[] {RelationType.Owner}), - new RelationBinding(PageType.Person, PageType.Location, new[] {RelationType.Location}), - new RelationBinding(PageType.Location, PageType.Person, new[] {RelationType.LocationInhabitant}), - new RelationBinding(PageType.Person, PageType.Event, new[] {RelationType.Event}), - new RelationBinding(PageType.Event, PageType.Person, new[] {RelationType.EventVisitor}) - }; + /// + /// List of allowed relations. + /// + public static readonly RelationBinding[] RelationBindingsMap = + [ + new RelationBinding(PageType.Person, PageType.Person, [ + RelationType.Parent, + RelationType.Child, + RelationType.Spouse, + RelationType.Friend, + RelationType.Colleague, + RelationType.StepParent, + RelationType.StepChild + ]), + new RelationBinding(PageType.Person, PageType.Pet, [RelationType.Pet]), + new RelationBinding(PageType.Pet, PageType.Person, [RelationType.Owner]), + new RelationBinding(PageType.Person, PageType.Location, [RelationType.Location]), + new RelationBinding(PageType.Location, PageType.Person, [RelationType.LocationInhabitant]), + new RelationBinding(PageType.Person, PageType.Event, [RelationType.Event]), + new RelationBinding(PageType.Event, PageType.Person, [RelationType.EventVisitor]) + ]; - /// - /// Returns possible source types for a relation. - /// - public static IReadOnlyList SuggestSourcePageTypes(RelationType relType) - { - return RelationBindingsMap.Where(x => x.RelationTypes.Contains(relType)) - .Select(x => x.SourceType) - .Distinct() - .ToList(); - } + /// + /// Returns possible source types for a relation. + /// + public static IReadOnlyList SuggestSourcePageTypes(RelationType relType) + { + return RelationBindingsMap.Where(x => x.RelationTypes.Contains(relType)) + .Select(x => x.SourceType) + .Distinct() + .ToList(); + } - /// - /// Returns possible target types for a relation. - /// - public static IReadOnlyList SuggestDestinationPageTypes(RelationType relType) - { - return RelationBindingsMap.Where(x => x.RelationTypes.Contains(relType)) - .Select(x => x.DestinationType) - .Distinct() - .ToList(); - } + /// + /// Returns possible target types for a relation. + /// + public static IReadOnlyList SuggestDestinationPageTypes(RelationType relType) + { + return RelationBindingsMap.Where(x => x.RelationTypes.Contains(relType)) + .Select(x => x.DestinationType) + .Distinct() + .ToList(); + } - /// - /// Checks if the relation is allowed. - /// - public static bool IsRelationAllowed(PageType source, PageType target, RelationType relation) - { - if (relation == RelationType.Other) - return source == PageType.Other || target == PageType.Other; + /// + /// Checks if the relation is allowed. + /// + public static bool IsRelationAllowed(PageType source, PageType target, RelationType relation) + { + if (relation == RelationType.Other) + return source == PageType.Other || target == PageType.Other; - return RelationBindingsMap.Any(x => x.SourceType == source - && x.DestinationType == target - && x.RelationTypes.Contains(relation)); - } + return RelationBindingsMap.Any(x => x.SourceType == source + && x.DestinationType == target + && x.RelationTypes.Contains(relation)); + } - /// - /// Checks if the relation can have an event reference. - /// - public static bool IsRelationEventReferenceAllowed(RelationType relation) - { - return relation == RelationType.Spouse; - } + /// + /// Checks if the relation can have an event reference. + /// + public static bool IsRelationEventReferenceAllowed(RelationType relation) + { + return relation == RelationType.Spouse; + } - /// - /// Checks if the relation can have a duration. - /// - public static bool IsRelationDurationAllowed(RelationType relation) - { - return relation == RelationType.Spouse - || relation == RelationType.Pet - || relation == RelationType.Owner - || relation == RelationType.StepParent - || relation == RelationType.StepChild; - } + /// + /// Checks if the relation can have a duration. + /// + public static bool IsRelationDurationAllowed(RelationType relation) + { + return relation is RelationType.Spouse + or RelationType.Pet + or RelationType.Owner + or RelationType.StepParent + or RelationType.StepChild; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Infrastructure/AppControllerBase.cs b/src/Bonsai/Code/Infrastructure/AppControllerBase.cs index 33dd0914..756c40c8 100644 --- a/src/Bonsai/Code/Infrastructure/AppControllerBase.cs +++ b/src/Bonsai/Code/Infrastructure/AppControllerBase.cs @@ -4,40 +4,39 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace Bonsai.Code.Infrastructure +namespace Bonsai.Code.Infrastructure; + +/// +/// Base class for all Bonsai-app controllers. +/// +public class AppControllerBase: Controller { /// - /// Base class for all Bonsai-app controllers. + /// Returns the current session. + /// + protected ISession Session => HttpContext.Session; + + /// + /// Sets the state for the model from a validation exception. /// - public class AppControllerBase: Controller + protected void SetModelState(ValidationException ex) { - /// - /// Returns the current session. - /// - protected ISession Session => HttpContext.Session; + foreach(var error in ex.Errors) + ModelState.AddModelError(error.Key, error.Value); + } - /// - /// Sets the state for the model from a validation exception. - /// - protected void SetModelState(ValidationException ex) + /// + /// Handles OperationExceptions. + /// + public override void OnActionExecuted(ActionExecutedContext context) + { + if (context.Exception is KeyNotFoundException) { - foreach(var error in ex.Errors) - ModelState.AddModelError(error.Key, error.Value); + context.Result = NotFound(); + context.ExceptionHandled = true; + return; } - /// - /// Handles OperationExceptions. - /// - public override void OnActionExecuted(ActionExecutedContext context) - { - if (context.Exception is KeyNotFoundException) - { - context.Result = NotFound(); - context.ExceptionHandled = true; - return; - } - - base.OnActionExecuted(context); - } + base.OnActionExecuted(context); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitAttribute.cs b/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitAttribute.cs index aea138b6..be8a4c8c 100644 --- a/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitAttribute.cs +++ b/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitAttribute.cs @@ -2,17 +2,16 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Infrastructure.Attributes +namespace Bonsai.Code.Infrastructure.Attributes; + +/// +/// Sets the upload size limit from the static configuration. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class ConfigurableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter { - /// - /// Sets the upload size limit from the static configuration. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class ConfigurableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter - { - public int Order { get; } = 900; - public bool IsReusable => true; + public int Order { get; } = 900; + public bool IsReusable => true; - public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService(); - } -} + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitFilter.cs b/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitFilter.cs index b0618f1a..8a4c31b3 100644 --- a/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitFilter.cs +++ b/src/Bonsai/Code/Infrastructure/Attributes/ConfigurableRequestSizeLimitFilter.cs @@ -4,35 +4,34 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace Bonsai.Code.Infrastructure.Attributes +namespace Bonsai.Code.Infrastructure.Attributes; + +/// +/// A filter that sets to a value from the config. +/// +internal class ConfigurableRequestSizeLimitFilter : IAuthorizationFilter, IRequestSizePolicy { - /// - /// A filter that sets to a value from the config. - /// - internal class ConfigurableRequestSizeLimitFilter : IAuthorizationFilter, IRequestSizePolicy + public ConfigurableRequestSizeLimitFilter(BonsaiConfigService cfg) { - public ConfigurableRequestSizeLimitFilter(BonsaiConfigService cfg) - { - _cfg = cfg; - } + _cfg = cfg; + } - private readonly BonsaiConfigService _cfg; + private readonly BonsaiConfigService _cfg; - public void OnAuthorization(AuthorizationFilterContext context) - { - if (context == null) - throw new ArgumentNullException(nameof(context)); + public void OnAuthorization(AuthorizationFilterContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); - var effectivePolicy = context.FindEffectivePolicy(); - if (effectivePolicy != null && effectivePolicy != this) - return; + var effectivePolicy = context.FindEffectivePolicy(); + if (effectivePolicy != null && effectivePolicy != this) + return; - var maxRequestBodySizeFeature = context.HttpContext.Features.Get(); + var maxRequestBodySizeFeature = context.HttpContext.Features.Get(); - if (maxRequestBodySizeFeature?.IsReadOnly != false) - return; + if (maxRequestBodySizeFeature?.IsReadOnly != false) + return; - maxRequestBodySizeFeature.MaxRequestBodySize = _cfg.GetStaticConfig().WebServer.MaxUploadSize; - } + maxRequestBodySizeFeature.MaxRequestBodySize = _cfg.GetStaticConfig().WebServer.MaxUploadSize; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Infrastructure/IMapped.cs b/src/Bonsai/Code/Infrastructure/IMapped.cs index db4deffe..18961562 100644 --- a/src/Bonsai/Code/Infrastructure/IMapped.cs +++ b/src/Bonsai/Code/Infrastructure/IMapped.cs @@ -1,12 +1,11 @@ using Mapster; -namespace Bonsai.Code.Infrastructure +namespace Bonsai.Code.Infrastructure; + +/// +/// Configures automapper to map between types. +/// +internal interface IMapped { - /// - /// Configures automapper to map between types. - /// - internal interface IMapped - { - void Configure(TypeAdapterConfig config); - } -} + void Configure(TypeAdapterConfig config); +} \ No newline at end of file diff --git a/src/Bonsai/Code/Infrastructure/RuntimeHelper.cs b/src/Bonsai/Code/Infrastructure/RuntimeHelper.cs index 07beb96a..1ee74dfe 100644 --- a/src/Bonsai/Code/Infrastructure/RuntimeHelper.cs +++ b/src/Bonsai/Code/Infrastructure/RuntimeHelper.cs @@ -3,95 +3,76 @@ using System.Linq; using System.Reflection; -namespace Bonsai.Code.Infrastructure +namespace Bonsai.Code.Infrastructure; + +/// +/// Helper utilities for configuring the runtime. +/// +public static class RuntimeHelper { - /// - /// Helper utilities for configuring the runtime. - /// - public static class RuntimeHelper + static RuntimeHelper() { - static RuntimeHelper() - { - var rootAsm = Assembly.GetEntryAssembly(); - var namePrefix = rootAsm.FullName.Split(new[] { ", " }, StringSplitOptions.None)[0]; - ForceLoadReferences(rootAsm, namePrefix); - OwnAssemblies = AppDomain.CurrentDomain - .GetAssemblies() - .Where(x => x.FullName?.StartsWith(namePrefix) == true).ToList(); - } - - /// - /// List of all assemblies defined in the current project. - /// - public static readonly IReadOnlyList OwnAssemblies; + var rootAsm = Assembly.GetEntryAssembly(); + var namePrefix = rootAsm.FullName.Split([", "], StringSplitOptions.None)[0]; + ForceLoadReferences(rootAsm, namePrefix); + OwnAssemblies = AppDomain.CurrentDomain + .GetAssemblies() + .Where(x => x.FullName?.StartsWith(namePrefix) == true).ToList(); + } - /// - /// List of types defined in own this project. - /// - public static IEnumerable OwnTypes => OwnAssemblies.SelectMany(x => x.GetTypes()); + /// + /// List of all assemblies defined in the current project. + /// + public static readonly IReadOnlyList OwnAssemblies; - /// - /// Instantiates and returns instances of all matching types in all own assemblies. - /// - public static IEnumerable GetAllInstances() - { - var targetType = typeof(T); + /// + /// List of types defined in own this project. + /// + public static IEnumerable OwnTypes => OwnAssemblies.SelectMany(x => x.GetTypes()); - foreach (var type in OwnTypes) - if (type.IsConcrete() && targetType.IsAssignableFrom(type)) - yield return (T)Activator.CreateInstance(type); - } + /// + /// Instantiates and returns instances of all matching types in all own assemblies. + /// + public static IEnumerable GetAllInstances() + { + var targetType = typeof(T); - /// - /// Finds a type by full name. - /// - public static Type FindOwnType(string name) - { - return OwnTypes.FirstOrDefault(x => x.FullName == name) - ?? throw new Exception($"Type '{name}' was not found."); - } + foreach (var type in OwnTypes) + if (type.IsConcrete() && targetType.IsAssignableFrom(type)) + yield return (T)Activator.CreateInstance(type); + } - /// - /// Returns true if the type is an implementation of the specified generic. - /// - public static bool ImplementsGeneric(this Type type, Type other) - { - return type == other - || (type.IsGenericType && type.GetGenericTypeDefinition() == other); - } + /// + /// Checks if the type can be instantiated via CreateInstance. + /// + public static bool IsConcrete(this Type type) + { + return !type.IsAbstract + && !type.IsInterface + && !type.IsGenericTypeDefinition; + } - /// - /// Checks if the type can be instantiated via CreateInstance. - /// - public static bool IsConcrete(this Type type) - { - return !type.IsAbstract - && !type.IsInterface - && !type.IsGenericTypeDefinition; - } + /// + /// Ensures that all recursive references are loaded. + /// + private static void ForceLoadReferences(Assembly asm, string namePrefix) + { + var loaded = AppDomain.CurrentDomain.GetAssemblies().ToDictionary(x => x.FullName, x => x); + LoadRecursively(asm); - /// - /// Ensures that all recursive references are loaded. - /// - private static void ForceLoadReferences(Assembly asm, string namePrefix) + void LoadRecursively(Assembly currAsm) { - var loaded = AppDomain.CurrentDomain.GetAssemblies().ToDictionary(x => x.FullName, x => x); - LoadRecursively(asm); - - void LoadRecursively(Assembly currAsm) + var refs = currAsm.GetReferencedAssemblies(); + foreach (var r in refs) { - var refs = currAsm.GetReferencedAssemblies(); - foreach (var r in refs) - { - if (r.FullName?.StartsWith(namePrefix) != true) - continue; + if (r.FullName?.StartsWith(namePrefix) != true) + continue; - if (!loaded.ContainsKey(r.FullName)) - loaded[r.FullName] = Assembly.Load(r); + if (!loaded.ContainsKey(r.FullName)) + loaded[r.FullName] = Assembly.Load(r); - LoadRecursively(loaded[r.FullName]); - } + LoadRecursively(loaded[r.FullName]); } } } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/CacheService.cs b/src/Bonsai/Code/Services/CacheService.cs index aa4881fc..eb47cbd6 100644 --- a/src/Bonsai/Code/Services/CacheService.cs +++ b/src/Bonsai/Code/Services/CacheService.cs @@ -6,105 +6,104 @@ using Bonsai.Code.Utils; using Newtonsoft.Json; -namespace Bonsai.Code.Services +namespace Bonsai.Code.Services; + +/// +/// In-memory page cache service. +/// +public class CacheService: IDisposable { - /// - /// In-memory page cache service. - /// - public class CacheService: IDisposable - { - #region Constructor + #region Constructor - static CacheService() + static CacheService() + { + _locks = new Locker<(Type type, string id)>(); + _cache = new ConcurrentDictionary<(Type type, string id), string>(); + _jsonSettings = new JsonSerializerSettings { - _locks = new Locker<(Type type, string id)>(); - _cache = new ConcurrentDictionary<(Type type, string id), string>(); - _jsonSettings = new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.Auto - }; - } + TypeNameHandling = TypeNameHandling.Auto + }; + } - public CacheService() - { - _cts = new CancellationTokenSource(); - } + public CacheService() + { + _cts = new CancellationTokenSource(); + } - #endregion + #endregion - #region Fields + #region Fields - private CancellationTokenSource _cts; + private CancellationTokenSource _cts; - private static readonly Locker<(Type type, string id)> _locks; - private static readonly ConcurrentDictionary<(Type type, string id), string> _cache; - private static readonly JsonSerializerSettings _jsonSettings; + private static readonly Locker<(Type type, string id)> _locks; + private static readonly ConcurrentDictionary<(Type type, string id), string> _cache; + private static readonly JsonSerializerSettings _jsonSettings; - #endregion + #endregion - #region Public methods + #region Public methods - /// - /// Adds the page's contents to the cache. - /// - public async Task GetOrAddAsync(string id, Func> getter) + /// + /// Adds the page's contents to the cache. + /// + public async Task GetOrAddAsync(string id, Func> getter) + { + var key = (typeof(T), id); + await _locks.WaitAsync(key, _cts.Token); + try { - var key = (typeof(T), id); - await _locks.WaitAsync(key, _cts.Token); - try - { - if (_cache.ContainsKey(key)) - return JsonConvert.DeserializeObject(_cache[key], _jsonSettings); - - var result = await getter(); - _cache.TryAdd(key, JsonConvert.SerializeObject(result, _jsonSettings)); - - return result; - } - finally - { - _locks.Release(key); - } - } + if (_cache.ContainsKey(key)) + return JsonConvert.DeserializeObject(_cache[key], _jsonSettings); - /// - /// Removes all entries of the specified type from the caching service. - /// - public void Remove() - { - // todo: a better approach? - var type = typeof(T); - var stales = _cache.Keys.Where(x => x.type == type); + var result = await getter(); + _cache.TryAdd(key, JsonConvert.SerializeObject(result, _jsonSettings)); - foreach (var stale in stales) - _cache.TryRemove(stale, out _); + return result; } - - /// - /// Removes the entry from the caching service. - /// - public void Remove(string id) + finally { - _cache.TryRemove((typeof(T), id), out _); + _locks.Release(key); } + } - /// - /// Clears the entire cache. - /// - public void Clear() - { - _cache.Clear(); - } + /// + /// Removes all entries of the specified type from the caching service. + /// + public void Remove() + { + // todo: a better approach? + var type = typeof(T); + var stales = _cache.Keys.Where(x => x.type == type); - #endregion + foreach (var stale in stales) + _cache.TryRemove(stale, out _); + } - #region IDisposable implementation + /// + /// Removes the entry from the caching service. + /// + public void Remove(string id) + { + _cache.TryRemove((typeof(T), id), out _); + } - public void Dispose() - { - _cts.Cancel(); - } + /// + /// Clears the entire cache. + /// + public void Clear() + { + _cache.Clear(); + } - #endregion + #endregion + + #region IDisposable implementation + + public void Dispose() + { + _cts.Cancel(); } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Config/BonsaiConfigService.cs b/src/Bonsai/Code/Services/Config/BonsaiConfigService.cs index 60b998dd..57b0c747 100644 --- a/src/Bonsai/Code/Services/Config/BonsaiConfigService.cs +++ b/src/Bonsai/Code/Services/Config/BonsaiConfigService.cs @@ -3,77 +3,76 @@ using Bonsai.Data; using Newtonsoft.Json; -namespace Bonsai.Code.Services.Config +namespace Bonsai.Code.Services.Config; + +/// +/// Provides read-only configuration instance. +/// +public class BonsaiConfigService { - /// - /// Provides read-only configuration instance. - /// - public class BonsaiConfigService + static BonsaiConfigService() { - static BonsaiConfigService() - { - _config = new ConcurrentDictionary(); - } + _config = new ConcurrentDictionary(); + } - public BonsaiConfigService(AppDbContext context, StaticConfig cfg) - { - _context = context; + public BonsaiConfigService(AppDbContext context, StaticConfig cfg) + { + _context = context; - if(cfg.DemoMode == null) - cfg.DemoMode = new DemoModeConfig(); + if(cfg.DemoMode == null) + cfg.DemoMode = new DemoModeConfig(); - _cfg = cfg; - } + _cfg = cfg; + } - private readonly AppDbContext _context; - private readonly StaticConfig _cfg; - private static ConcurrentDictionary _config; + private readonly AppDbContext _context; + private readonly StaticConfig _cfg; + private static ConcurrentDictionary _config; - /// - /// Returns the configuration instance. - /// - public DynamicConfig GetDynamicConfig() - { - return _config.GetOrAdd("default", x => LoadDynamicConfig()); - } + /// + /// Returns the configuration instance. + /// + public DynamicConfig GetDynamicConfig() + { + return _config.GetOrAdd("default", x => LoadDynamicConfig()); + } - /// - /// Returns the configuration options from appsettings.json or env variables. - /// - public StaticConfig GetStaticConfig() - { - return _cfg; - } + /// + /// Returns the configuration options from appsettings.json or env variables. + /// + public StaticConfig GetStaticConfig() + { + return _cfg; + } - /// - /// Resets the currently loaded config. - /// - public void ResetCache() - { - _config.TryRemove("default", out _); - } + /// + /// Resets the currently loaded config. + /// + public void ResetCache() + { + _config.TryRemove("default", out _); + } - /// - /// Loads the configuration instance from the database. - /// - private DynamicConfig LoadDynamicConfig() - { - var cfg = JsonConvert.DeserializeObject( - _context.DynamicConfig.First().Value - ); + /// + /// Loads the configuration instance from the database. + /// + private DynamicConfig LoadDynamicConfig() + { + var cfg = JsonConvert.DeserializeObject( + _context.DynamicConfig.First().Value + ); - ApplyDefaults(cfg); + ApplyDefaults(cfg); - return cfg; - } + return cfg; + } - /// - /// Sets default values to properties (for backwards compatibility). - /// - private void ApplyDefaults(DynamicConfig cfg) - { - if (cfg.TreeRenderThoroughness == 0) - cfg.TreeRenderThoroughness = 50; - } + /// + /// Sets default values to properties (for backwards compatibility). + /// + private void ApplyDefaults(DynamicConfig cfg) + { + if (cfg.TreeRenderThoroughness == 0) + cfg.TreeRenderThoroughness = 50; } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Config/ConfigValidator.cs b/src/Bonsai/Code/Services/Config/ConfigValidator.cs index 23cfcf47..56b462bb 100644 --- a/src/Bonsai/Code/Services/Config/ConfigValidator.cs +++ b/src/Bonsai/Code/Services/Config/ConfigValidator.cs @@ -2,91 +2,90 @@ using Bonsai.Code.Utils.Validation; using Impworks.Utils.Linq; -namespace Bonsai.Code.Services.Config +namespace Bonsai.Code.Services.Config; + +/// +/// Helper class for ensuring the configuration is valid. +/// +public static class ConfigValidator { /// - /// Helper class for ensuring the configuration is valid. + /// Checks that the config has everything set correctly and throws an exception otherwise. /// - public static class ConfigValidator + public static void EnsureValid(StaticConfig config) { - /// - /// Checks that the config has everything set correctly and throws an exception otherwise. - /// - public static void EnsureValid(StaticConfig config) + var validator = new Validator(); + + if (config.Auth is { } auth) { - var validator = new Validator(); + if (auth.AllowPasswordAuth == false && auth.Facebook == null && auth.Google == null && auth.Vkontakte == null && auth.Yandex == null) + validator.Add(nameof(StaticConfig.Auth), "All authorization options are disabled. Please allow at lease one (e.g. AllowPasswordAuth = true)"); - if (config.Auth is { } auth) + if (auth.Facebook is { } fb) { - if (auth.AllowPasswordAuth == false && auth.Facebook == null && auth.Google == null && auth.Vkontakte == null && auth.Yandex == null) - validator.Add(nameof(StaticConfig.Auth), "All authorization options are disabled. Please allow at lease one (e.g. AllowPasswordAuth = true)"); - - if (auth.Facebook is { } fb) - { - if(string.IsNullOrWhiteSpace(fb.AppId)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(fb.AppId), "Application ID is required for Facebook auth."); + if(string.IsNullOrWhiteSpace(fb.AppId)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(fb.AppId), "Application ID is required for Facebook auth."); - if (string.IsNullOrWhiteSpace(fb.AppSecret)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(fb.AppSecret), "Application secret is required for Facebook auth."); - } - - if (auth.Google is { } google) - { - if (string.IsNullOrWhiteSpace(google.ClientId)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(google.ClientId), "Client ID is required for Google auth."); - - if (string.IsNullOrWhiteSpace(google.ClientSecret)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(google.ClientSecret), "Client secret is required for Google auth."); - } + if (string.IsNullOrWhiteSpace(fb.AppSecret)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(fb.AppSecret), "Application secret is required for Facebook auth."); + } - if (auth.Vkontakte is { } vk) - { - if (string.IsNullOrWhiteSpace(vk.ClientId)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(vk.ClientId), "Client ID is required for Vkontakte auth."); + if (auth.Google is { } google) + { + if (string.IsNullOrWhiteSpace(google.ClientId)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(google.ClientId), "Client ID is required for Google auth."); - if (string.IsNullOrWhiteSpace(vk.ClientSecret)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(vk.ClientSecret), "Client secret is required for Vkontakte auth."); - } + if (string.IsNullOrWhiteSpace(google.ClientSecret)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(google.ClientSecret), "Client secret is required for Google auth."); + } - if (auth.Yandex is { } ya) - { - if (string.IsNullOrWhiteSpace(ya.ClientId)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(ya.ClientId), "Client ID is required for Yandex auth."); + if (auth.Vkontakte is { } vk) + { + if (string.IsNullOrWhiteSpace(vk.ClientId)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(vk.ClientId), "Client ID is required for Vkontakte auth."); - if (string.IsNullOrWhiteSpace(ya.ClientSecret)) - validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(ya.ClientSecret), "Client secret is required for Yandex auth."); - } + if (string.IsNullOrWhiteSpace(vk.ClientSecret)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(vk.ClientSecret), "Client secret is required for Vkontakte auth."); } - else + + if (auth.Yandex is { } ya) { - validator.Add(nameof(StaticConfig.Auth), "All authorization options are disabled. Please allow at lease one (e.g. AllowPasswordAuth = true)"); + if (string.IsNullOrWhiteSpace(ya.ClientId)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(ya.ClientId), "Client ID is required for Yandex auth."); + + if (string.IsNullOrWhiteSpace(ya.ClientSecret)) + validator.Add(nameof(StaticConfig.Auth) + "__" + nameof(ya.ClientSecret), "Client secret is required for Yandex auth."); } + } + else + { + validator.Add(nameof(StaticConfig.Auth), "All authorization options are disabled. Please allow at lease one (e.g. AllowPasswordAuth = true)"); + } - if (config.ConnectionStrings is { } conns) + if (config.ConnectionStrings is { } conns) + { + if (conns.UseEmbeddedDatabase) { - if (conns.UseEmbeddedDatabase) - { - if(string.IsNullOrWhiteSpace(conns.EmbeddedDatabase)) - validator.Add(nameof(StaticConfig.ConnectionStrings) + "__" + nameof(conns.EmbeddedDatabase), "Embedded database connection string is missing."); - } - else - { - if (string.IsNullOrWhiteSpace(conns.Database)) - validator.Add(nameof(StaticConfig.ConnectionStrings) + "__" + nameof(conns.Database), "Database connection string is missing."); - } + if(string.IsNullOrWhiteSpace(conns.EmbeddedDatabase)) + validator.Add(nameof(StaticConfig.ConnectionStrings) + "__" + nameof(conns.EmbeddedDatabase), "Embedded database connection string is missing."); } else { - validator.Add(nameof(StaticConfig.ConnectionStrings), "Database connection strings configuration is missing. The 'ConnectionStrings__UseEmbeddedDatabase' flag and either 'ConnectionStrings__EmbeddedDatabase' or 'ConnectionStrings__Database' are required."); - } - - if (config.Locale is { } loc) - { - if(!LocaleProvider.IsSupported(loc)) - validator.Add(nameof(StaticConfig.Locale), $"Locale {loc} is not supported."); + if (string.IsNullOrWhiteSpace(conns.Database)) + validator.Add(nameof(StaticConfig.ConnectionStrings) + "__" + nameof(conns.Database), "Database connection string is missing."); } + } + else + { + validator.Add(nameof(StaticConfig.ConnectionStrings), "Database connection strings configuration is missing. The 'ConnectionStrings__UseEmbeddedDatabase' flag and either 'ConnectionStrings__EmbeddedDatabase' or 'ConnectionStrings__Database' are required."); + } - validator.ThrowIfInvalid("Bonsai configuration is invalid!"); + if (config.Locale is { } loc) + { + if(!LocaleProvider.IsSupported(loc)) + validator.Add(nameof(StaticConfig.Locale), $"Locale {loc} is not supported."); } + + validator.ThrowIfInvalid("Bonsai configuration is invalid!"); } -} +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Config/DynamicConfig.cs b/src/Bonsai/Code/Services/Config/DynamicConfig.cs index f4851b1c..87c7c57a 100644 --- a/src/Bonsai/Code/Services/Config/DynamicConfig.cs +++ b/src/Bonsai/Code/Services/Config/DynamicConfig.cs @@ -1,43 +1,42 @@ using System.ComponentModel.DataAnnotations; using Bonsai.Data.Models; -namespace Bonsai.Code.Services.Config +namespace Bonsai.Code.Services.Config; + +/// +/// General application configuration. +/// +public class DynamicConfig { /// - /// General application configuration. + /// The title of the website. Displayed in the top bar and browser title. /// - public class DynamicConfig - { - /// - /// The title of the website. Displayed in the top bar and browser title. - /// - [Required] - [StringLength(200)] - public string Title { get; set; } + [Required] + [StringLength(200)] + public string Title { get; set; } - /// - /// Flag indicating that the website allows unauthorized visitors to view the contents. - /// - public bool AllowGuests { get; set; } + /// + /// Flag indicating that the website allows unauthorized visitors to view the contents. + /// + public bool AllowGuests { get; set; } - /// - /// Flag indicating that new registrations are accepted. - /// - public bool AllowRegistration { get; set; } + /// + /// Flag indicating that new registrations are accepted. + /// + public bool AllowRegistration { get; set; } - /// - /// Flag indicating that the black ribbon should not be displayed for deceased relatives in tree view. - /// - public bool HideBlackRibbon { get; set; } + /// + /// Flag indicating that the black ribbon should not be displayed for deceased relatives in tree view. + /// + public bool HideBlackRibbon { get; set; } - /// - /// Tree render thoroughness coefficient. - /// - public int TreeRenderThoroughness { get; set; } + /// + /// Tree render thoroughness coefficient. + /// + public int TreeRenderThoroughness { get; set; } - /// - /// Allowed kinds of trees. - /// - public TreeKind TreeKinds { get; set; } - } -} + /// + /// Allowed kinds of trees. + /// + public TreeKind TreeKinds { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Config/StaticConfig.cs b/src/Bonsai/Code/Services/Config/StaticConfig.cs index 89f3f4f9..ce0585b3 100644 --- a/src/Bonsai/Code/Services/Config/StaticConfig.cs +++ b/src/Bonsai/Code/Services/Config/StaticConfig.cs @@ -1,85 +1,84 @@ -namespace Bonsai.Code.Services.Config -{ - /// - /// Global configuration properties defined in appsettings.json. - /// - public class StaticConfig - { - public ConnectionStringsConfig ConnectionStrings { get; set; } - public DebugConfig Debug { get; set; } - public WebServerConfig WebServer { get; set; } - public DemoModeConfig DemoMode { get; set; } - public AuthConfig Auth { get; set; } - public string Locale { get; set; } - public string BuildCommit { get; set; } - } +namespace Bonsai.Code.Services.Config; - /// - /// Connection string properties. - /// - public class ConnectionStringsConfig - { - public string Database { get; set; } - public string EmbeddedDatabase { get; set; } - public bool UseEmbeddedDatabase { get; set; } - } +/// +/// Global configuration properties defined in appsettings.json. +/// +public class StaticConfig +{ + public ConnectionStringsConfig ConnectionStrings { get; set; } + public DebugConfig Debug { get; set; } + public WebServerConfig WebServer { get; set; } + public DemoModeConfig DemoMode { get; set; } + public AuthConfig Auth { get; set; } + public string Locale { get; set; } + public string BuildCommit { get; set; } +} - /// - /// Properties related to debugging. - /// - public class DebugConfig - { - public bool DetailedExceptions { get; set; } - } +/// +/// Connection string properties. +/// +public class ConnectionStringsConfig +{ + public string Database { get; set; } + public string EmbeddedDatabase { get; set; } + public bool UseEmbeddedDatabase { get; set; } +} - /// - /// Webserver properties. - /// - public class WebServerConfig - { - public bool RequireHttps { get; set; } - public long? MaxUploadSize { get; set; } - } +/// +/// Properties related to debugging. +/// +public class DebugConfig +{ + public bool DetailedExceptions { get; set; } +} - /// - /// Demo mode configuration options. - /// - public class DemoModeConfig - { - public bool Enabled { get; set; } - public bool CreateDefaultPages { get; set; } - public bool CreateDefaultAdmin { get; set; } - public bool ClearOnStartup { get; set; } - public string YandexMetrikaId { get; set; } - } +/// +/// Webserver properties. +/// +public class WebServerConfig +{ + public bool RequireHttps { get; set; } + public long? MaxUploadSize { get; set; } +} - /// - /// Authorization options properties. - /// - public class AuthConfig - { - public bool AllowPasswordAuth { get; set; } - public FacebookAuthConfig Facebook { get; set; } - public GenericAuthConfig Google { get; set; } - public GenericAuthConfig Vkontakte { get; set; } - public GenericAuthConfig Yandex { get; set; } - } +/// +/// Demo mode configuration options. +/// +public class DemoModeConfig +{ + public bool Enabled { get; set; } + public bool CreateDefaultPages { get; set; } + public bool CreateDefaultAdmin { get; set; } + public bool ClearOnStartup { get; set; } + public string YandexMetrikaId { get; set; } +} - /// - /// Facebook-related authorization properties. - /// - public class FacebookAuthConfig - { - public string AppId { get; set; } - public string AppSecret { get; set; } - } +/// +/// Authorization options properties. +/// +public class AuthConfig +{ + public bool AllowPasswordAuth { get; set; } + public FacebookAuthConfig Facebook { get; set; } + public GenericAuthConfig Google { get; set; } + public GenericAuthConfig Vkontakte { get; set; } + public GenericAuthConfig Yandex { get; set; } +} - /// - /// Authorization properties for all other providers. - /// - public class GenericAuthConfig - { - public string ClientId { get; set; } - public string ClientSecret { get; set; } - } +/// +/// Facebook-related authorization properties. +/// +public class FacebookAuthConfig +{ + public string AppId { get; set; } + public string AppSecret { get; set; } } + +/// +/// Authorization properties for all other providers. +/// +public class GenericAuthConfig +{ + public string ClientId { get; set; } + public string ClientSecret { get; set; } +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/BackgroundJobService.cs b/src/Bonsai/Code/Services/Jobs/BackgroundJobService.cs index e13f530d..11639c5d 100644 --- a/src/Bonsai/Code/Services/Jobs/BackgroundJobService.cs +++ b/src/Bonsai/Code/Services/Jobs/BackgroundJobService.cs @@ -14,207 +14,206 @@ using Newtonsoft.Json; using Serilog; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +/// +/// Background job runner and manager. +/// +public class BackgroundJobService: IHostedService, IBackgroundJobService { - /// - /// Background job runner and manager. - /// - public class BackgroundJobService: IHostedService, IBackgroundJobService + public BackgroundJobService(IServiceScopeFactory scopeFactory, StartupService startup, ILogger logger) { - public BackgroundJobService(IServiceScopeFactory scopeFactory, StartupService startup, ILogger logger) - { - _scopeFactory = scopeFactory; - _startup = startup; - _logger = logger; + _scopeFactory = scopeFactory; + _startup = startup; + _logger = logger; - _jobs = new ConcurrentDictionary(); - } + _jobs = new ConcurrentDictionary(); + } - private readonly IServiceScopeFactory _scopeFactory; - private readonly StartupService _startup; - private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private readonly StartupService _startup; + private readonly ILogger _logger; - private readonly ConcurrentDictionary _jobs; + private readonly ConcurrentDictionary _jobs; - /// - /// Starts the job supervisor service. - /// - public async Task StartAsync(CancellationToken cancellationToken) - { - await _startup.WaitForStartup(); + /// + /// Starts the job supervisor service. + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + await _startup.WaitForStartup(); - foreach (var def in await LoadPendingJobsAsync()) - _ = ExecuteJobAsync(def); - } + foreach (var def in await LoadPendingJobsAsync()) + _ = ExecuteJobAsync(def); + } - /// - /// Stops the job supervisor service. - /// - public Task StopAsync(CancellationToken cancellationToken) - { - foreach (var job in _jobs.Values.ToList()) - job.Cancellation.Cancel(); + /// + /// Stops the job supervisor service. + /// + public Task StopAsync(CancellationToken cancellationToken) + { + foreach (var job in _jobs.Values.ToList()) + job.Cancellation.Cancel(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - #region Public interface + #region Public interface - /// - /// Runs a new job. - /// - public async Task RunAsync(JobBuilder jb) - { - var def = await CreateDescriptorAsync(jb); + /// + /// Runs a new job. + /// + public async Task RunAsync(JobBuilder jb) + { + var def = await CreateDescriptorAsync(jb); - if(jb.IsSuperseding) - Cancel(def.ResourceKey); + if(jb.IsSuperseding) + Cancel(def.ResourceKey); - _ = ExecuteJobAsync(def); // sic! fire and forget - } + _ = ExecuteJobAsync(def); // sic! fire and forget + } - /// - /// Terminates all jobs related to the specified key. - /// - public void Cancel(string key) - { - if(key == null) - throw new ArgumentNullException(nameof(key)); + /// + /// Terminates all jobs related to the specified key. + /// + public void Cancel(string key) + { + if(key == null) + throw new ArgumentNullException(nameof(key)); - var jobsToTerminate = _jobs.Values.Where(x => x.ResourceKey == key).ToList(); - foreach (var job in jobsToTerminate) - { - _logger.Information($"Cancelling job {job}."); - job.Cancellation.Cancel(); - } + var jobsToTerminate = _jobs.Values.Where(x => x.ResourceKey == key).ToList(); + foreach (var job in jobsToTerminate) + { + _logger.Information($"Cancelling job {job}."); + job.Cancellation.Cancel(); } + } - #endregion + #endregion - #region Helpers + #region Helpers - /// - /// Loads all incomplete jobs from the database. - /// - private async Task> LoadPendingJobsAsync() + /// + /// Loads all incomplete jobs from the database. + /// + private async Task> LoadPendingJobsAsync() + { + var scope = _scopeFactory.CreateScope(); + var di = scope.ServiceProvider; + + var db = di.GetRequiredService(); + var states = await db.JobStates + .AsNoTracking() + .Where(x => x.FinishDate == null) + .OrderBy(x => x.StartDate) + .ToListAsync(); + + var result = new List(); + foreach (var state in states) { - var scope = _scopeFactory.CreateScope(); - var di = scope.ServiceProvider; - - var db = di.GetRequiredService(); - var states = await db.JobStates - .AsNoTracking() - .Where(x => x.FinishDate == null) - .OrderBy(x => x.StartDate) - .ToListAsync(); - - var result = new List(); - foreach (var state in states) - { - var jscope = _scopeFactory.CreateScope(); - var jdi = jscope.ServiceProvider; - var jobType = Type.GetType(state.Type)!; - var job = (IJob) jdi.GetRequiredService(jobType); - var args = GetArguments(state); - - result.Add(new JobDescriptor - { - Scope = jscope, - Job = job, - Arguments = args, - ResourceKey = job.GetResourceKey(args), - Cancellation = new CancellationTokenSource(), - JobStateId = state.Id - }); - } - - return result; - - object GetArguments(JobState state) - { - return !string.IsNullOrEmpty(state.Arguments) - ? JsonConvert.DeserializeObject(state.Arguments, Type.GetType(state.ArgumentsType)!) - : null; - } - } + var jscope = _scopeFactory.CreateScope(); + var jdi = jscope.ServiceProvider; + var jobType = Type.GetType(state.Type)!; + var job = (IJob) jdi.GetRequiredService(jobType); + var args = GetArguments(state); - /// - /// Persists job information in the database. - /// - private async Task CreateDescriptorAsync(JobBuilder jb) - { - var scope = _scopeFactory.CreateScope(); - var di = scope.ServiceProvider; - - var state = new JobState - { - Id = Guid.NewGuid(), - StartDate = DateTime.Now, - Type = jb.Type.FullName, - Arguments = jb.Args != null ? JsonConvert.SerializeObject(jb.Args) : null, - ArgumentsType = jb.Args?.GetType().FullName, - }; - var db = di.GetRequiredService(); - db.JobStates.Add(state); - await db.SaveChangesAsync(); - - var job = (IJob) di.GetRequiredService(jb.Type); - return new JobDescriptor + result.Add(new JobDescriptor { + Scope = jscope, Job = job, - Arguments = jb.Args, - ResourceKey = job.GetResourceKey(jb.Args), + Arguments = args, + ResourceKey = job.GetResourceKey(args), Cancellation = new CancellationTokenSource(), - Scope = scope, JobStateId = state.Id - }; + }); } - /// - /// Runs the specified job. - /// - private async Task ExecuteJobAsync(JobDescriptor def) + return result; + + object GetArguments(JobState state) { - _jobs.TryAdd(def.JobStateId, def); - - var success = false; + return !string.IsNullOrEmpty(state.Arguments) + ? JsonConvert.DeserializeObject(state.Arguments, Type.GetType(state.ArgumentsType)!) + : null; + } + } - try - { - await def.Job.ExecuteAsync(def.Arguments, def.Cancellation.Token); - _logger.Information($"Job {def} has completed successfully."); - success = true; - } - catch (TaskCanceledException) - { - _logger.Information($"Job {def} has been cancelled."); - } - catch (Exception ex) - { - _logger.Error(ex.Demystify(), $"Job {def} has failed."); - } - finally - { - def.Scope.Dispose(); - } + /// + /// Persists job information in the database. + /// + private async Task CreateDescriptorAsync(JobBuilder jb) + { + var scope = _scopeFactory.CreateScope(); + var di = scope.ServiceProvider; + + var state = new JobState + { + Id = Guid.NewGuid(), + StartDate = DateTime.Now, + Type = jb.Type.FullName, + Arguments = jb.Args != null ? JsonConvert.SerializeObject(jb.Args) : null, + ArgumentsType = jb.Args?.GetType().FullName, + }; + var db = di.GetRequiredService(); + db.JobStates.Add(state); + await db.SaveChangesAsync(); + + var job = (IJob) di.GetRequiredService(jb.Type); + return new JobDescriptor + { + Job = job, + Arguments = jb.Args, + ResourceKey = job.GetResourceKey(jb.Args), + Cancellation = new CancellationTokenSource(), + Scope = scope, + JobStateId = state.Id + }; + } - try - { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var state = await db.JobStates.GetAsync(x => x.Id == def.JobStateId, $"Job {def.JobStateId} not found"); - state.Success = success; - state.FinishDate = DateTime.Now; - await db.SaveChangesAsync(CancellationToken.None); - } - catch (Exception ex) - { - _logger.Error(ex.Demystify(), $"Failed to save {def} state."); - } + /// + /// Runs the specified job. + /// + private async Task ExecuteJobAsync(JobDescriptor def) + { + _jobs.TryAdd(def.JobStateId, def); - _jobs.TryRemove(def.JobStateId, out _); + var success = false; + + try + { + await def.Job.ExecuteAsync(def.Arguments, def.Cancellation.Token); + _logger.Information($"Job {def} has completed successfully."); + success = true; + } + catch (TaskCanceledException) + { + _logger.Information($"Job {def} has been cancelled."); + } + catch (Exception ex) + { + _logger.Error(ex.Demystify(), $"Job {def} has failed."); + } + finally + { + def.Scope.Dispose(); } - #endregion + try + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var state = await db.JobStates.GetAsync(x => x.Id == def.JobStateId, $"Job {def.JobStateId} not found"); + state.Success = success; + state.FinishDate = DateTime.Now; + await db.SaveChangesAsync(CancellationToken.None); + } + catch (Exception ex) + { + _logger.Error(ex.Demystify(), $"Failed to save {def} state."); + } + + _jobs.TryRemove(def.JobStateId, out _); } + + #endregion } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/IBackgroundJobService.cs b/src/Bonsai/Code/Services/Jobs/IBackgroundJobService.cs index 5b8e6da4..7e0b628b 100644 --- a/src/Bonsai/Code/Services/Jobs/IBackgroundJobService.cs +++ b/src/Bonsai/Code/Services/Jobs/IBackgroundJobService.cs @@ -1,20 +1,19 @@ using System.Threading.Tasks; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +/// +/// Public interface for handling background tasks. +/// +public interface IBackgroundJobService { /// - /// Public interface for handling background tasks. + /// Runs a new persistent background task. /// - public interface IBackgroundJobService - { - /// - /// Runs a new persistent background task. - /// - Task RunAsync(JobBuilder jb); + Task RunAsync(JobBuilder jb); - /// - /// Terminates all background tasks acquire the resource (specified by key). - /// - void Cancel(string key); - } + /// + /// Terminates all background tasks acquire the resource (specified by key). + /// + void Cancel(string key); } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/IJob.cs b/src/Bonsai/Code/Services/Jobs/IJob.cs index fbb86597..18786c27 100644 --- a/src/Bonsai/Code/Services/Jobs/IJob.cs +++ b/src/Bonsai/Code/Services/Jobs/IJob.cs @@ -1,21 +1,20 @@ using System.Threading; using System.Threading.Tasks; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +/// +/// Interface for job implementations. +/// +public interface IJob { /// - /// Interface for job implementations. + /// Returns the resource key occupied by this job. /// - public interface IJob - { - /// - /// Returns the resource key occupied by this job. - /// - string GetResourceKey(object args); + string GetResourceKey(object args); - /// - /// Runs the job. - /// - Task ExecuteAsync(object args, CancellationToken token); - } + /// + /// Runs the job. + /// + Task ExecuteAsync(object args, CancellationToken token); } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/JobBase.cs b/src/Bonsai/Code/Services/Jobs/JobBase.cs index 1e71e1fa..2f57fe9a 100644 --- a/src/Bonsai/Code/Services/Jobs/JobBase.cs +++ b/src/Bonsai/Code/Services/Jobs/JobBase.cs @@ -1,29 +1,28 @@ using System.Threading; using System.Threading.Tasks; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +/// +/// Base implementation for strongly typed jobs with arguments. +/// +public abstract class JobBase: IJob { - /// - /// Base implementation for strongly typed jobs with arguments. - /// - public abstract class JobBase: IJob - { - public string GetResourceKey(object args) => GetResourceKey((TArgs) args); - public Task ExecuteAsync(object args, CancellationToken token) => ExecuteAsync((TArgs) args, token); + public string GetResourceKey(object args) => GetResourceKey((TArgs) args); + public Task ExecuteAsync(object args, CancellationToken token) => ExecuteAsync((TArgs) args, token); - protected virtual string GetResourceKey(TArgs args) => GetType().FullName; - protected abstract Task ExecuteAsync(TArgs args, CancellationToken token); - } + protected virtual string GetResourceKey(TArgs args) => GetType().FullName; + protected abstract Task ExecuteAsync(TArgs args, CancellationToken token); +} - /// - /// Base implementation for strongly typed jobs without arguments. - /// - public abstract class JobBase: IJob - { - public string GetResourceKey(object args) => GetResourceKey(); - public Task ExecuteAsync(object args, CancellationToken token) => ExecuteAsync(token); +/// +/// Base implementation for strongly typed jobs without arguments. +/// +public abstract class JobBase: IJob +{ + public string GetResourceKey(object args) => GetResourceKey(); + public Task ExecuteAsync(object args, CancellationToken token) => ExecuteAsync(token); - protected virtual string GetResourceKey() => GetType().FullName; - protected abstract Task ExecuteAsync(CancellationToken token); - } + protected virtual string GetResourceKey() => GetType().FullName; + protected abstract Task ExecuteAsync(CancellationToken token); } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/JobBuilder.cs b/src/Bonsai/Code/Services/Jobs/JobBuilder.cs index fa964445..322bc4b6 100644 --- a/src/Bonsai/Code/Services/Jobs/JobBuilder.cs +++ b/src/Bonsai/Code/Services/Jobs/JobBuilder.cs @@ -1,42 +1,41 @@ using System; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +/// +/// Details for starting a new job. +/// +public class JobBuilder { /// - /// Details for starting a new job. + /// Creates a new JobBuilder for specified job type. /// - public class JobBuilder - { - /// - /// Creates a new JobBuilder for specified job type. - /// - public static JobBuilder For() where TJob: IJob => new JobBuilder(typeof(TJob)); + public static JobBuilder For() where TJob: IJob => new JobBuilder(typeof(TJob)); - private JobBuilder(Type type) - { - Type = type; - } + private JobBuilder(Type type) + { + Type = type; + } - public Type Type { get; } - public object Args { get; private set; } - public bool IsSuperseding { get; private set; } + public Type Type { get; } + public object Args { get; private set; } + public bool IsSuperseding { get; private set; } - /// - /// Adds arguments to the job. - /// - public JobBuilder WithArgs(object args) - { - Args = args; - return this; - } + /// + /// Adds arguments to the job. + /// + public JobBuilder WithArgs(object args) + { + Args = args; + return this; + } - /// - /// Terminates all other jobs of this kind before running this one. - /// - public JobBuilder SupersedeAll() - { - IsSuperseding = true; - return this; - } + /// + /// Terminates all other jobs of this kind before running this one. + /// + public JobBuilder SupersedeAll() + { + IsSuperseding = true; + return this; } } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/Jobs/JobDescriptor.cs b/src/Bonsai/Code/Services/Jobs/JobDescriptor.cs index 3c80854f..96550c4a 100644 --- a/src/Bonsai/Code/Services/Jobs/JobDescriptor.cs +++ b/src/Bonsai/Code/Services/Jobs/JobDescriptor.cs @@ -2,29 +2,28 @@ using System.Threading; using Microsoft.Extensions.DependencyInjection; -namespace Bonsai.Code.Services.Jobs +namespace Bonsai.Code.Services.Jobs; + +public class JobDescriptor: IDisposable { - public class JobDescriptor: IDisposable - { - public IJob Job { get; set; } - public object Arguments { get; set; } + public IJob Job { get; set; } + public object Arguments { get; set; } - public string ResourceKey { get; set; } + public string ResourceKey { get; set; } - public CancellationTokenSource Cancellation { get; set; } - public Guid JobStateId { get; set; } + public CancellationTokenSource Cancellation { get; set; } + public Guid JobStateId { get; set; } - public IServiceScope Scope { get; set; } + public IServiceScope Scope { get; set; } - public void Dispose() - { - Scope?.Dispose(); - Scope = null; - } + public void Dispose() + { + Scope?.Dispose(); + Scope = null; + } - public override string ToString() - { - return $"{JobStateId} ({Job?.GetType().Name ?? "Unknown"})"; - } + public override string ToString() + { + return $"{JobStateId} ({Job?.GetType().Name ?? "Unknown"})"; } } \ No newline at end of file diff --git a/src/Bonsai/Code/Services/LocaleProvider.cs b/src/Bonsai/Code/Services/LocaleProvider.cs index 4e93ca66..985ee26c 100644 --- a/src/Bonsai/Code/Services/LocaleProvider.cs +++ b/src/Bonsai/Code/Services/LocaleProvider.cs @@ -6,147 +6,146 @@ using Impworks.Utils.Format; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Bonsai.Code.Services +namespace Bonsai.Code.Services; + +/// +/// Helper service for managing supported locales. +/// +public static class LocaleProvider { + private static readonly LocaleBase[] Locales = + [ + new LocaleRu(), + new LocaleEn() + ]; + + private static LocaleBase CurrentLocale; + /// - /// Helper service for managing supported locales. + /// Checks if the specified locale is supported. /// - public static class LocaleProvider - { - private static readonly LocaleBase[] Locales = - [ - new LocaleRu(), - new LocaleEn() - ]; - - private static LocaleBase CurrentLocale; - - /// - /// Checks if the specified locale is supported. - /// - public static bool IsSupported(string code) => Locales.Any(x => x.Code == code); - - /// - /// Sets the current locale. - /// - public static void SetLocale(string code) - { - CurrentLocale = Locales.FirstOrDefault(x => x.Code == code) ?? Locales[0]; - } + public static bool IsSupported(string code) => Locales.Any(x => x.Code == code); - /// - /// Returns the string code of the current locale (e.g. ru-RU). - /// - public static string GetLocaleCode() - { - if (CurrentLocale == null) - throw new Exception("Current locale is not set."); + /// + /// Sets the current locale. + /// + public static void SetLocale(string code) + { + CurrentLocale = Locales.FirstOrDefault(x => x.Code == code) ?? Locales[0]; + } - return CurrentLocale.Code; - } + /// + /// Returns the string code of the current locale (e.g. ru-RU). + /// + public static string GetLocaleCode() + { + if (CurrentLocale == null) + throw new Exception("Current locale is not set."); - /// - /// Returns the CultureInfo object for current locale. - /// - public static CultureInfo GetCulture() - { - if (CurrentLocale == null) - throw new Exception("Current locale is not set."); + return CurrentLocale.Code; + } - return CultureInfo.GetCultureInfo(CurrentLocale.Code); - } + /// + /// Returns the CultureInfo object for current locale. + /// + public static CultureInfo GetCulture() + { + if (CurrentLocale == null) + throw new Exception("Current locale is not set."); - /// - /// Returns the appropriate word form for the number (e.g. 1 - object, 2 - objects). - /// - public static string GetNumericWord(int num, string forms) - { - if (CurrentLocale == null) - throw new Exception("Current locale is not set."); + return CultureInfo.GetCultureInfo(CurrentLocale.Code); + } - var formsArray = forms.Split('|'); - try - { - return CurrentLocale.GetNumericWord(num, formsArray); - } - catch (IndexOutOfRangeException) - { - throw new FormatException($"Invalid pluralization form '{forms}' for locale {CurrentLocale.Code}!"); - } - } + /// + /// Returns the appropriate word form for the number (e.g. 1 - object, 2 - objects). + /// + public static string GetNumericWord(int num, string forms) + { + if (CurrentLocale == null) + throw new Exception("Current locale is not set."); - /// - /// Returns enum descriptions localized to current language. - /// - public static IReadOnlyDictionary GetLocaleEnumDescriptions() - where T: struct, Enum + var formsArray = forms.Split('|'); + try { - var values = EnumHelper.GetEnumValues(); - return values.ToDictionary(x => x, GetLocaleEnumDescription); + return CurrentLocale.GetNumericWord(num, formsArray); } - - /// - /// Returns a single enum description localized to current language. - /// - public static string GetLocaleEnumDescription(this T value) - where T : struct, Enum + catch (IndexOutOfRangeException) { - return GetLocaleEnumDescription(typeof(T), value.ToString()); + throw new FormatException($"Invalid pluralization form '{forms}' for locale {CurrentLocale.Code}!"); } + } - /// - /// Returns a single enum description localized to current language. - /// - public static string GetLocaleEnumDescription(Type type, string name) - { - var key = $"Enum_{type.Name}_{name}"; - return Texts.ResourceManager.GetString(key) ?? name; - } + /// + /// Returns enum descriptions localized to current language. + /// + public static IReadOnlyDictionary GetLocaleEnumDescriptions() + where T: struct, Enum + { + var values = EnumHelper.GetEnumValues(); + return values.ToDictionary(x => x, GetLocaleEnumDescription); + } - #region Locale implementations + /// + /// Returns a single enum description localized to current language. + /// + public static string GetLocaleEnumDescription(this T value) + where T : struct, Enum + { + return GetLocaleEnumDescription(typeof(T), value.ToString()); + } - /// - /// Base class for locale implementations. - /// - private abstract record LocaleBase(string Code) - { - public abstract string GetNumericWord(int number, string[] forms); - } + /// + /// Returns a single enum description localized to current language. + /// + public static string GetLocaleEnumDescription(Type type, string name) + { + var key = $"Enum_{type.Name}_{name}"; + return Texts.ResourceManager.GetString(key) ?? name; + } - /// - /// Russian locale. - /// - private record LocaleRu() : LocaleBase("ru-RU") - { - public override string GetNumericWord(int number, string[] forms) - { - var ones = number % 10; - var tens = (number / 10) % 10; + #region Locale implementations - if (tens != 1) - { - if (ones == 1) - return forms[0]; + /// + /// Base class for locale implementations. + /// + private abstract record LocaleBase(string Code) + { + public abstract string GetNumericWord(int number, string[] forms); + } - if (ones >= 2 && ones <= 4) - return forms[1]; - } + /// + /// Russian locale. + /// + private record LocaleRu() : LocaleBase("ru-RU") + { + public override string GetNumericWord(int number, string[] forms) + { + var ones = number % 10; + var tens = (number / 10) % 10; + + if (tens != 1) + { + if (ones == 1) + return forms[0]; - return forms[2]; + if (ones >= 2 && ones <= 4) + return forms[1]; } + + return forms[2]; } + } - /// - /// English locale. - /// - private record LocaleEn() : LocaleBase("en-US") + /// + /// English locale. + /// + private record LocaleEn() : LocaleBase("en-US") + { + public override string GetNumericWord(int number, string[] forms) { - public override string GetNumericWord(int number, string[] forms) - { - return number == 1 ? forms[0] : forms[1]; - } + return number == 1 ? forms[0] : forms[1]; } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bonsai/Code/Services/MarkdownService.cs b/src/Bonsai/Code/Services/MarkdownService.cs index 5f2fafa4..823bba09 100644 --- a/src/Bonsai/Code/Services/MarkdownService.cs +++ b/src/Bonsai/Code/Services/MarkdownService.cs @@ -13,218 +13,217 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace Bonsai.Code.Services +namespace Bonsai.Code.Services; + +/// +/// Markdown to HTML compilation service. +/// +public class MarkdownService { - /// - /// Markdown to HTML compilation service. - /// - public class MarkdownService + public MarkdownService(AppDbContext context, IUrlHelper urlHelper) { - public MarkdownService(AppDbContext context, IUrlHelper urlHelper) - { - _db = context; - _url = urlHelper; - - _render = new MarkdownPipelineBuilder() - .UseAutoLinks() - .UseAutoIdentifiers() - .UseEmphasisExtras() - .UseBootstrap() - .Build(); - } + _db = context; + _url = urlHelper; + + _render = new MarkdownPipelineBuilder() + .UseAutoLinks() + .UseAutoIdentifiers() + .UseEmphasisExtras() + .UseBootstrap() + .Build(); + } - private readonly MarkdownPipeline _render; - private readonly AppDbContext _db; - private readonly IUrlHelper _url; + private readonly MarkdownPipeline _render; + private readonly AppDbContext _db; + private readonly IUrlHelper _url; - private static readonly Regex MediaRegex = Compile(@"(?

)?\[\[media:(?[^\[|]+)(\|(?[^\]]+))?\]\](?(tag)

|)"); - private static readonly Regex LinkRegex = Compile(@"\[\[(?[^\[|]+)(\|(?