mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
A bunch of bug fixes and some enhancements (#3871)
Co-authored-by: Joseph Milazzo <josephmajora@gmail.com>
This commit is contained in:
parent
62231d3c4e
commit
6fa1cf994e
280
API.Tests/Repository/GenreRepositoryTests.cs
Normal file
280
API.Tests/Repository/GenreRepositoryTests.cs
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Metadata.Browse;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Helpers.Builders;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Repository;
|
||||||
|
|
||||||
|
public class GenreRepositoryTests : AbstractDbTest
|
||||||
|
{
|
||||||
|
private AppUser _fullAccess;
|
||||||
|
private AppUser _restrictedAccess;
|
||||||
|
private AppUser _restrictedAgeAccess;
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
|
{
|
||||||
|
Context.Genre.RemoveRange(Context.Genre);
|
||||||
|
Context.Library.RemoveRange(Context.Library);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestGenreSet CreateTestGenres()
|
||||||
|
{
|
||||||
|
return new TestGenreSet
|
||||||
|
{
|
||||||
|
SharedSeriesChaptersGenre = new GenreBuilder("Shared Series Chapter Genre").Build(),
|
||||||
|
SharedSeriesGenre = new GenreBuilder("Shared Series Genre").Build(),
|
||||||
|
SharedChaptersGenre = new GenreBuilder("Shared Chapters Genre").Build(),
|
||||||
|
Lib0SeriesChaptersGenre = new GenreBuilder("Lib0 Series Chapter Genre").Build(),
|
||||||
|
Lib0SeriesGenre = new GenreBuilder("Lib0 Series Genre").Build(),
|
||||||
|
Lib0ChaptersGenre = new GenreBuilder("Lib0 Chapters Genre").Build(),
|
||||||
|
Lib1SeriesChaptersGenre = new GenreBuilder("Lib1 Series Chapter Genre").Build(),
|
||||||
|
Lib1SeriesGenre = new GenreBuilder("Lib1 Series Genre").Build(),
|
||||||
|
Lib1ChaptersGenre = new GenreBuilder("Lib1 Chapters Genre").Build(),
|
||||||
|
Lib1ChapterAgeGenre = new GenreBuilder("Lib1 Chapter Age Genre").Build()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedDbWithGenres(TestGenreSet genres)
|
||||||
|
{
|
||||||
|
await CreateTestUsers();
|
||||||
|
await AddGenresToContext(genres);
|
||||||
|
await CreateLibrariesWithGenres(genres);
|
||||||
|
await AssignLibrariesToUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateTestUsers()
|
||||||
|
{
|
||||||
|
_fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build();
|
||||||
|
_restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build();
|
||||||
|
_restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build();
|
||||||
|
_restrictedAgeAccess.AgeRestriction = AgeRating.Teen;
|
||||||
|
_restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true;
|
||||||
|
|
||||||
|
Context.Users.Add(_fullAccess);
|
||||||
|
Context.Users.Add(_restrictedAccess);
|
||||||
|
Context.Users.Add(_restrictedAgeAccess);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddGenresToContext(TestGenreSet genres)
|
||||||
|
{
|
||||||
|
var allGenres = genres.GetAllGenres();
|
||||||
|
Context.Genre.AddRange(allGenres);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateLibrariesWithGenres(TestGenreSet genres)
|
||||||
|
{
|
||||||
|
var lib0 = new LibraryBuilder("lib0")
|
||||||
|
.WithSeries(new SeriesBuilder("lib0-s0")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0SeriesGenre])
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0ChaptersGenre])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre])
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var lib1 = new LibraryBuilder("lib1")
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s0")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre])
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre, genres.Lib1ChapterAgeGenre])
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s1")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre])
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre])
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Context.Library.Add(lib0);
|
||||||
|
Context.Library.Add(lib1);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AssignLibrariesToUsers()
|
||||||
|
{
|
||||||
|
var lib0 = Context.Library.First(l => l.Name == "lib0");
|
||||||
|
var lib1 = Context.Library.First(l => l.Name == "lib1");
|
||||||
|
|
||||||
|
_fullAccess.Libraries.Add(lib0);
|
||||||
|
_fullAccess.Libraries.Add(lib1);
|
||||||
|
_restrictedAccess.Libraries.Add(lib1);
|
||||||
|
_restrictedAgeAccess.Libraries.Add(lib1);
|
||||||
|
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Predicate<BrowseGenreDto> ContainsGenreCheck(Genre genre)
|
||||||
|
{
|
||||||
|
return g => g.Id == genre.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertGenrePresent(IEnumerable<BrowseGenreDto> genres, Genre expectedGenre)
|
||||||
|
{
|
||||||
|
Assert.Contains(genres, ContainsGenreCheck(expectedGenre));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertGenreNotPresent(IEnumerable<BrowseGenreDto> genres, Genre expectedGenre)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(genres, ContainsGenreCheck(expectedGenre));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BrowseGenreDto GetGenreDto(IEnumerable<BrowseGenreDto> genres, Genre genre)
|
||||||
|
{
|
||||||
|
return genres.First(dto => dto.Id == genre.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableGenre_FullAccess_ReturnsAllGenresWithCorrectCounts()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var genres = CreateTestGenres();
|
||||||
|
await SeedDbWithGenres(genres);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var fullAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_fullAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(genres.GetAllGenres().Count, fullAccessGenres.TotalCount);
|
||||||
|
|
||||||
|
foreach (var genre in genres.GetAllGenres())
|
||||||
|
{
|
||||||
|
AssertGenrePresent(fullAccessGenres, genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify counts - 1 lib0 series, 2 lib1 series = 3 total series
|
||||||
|
Assert.Equal(3, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount);
|
||||||
|
Assert.Equal(6, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount);
|
||||||
|
Assert.Equal(1, GetGenreDto(fullAccessGenres, genres.Lib0SeriesGenre).SeriesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableGenre_RestrictedAccess_ReturnsOnlyAccessibleGenres()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var genres = CreateTestGenres();
|
||||||
|
await SeedDbWithGenres(genres);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var restrictedAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert - Should see: 3 shared + 4 library 1 specific = 7 genres
|
||||||
|
Assert.Equal(7, restrictedAccessGenres.TotalCount);
|
||||||
|
|
||||||
|
// Verify shared and Library 1 genres are present
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.SharedChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChapterAgeGenre);
|
||||||
|
|
||||||
|
// Verify Library 0 specific genres are not present
|
||||||
|
AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesChaptersGenre);
|
||||||
|
AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesGenre);
|
||||||
|
AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0ChaptersGenre);
|
||||||
|
|
||||||
|
// Verify counts - 2 lib1 series
|
||||||
|
Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount);
|
||||||
|
Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount);
|
||||||
|
Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.Lib1SeriesGenre).SeriesCount);
|
||||||
|
Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.Lib1ChaptersGenre).ChapterCount);
|
||||||
|
Assert.Equal(1, GetGenreDto(restrictedAccessGenres, genres.Lib1ChapterAgeGenre).ChapterCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableGenre_RestrictedAgeAccess_FiltersAgeRestrictedContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var genres = CreateTestGenres();
|
||||||
|
await SeedDbWithGenres(genres);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var restrictedAgeAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAgeAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert - Should see: 3 shared + 3 lib1 specific = 6 genres (age-restricted genre filtered out)
|
||||||
|
Assert.Equal(6, restrictedAgeAccessGenres.TotalCount);
|
||||||
|
|
||||||
|
// Verify accessible genres are present
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesGenre);
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesChaptersGenre);
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesGenre);
|
||||||
|
AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre);
|
||||||
|
|
||||||
|
// Verify age-restricted genre is filtered out
|
||||||
|
AssertGenreNotPresent(restrictedAgeAccessGenres, genres.Lib1ChapterAgeGenre);
|
||||||
|
|
||||||
|
// Verify counts - 1 series lib1 (age-restricted series filtered out)
|
||||||
|
Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount);
|
||||||
|
Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1SeriesGenre).SeriesCount);
|
||||||
|
|
||||||
|
// These values represent a bug - chapters are not properly filtered when their series is age-restricted
|
||||||
|
// Should be 2, but currently returns 3 due to the filtering issue
|
||||||
|
Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount);
|
||||||
|
Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre).ChapterCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestGenreSet
|
||||||
|
{
|
||||||
|
public Genre SharedSeriesChaptersGenre { get; set; }
|
||||||
|
public Genre SharedSeriesGenre { get; set; }
|
||||||
|
public Genre SharedChaptersGenre { get; set; }
|
||||||
|
public Genre Lib0SeriesChaptersGenre { get; set; }
|
||||||
|
public Genre Lib0SeriesGenre { get; set; }
|
||||||
|
public Genre Lib0ChaptersGenre { get; set; }
|
||||||
|
public Genre Lib1SeriesChaptersGenre { get; set; }
|
||||||
|
public Genre Lib1SeriesGenre { get; set; }
|
||||||
|
public Genre Lib1ChaptersGenre { get; set; }
|
||||||
|
public Genre Lib1ChapterAgeGenre { get; set; }
|
||||||
|
|
||||||
|
public List<Genre> GetAllGenres()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
SharedSeriesChaptersGenre, SharedSeriesGenre, SharedChaptersGenre,
|
||||||
|
Lib0SeriesChaptersGenre, Lib0SeriesGenre, Lib0ChaptersGenre,
|
||||||
|
Lib1SeriesChaptersGenre, Lib1SeriesGenre, Lib1ChaptersGenre, Lib1ChapterAgeGenre
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
342
API.Tests/Repository/PersonRepositoryTests.cs
Normal file
342
API.Tests/Repository/PersonRepositoryTests.cs
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Metadata.Browse;
|
||||||
|
using API.DTOs.Metadata.Browse.Requests;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Person;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Helpers.Builders;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Repository;
|
||||||
|
|
||||||
|
public class PersonRepositoryTests : AbstractDbTest
|
||||||
|
{
|
||||||
|
private AppUser _fullAccess;
|
||||||
|
private AppUser _restrictedAccess;
|
||||||
|
private AppUser _restrictedAgeAccess;
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
|
{
|
||||||
|
Context.Person.RemoveRange(Context.Person.ToList());
|
||||||
|
Context.Library.RemoveRange(Context.Library.ToList());
|
||||||
|
Context.AppUser.RemoveRange(Context.AppUser.ToList());
|
||||||
|
await UnitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedDb()
|
||||||
|
{
|
||||||
|
_fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build();
|
||||||
|
_restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build();
|
||||||
|
_restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build();
|
||||||
|
_restrictedAgeAccess.AgeRestriction = AgeRating.Teen;
|
||||||
|
_restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true;
|
||||||
|
|
||||||
|
Context.AppUser.Add(_fullAccess);
|
||||||
|
Context.AppUser.Add(_restrictedAccess);
|
||||||
|
Context.AppUser.Add(_restrictedAgeAccess);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var people = CreateTestPeople();
|
||||||
|
Context.Person.AddRange(people);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var libraries = CreateTestLibraries(people);
|
||||||
|
Context.Library.AddRange(libraries);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_fullAccess.Libraries.Add(libraries[0]); // lib0
|
||||||
|
_fullAccess.Libraries.Add(libraries[1]); // lib1
|
||||||
|
_restrictedAccess.Libraries.Add(libraries[1]); // lib1 only
|
||||||
|
_restrictedAgeAccess.Libraries.Add(libraries[1]); // lib1 only
|
||||||
|
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Person> CreateTestPeople()
|
||||||
|
{
|
||||||
|
return new List<Person>
|
||||||
|
{
|
||||||
|
new PersonBuilder("Shared Series Chapter Person").Build(),
|
||||||
|
new PersonBuilder("Shared Series Person").Build(),
|
||||||
|
new PersonBuilder("Shared Chapters Person").Build(),
|
||||||
|
new PersonBuilder("Lib0 Series Chapter Person").Build(),
|
||||||
|
new PersonBuilder("Lib0 Series Person").Build(),
|
||||||
|
new PersonBuilder("Lib0 Chapters Person").Build(),
|
||||||
|
new PersonBuilder("Lib1 Series Chapter Person").Build(),
|
||||||
|
new PersonBuilder("Lib1 Series Person").Build(),
|
||||||
|
new PersonBuilder("Lib1 Chapters Person").Build(),
|
||||||
|
new PersonBuilder("Lib1 Chapter Age Person").Build()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Library> CreateTestLibraries(List<Person> people)
|
||||||
|
{
|
||||||
|
var lib0 = new LibraryBuilder("lib0")
|
||||||
|
.WithSeries(new SeriesBuilder("lib0-s0")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Writer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Writer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Writer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Series Person"), PersonRole.Writer)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Colorist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Colorist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Colorist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Colorist)
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Editor)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Editor)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Editor)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Editor)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var lib1 = new LibraryBuilder("lib1")
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s0")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Letterer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Letterer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Letterer)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Letterer)
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Imprint)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Imprint)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Imprint)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Imprint)
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.CoverArtist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.CoverArtist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.CoverArtist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.CoverArtist)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Chapter Age Person"), PersonRole.CoverArtist)
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s1")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Inker)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Inker)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Inker)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Inker)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Team)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Team)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Team)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Team)
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Translator)
|
||||||
|
.WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Translator)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Translator)
|
||||||
|
.WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Translator)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return new List<Library> { lib0, lib1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Person GetPersonByName(List<Person> people, string name)
|
||||||
|
{
|
||||||
|
return people.First(p => p.Name == name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Person GetPersonByName(string name)
|
||||||
|
{
|
||||||
|
return Context.Person.First(p => p.Name == name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Predicate<BrowsePersonDto> ContainsPersonCheck(Person person)
|
||||||
|
{
|
||||||
|
return p => p.Id == person.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowsePersonDtos()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
await SeedDb();
|
||||||
|
|
||||||
|
// Get people from database for assertions
|
||||||
|
var sharedSeriesChaptersPerson = GetPersonByName("Shared Series Chapter Person");
|
||||||
|
var lib0SeriesPerson = GetPersonByName("Lib0 Series Person");
|
||||||
|
var lib1SeriesPerson = GetPersonByName("Lib1 Series Person");
|
||||||
|
var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person");
|
||||||
|
var allPeople = Context.Person.ToList();
|
||||||
|
|
||||||
|
var fullAccessPeople =
|
||||||
|
await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_fullAccess.Id, new BrowsePersonFilterDto(),
|
||||||
|
new UserParams());
|
||||||
|
Assert.Equal(allPeople.Count, fullAccessPeople.TotalCount);
|
||||||
|
|
||||||
|
foreach (var person in allPeople)
|
||||||
|
Assert.Contains(fullAccessPeople, ContainsPersonCheck(person));
|
||||||
|
|
||||||
|
// 1 series in lib0, 2 series in lib1
|
||||||
|
Assert.Equal(3, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount);
|
||||||
|
// 3 series with each 2 chapters
|
||||||
|
Assert.Equal(6, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount);
|
||||||
|
// 1 series in lib0
|
||||||
|
Assert.Equal(1, fullAccessPeople.First(dto => dto.Id == lib0SeriesPerson.Id).SeriesCount);
|
||||||
|
// 2 series in lib1
|
||||||
|
Assert.Equal(2, fullAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount);
|
||||||
|
|
||||||
|
var restrictedAccessPeople =
|
||||||
|
await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAccess.Id, new BrowsePersonFilterDto(),
|
||||||
|
new UserParams());
|
||||||
|
|
||||||
|
Assert.Equal(7, restrictedAccessPeople.TotalCount);
|
||||||
|
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Chapter Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Chapters Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Chapter Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapters Person")));
|
||||||
|
Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapter Age Person")));
|
||||||
|
|
||||||
|
// 2 series in lib1, no series in lib0
|
||||||
|
Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount);
|
||||||
|
// 2 series with each 2 chapters
|
||||||
|
Assert.Equal(4, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount);
|
||||||
|
// 2 series in lib1
|
||||||
|
Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount);
|
||||||
|
|
||||||
|
var restrictedAgeAccessPeople = await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAgeAccess.Id,
|
||||||
|
new BrowsePersonFilterDto(), new UserParams());
|
||||||
|
|
||||||
|
// Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up
|
||||||
|
Assert.Equal(6, restrictedAgeAccessPeople.TotalCount);
|
||||||
|
|
||||||
|
// No access to the age restricted chapter
|
||||||
|
Assert.DoesNotContain(restrictedAgeAccessPeople, ContainsPersonCheck(lib1ChapterAgePerson));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetRolesForPersonByName()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
await SeedDb();
|
||||||
|
|
||||||
|
var sharedSeriesPerson = GetPersonByName("Shared Series Person");
|
||||||
|
var sharedChaptersPerson = GetPersonByName("Shared Chapters Person");
|
||||||
|
var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person");
|
||||||
|
|
||||||
|
var sharedSeriesRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _fullAccess.Id);
|
||||||
|
var chapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _fullAccess.Id);
|
||||||
|
var ageChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _fullAccess.Id);
|
||||||
|
Assert.Equal(3, sharedSeriesRoles.Count());
|
||||||
|
Assert.Equal(6, chapterRoles.Count());
|
||||||
|
Assert.Single(ageChapterRoles);
|
||||||
|
|
||||||
|
var restrictedRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAccess.Id);
|
||||||
|
var restrictedChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAccess.Id);
|
||||||
|
var restrictedAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAccess.Id);
|
||||||
|
Assert.Equal(2, restrictedRoles.Count());
|
||||||
|
Assert.Equal(4, restrictedChapterRoles.Count());
|
||||||
|
Assert.Single(restrictedAgePersonChapterRoles);
|
||||||
|
|
||||||
|
var restrictedAgeRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAgeAccess.Id);
|
||||||
|
var restrictedAgeChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAgeAccess.Id);
|
||||||
|
var restrictedAgeAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAgeAccess.Id);
|
||||||
|
Assert.Single(restrictedAgeRoles);
|
||||||
|
Assert.Equal(2, restrictedAgeChapterRoles.Count());
|
||||||
|
// Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up
|
||||||
|
Assert.Empty(restrictedAgeAgePersonChapterRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPersonDtoByName()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
await SeedDb();
|
||||||
|
|
||||||
|
var allPeople = Context.Person.ToList();
|
||||||
|
|
||||||
|
foreach (var person in allPeople)
|
||||||
|
{
|
||||||
|
Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName(person.Name, _fullAccess.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAccess.Id));
|
||||||
|
Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Shared Series Person", _restrictedAccess.Id));
|
||||||
|
Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAccess.Id));
|
||||||
|
|
||||||
|
Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAgeAccess.Id));
|
||||||
|
Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAgeAccess.Id));
|
||||||
|
// Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up
|
||||||
|
Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Chapter Age Person", _restrictedAgeAccess.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSeriesKnownFor()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
await SeedDb();
|
||||||
|
|
||||||
|
var sharedSeriesPerson = GetPersonByName("Shared Series Person");
|
||||||
|
var lib1SeriesPerson = GetPersonByName("Lib1 Series Person");
|
||||||
|
|
||||||
|
var series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _fullAccess.Id);
|
||||||
|
Assert.Equal(3, series.Count());
|
||||||
|
|
||||||
|
series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAccess.Id);
|
||||||
|
Assert.Equal(2, series.Count());
|
||||||
|
|
||||||
|
series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAgeAccess.Id);
|
||||||
|
Assert.Single(series);
|
||||||
|
|
||||||
|
series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, _restrictedAgeAccess.Id);
|
||||||
|
Assert.Single(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetChaptersForPersonByRole()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
await SeedDb();
|
||||||
|
|
||||||
|
var sharedChaptersPerson = GetPersonByName("Shared Chapters Person");
|
||||||
|
|
||||||
|
// Lib0
|
||||||
|
var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Colorist);
|
||||||
|
var restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Colorist);
|
||||||
|
var restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Colorist);
|
||||||
|
Assert.Single(chapters);
|
||||||
|
Assert.Empty(restrictedChapters);
|
||||||
|
Assert.Empty(restrictedAgeChapters);
|
||||||
|
|
||||||
|
// Lib1 - age restricted series
|
||||||
|
chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Imprint);
|
||||||
|
restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Imprint);
|
||||||
|
restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Imprint);
|
||||||
|
Assert.Single(chapters);
|
||||||
|
Assert.Single(restrictedChapters);
|
||||||
|
Assert.Empty(restrictedAgeChapters);
|
||||||
|
|
||||||
|
// Lib1 - not age restricted series
|
||||||
|
chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Team);
|
||||||
|
restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Team);
|
||||||
|
restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Team);
|
||||||
|
Assert.Single(chapters);
|
||||||
|
Assert.Single(restrictedChapters);
|
||||||
|
Assert.Single(restrictedAgeChapters);
|
||||||
|
}
|
||||||
|
}
|
278
API.Tests/Repository/TagRepositoryTests.cs
Normal file
278
API.Tests/Repository/TagRepositoryTests.cs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Metadata.Browse;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Helpers.Builders;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Repository;
|
||||||
|
|
||||||
|
public class TagRepositoryTests : AbstractDbTest
|
||||||
|
{
|
||||||
|
private AppUser _fullAccess;
|
||||||
|
private AppUser _restrictedAccess;
|
||||||
|
private AppUser _restrictedAgeAccess;
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
|
{
|
||||||
|
Context.Tag.RemoveRange(Context.Tag);
|
||||||
|
Context.Library.RemoveRange(Context.Library);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestTagSet CreateTestTags()
|
||||||
|
{
|
||||||
|
return new TestTagSet
|
||||||
|
{
|
||||||
|
SharedSeriesChaptersTag = new TagBuilder("Shared Series Chapter Tag").Build(),
|
||||||
|
SharedSeriesTag = new TagBuilder("Shared Series Tag").Build(),
|
||||||
|
SharedChaptersTag = new TagBuilder("Shared Chapters Tag").Build(),
|
||||||
|
Lib0SeriesChaptersTag = new TagBuilder("Lib0 Series Chapter Tag").Build(),
|
||||||
|
Lib0SeriesTag = new TagBuilder("Lib0 Series Tag").Build(),
|
||||||
|
Lib0ChaptersTag = new TagBuilder("Lib0 Chapters Tag").Build(),
|
||||||
|
Lib1SeriesChaptersTag = new TagBuilder("Lib1 Series Chapter Tag").Build(),
|
||||||
|
Lib1SeriesTag = new TagBuilder("Lib1 Series Tag").Build(),
|
||||||
|
Lib1ChaptersTag = new TagBuilder("Lib1 Chapters Tag").Build(),
|
||||||
|
Lib1ChapterAgeTag = new TagBuilder("Lib1 Chapter Age Tag").Build()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedDbWithTags(TestTagSet tags)
|
||||||
|
{
|
||||||
|
await CreateTestUsers();
|
||||||
|
await AddTagsToContext(tags);
|
||||||
|
await CreateLibrariesWithTags(tags);
|
||||||
|
await AssignLibrariesToUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateTestUsers()
|
||||||
|
{
|
||||||
|
_fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build();
|
||||||
|
_restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build();
|
||||||
|
_restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build();
|
||||||
|
_restrictedAgeAccess.AgeRestriction = AgeRating.Teen;
|
||||||
|
_restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true;
|
||||||
|
|
||||||
|
Context.Users.Add(_fullAccess);
|
||||||
|
Context.Users.Add(_restrictedAccess);
|
||||||
|
Context.Users.Add(_restrictedAgeAccess);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddTagsToContext(TestTagSet tags)
|
||||||
|
{
|
||||||
|
var allTags = tags.GetAllTags();
|
||||||
|
Context.Tag.AddRange(allTags);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateLibrariesWithTags(TestTagSet tags)
|
||||||
|
{
|
||||||
|
var lib0 = new LibraryBuilder("lib0")
|
||||||
|
.WithSeries(new SeriesBuilder("lib0-s0")
|
||||||
|
.WithMetadata(new SeriesMetadata
|
||||||
|
{
|
||||||
|
Tags = [tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib0SeriesChaptersTag, tags.Lib0SeriesTag]
|
||||||
|
})
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib0SeriesChaptersTag, tags.Lib0ChaptersTag])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag])
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var lib1 = new LibraryBuilder("lib1")
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s0")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag])
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag, tags.Lib1ChapterAgeTag])
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.WithSeries(new SeriesBuilder("lib1-s1")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag])
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1")
|
||||||
|
.WithChapter(new ChapterBuilder("1")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag])
|
||||||
|
.Build())
|
||||||
|
.WithChapter(new ChapterBuilder("2")
|
||||||
|
.WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag])
|
||||||
|
.WithAgeRating(AgeRating.Mature17Plus)
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Context.Library.Add(lib0);
|
||||||
|
Context.Library.Add(lib1);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AssignLibrariesToUsers()
|
||||||
|
{
|
||||||
|
var lib0 = Context.Library.First(l => l.Name == "lib0");
|
||||||
|
var lib1 = Context.Library.First(l => l.Name == "lib1");
|
||||||
|
|
||||||
|
_fullAccess.Libraries.Add(lib0);
|
||||||
|
_fullAccess.Libraries.Add(lib1);
|
||||||
|
_restrictedAccess.Libraries.Add(lib1);
|
||||||
|
_restrictedAgeAccess.Libraries.Add(lib1);
|
||||||
|
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Predicate<BrowseTagDto> ContainsTagCheck(Tag tag)
|
||||||
|
{
|
||||||
|
return t => t.Id == tag.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertTagPresent(IEnumerable<BrowseTagDto> tags, Tag expectedTag)
|
||||||
|
{
|
||||||
|
Assert.Contains(tags, ContainsTagCheck(expectedTag));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertTagNotPresent(IEnumerable<BrowseTagDto> tags, Tag expectedTag)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(tags, ContainsTagCheck(expectedTag));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BrowseTagDto GetTagDto(IEnumerable<BrowseTagDto> tags, Tag tag)
|
||||||
|
{
|
||||||
|
return tags.First(dto => dto.Id == tag.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableTag_FullAccess_ReturnsAllTagsWithCorrectCounts()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var tags = CreateTestTags();
|
||||||
|
await SeedDbWithTags(tags);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var fullAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_fullAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(tags.GetAllTags().Count, fullAccessTags.TotalCount);
|
||||||
|
|
||||||
|
foreach (var tag in tags.GetAllTags())
|
||||||
|
{
|
||||||
|
AssertTagPresent(fullAccessTags, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify counts - 1 series lib0, 2 series lib1 = 3 total series
|
||||||
|
Assert.Equal(3, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).SeriesCount);
|
||||||
|
Assert.Equal(6, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).ChapterCount);
|
||||||
|
Assert.Equal(1, GetTagDto(fullAccessTags, tags.Lib0SeriesTag).SeriesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableTag_RestrictedAccess_ReturnsOnlyAccessibleTags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var tags = CreateTestTags();
|
||||||
|
await SeedDbWithTags(tags);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var restrictedAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert - Should see: 3 shared + 4 library 1 specific = 7 tags
|
||||||
|
Assert.Equal(7, restrictedAccessTags.TotalCount);
|
||||||
|
|
||||||
|
// Verify shared and Library 1 tags are present
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.SharedSeriesChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.SharedSeriesTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.SharedChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.Lib1ChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAccessTags, tags.Lib1ChapterAgeTag);
|
||||||
|
|
||||||
|
// Verify Library 0 specific tags are not present
|
||||||
|
AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesChaptersTag);
|
||||||
|
AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesTag);
|
||||||
|
AssertTagNotPresent(restrictedAccessTags, tags.Lib0ChaptersTag);
|
||||||
|
|
||||||
|
// Verify counts - 2 series lib1
|
||||||
|
Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).SeriesCount);
|
||||||
|
Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).ChapterCount);
|
||||||
|
Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.Lib1SeriesTag).SeriesCount);
|
||||||
|
Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.Lib1ChaptersTag).ChapterCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBrowseableTag_RestrictedAgeAccess_FiltersAgeRestrictedContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await ResetDb();
|
||||||
|
var tags = CreateTestTags();
|
||||||
|
await SeedDbWithTags(tags);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var restrictedAgeAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAgeAccess.Id, new UserParams());
|
||||||
|
|
||||||
|
// Assert - Should see: 3 shared + 3 lib1 specific = 6 tags (age-restricted tag filtered out)
|
||||||
|
Assert.Equal(6, restrictedAgeAccessTags.TotalCount);
|
||||||
|
|
||||||
|
// Verify accessible tags are present
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesTag);
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.SharedChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesChaptersTag);
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesTag);
|
||||||
|
AssertTagPresent(restrictedAgeAccessTags, tags.Lib1ChaptersTag);
|
||||||
|
|
||||||
|
// Verify age-restricted tag is filtered out
|
||||||
|
AssertTagNotPresent(restrictedAgeAccessTags, tags.Lib1ChapterAgeTag);
|
||||||
|
|
||||||
|
// Verify counts - 1 series lib1 (age-restricted series filtered out)
|
||||||
|
Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).SeriesCount);
|
||||||
|
Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).ChapterCount);
|
||||||
|
Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.Lib1SeriesTag).SeriesCount);
|
||||||
|
Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.Lib1ChaptersTag).ChapterCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestTagSet
|
||||||
|
{
|
||||||
|
public Tag SharedSeriesChaptersTag { get; set; }
|
||||||
|
public Tag SharedSeriesTag { get; set; }
|
||||||
|
public Tag SharedChaptersTag { get; set; }
|
||||||
|
public Tag Lib0SeriesChaptersTag { get; set; }
|
||||||
|
public Tag Lib0SeriesTag { get; set; }
|
||||||
|
public Tag Lib0ChaptersTag { get; set; }
|
||||||
|
public Tag Lib1SeriesChaptersTag { get; set; }
|
||||||
|
public Tag Lib1SeriesTag { get; set; }
|
||||||
|
public Tag Lib1ChaptersTag { get; set; }
|
||||||
|
public Tag Lib1ChapterAgeTag { get; set; }
|
||||||
|
|
||||||
|
public List<Tag> GetAllTags()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
SharedSeriesChaptersTag, SharedSeriesTag, SharedChaptersTag,
|
||||||
|
Lib0SeriesChaptersTag, Lib0SeriesTag, Lib0ChaptersTag,
|
||||||
|
Lib1SeriesChaptersTag, Lib1SeriesTag, Lib1ChaptersTag, Lib1ChapterAgeTag
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ using API.Entities.Person;
|
|||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks.Metadata;
|
using API.Services.Tasks.Metadata;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -881,6 +882,217 @@ public class ExternalMetadataServiceTests : AbstractDbTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_ExactMatch()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Exact Match";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(5)
|
||||||
|
.WithTotalCount(5)
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 5, Volumes = 0 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_Volumes_DecimalVolumes()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Volume Complete";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(2)
|
||||||
|
.WithTotalCount(3)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1").WithNumber(1).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2").WithNumber(2).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2.5").WithNumber(2.5f).Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
// External metadata includes decimal volume 2.5
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(3, series.Metadata.MaxCount);
|
||||||
|
Assert.Equal(3, series.Metadata.TotalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is validating that we get a completed even though we have a special chapter and AL doesn't count it
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_Volumes_HasSpecialAndDecimal_ExternalNoSpecial()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Volume Complete";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(2)
|
||||||
|
.WithTotalCount(3)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1").WithNumber(1).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1.5").WithNumber(1.5f).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2").WithNumber(2).Build())
|
||||||
|
.WithVolume(new VolumeBuilder(Parser.SpecialVolume).Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
// External metadata includes volume 1.5, but not the special
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(3, series.Metadata.MaxCount);
|
||||||
|
Assert.Equal(3, series.Metadata.TotalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This unit test also illustrates the bug where you may get a false positive if you had Volumes 1,2, and 2.1. While
|
||||||
|
/// missing volume 3. With the external metadata expecting non-decimal volumes.
|
||||||
|
/// i.e. it would fail if we only had one decimal volume
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_Volumes_TooManyDecimalVolumes()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Volume Complete";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(2)
|
||||||
|
.WithTotalCount(3)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1").WithNumber(1).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2").WithNumber(2).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2.1").WithNumber(2.1f).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2.2").WithNumber(2.2f).Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
// External metadata includes no special or decimals. There are 3 volumes. And we're missing volume 3
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_NoVolumes_GEQChapterCheck()
|
||||||
|
{
|
||||||
|
// We own 11 chapters, the external metadata expects 10
|
||||||
|
const string seriesName = "Test - Chapter MaxCount, no volumes";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(11)
|
||||||
|
.WithTotalCount(10)
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(11, series.Metadata.TotalCount);
|
||||||
|
Assert.Equal(11, series.Metadata.MaxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_NoVolumes_IncludeAllChaptersCheck()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Chapter Count";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(7)
|
||||||
|
.WithTotalCount(10)
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>
|
||||||
|
{
|
||||||
|
new ChapterBuilder("0").Build(),
|
||||||
|
new ChapterBuilder("2").Build(),
|
||||||
|
new ChapterBuilder("3").Build(),
|
||||||
|
new ChapterBuilder("4").Build(),
|
||||||
|
new ChapterBuilder("5").Build(),
|
||||||
|
new ChapterBuilder("6").Build(),
|
||||||
|
new ChapterBuilder("7").Build(),
|
||||||
|
new ChapterBuilder("7.1").Build(),
|
||||||
|
new ChapterBuilder("7.2").Build(),
|
||||||
|
new ChapterBuilder("7.3").Build()
|
||||||
|
};
|
||||||
|
// External metadata includes prologues (0) and extra's (7.X)
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(10, series.Metadata.TotalCount);
|
||||||
|
Assert.Equal(10, series.Metadata.MaxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_NotEnoughVolumes()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Incomplete Volume";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(2)
|
||||||
|
.WithTotalCount(5)
|
||||||
|
.Build())
|
||||||
|
.WithVolume(new VolumeBuilder("1").WithNumber(1).Build())
|
||||||
|
.WithVolume(new VolumeBuilder("2").WithNumber(2).Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>();
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 5 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsSeriesCompleted_NoVolumes_NotEnoughChapters()
|
||||||
|
{
|
||||||
|
const string seriesName = "Test - Incomplete Chapter";
|
||||||
|
var series = new SeriesBuilder(seriesName)
|
||||||
|
.WithLibraryId(1)
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder()
|
||||||
|
.WithMaxCount(5)
|
||||||
|
.WithTotalCount(8)
|
||||||
|
.Build())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var chapters = new List<Chapter>
|
||||||
|
{
|
||||||
|
new ChapterBuilder("1").Build(),
|
||||||
|
new ChapterBuilder("2").Build(),
|
||||||
|
new ChapterBuilder("3").Build()
|
||||||
|
};
|
||||||
|
var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 };
|
||||||
|
|
||||||
|
var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ public class PersonController : BaseApiController
|
|||||||
[HttpGet("series-known-for")]
|
[HttpGet("series-known-for")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetKnownSeries(int personId)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetKnownSeries(int personId)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId));
|
return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, User.GetUserId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -206,6 +206,7 @@ public class PersonController : BaseApiController
|
|||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("merge")]
|
[HttpPost("merge")]
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
public async Task<ActionResult<PersonDto>> MergePeople(PersonMergeDto dto)
|
public async Task<ActionResult<PersonDto>> MergePeople(PersonMergeDto dto)
|
||||||
{
|
{
|
||||||
var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All);
|
var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All);
|
||||||
|
@ -29,7 +29,9 @@ public sealed record ExternalSeriesDetailDto
|
|||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
public DateTime? EndDate { get; set; }
|
public DateTime? EndDate { get; set; }
|
||||||
public int AverageScore { get; set; }
|
public int AverageScore { get; set; }
|
||||||
|
/// <remarks>AniList returns the total count of unique chapters, includes 1.1 for example</remarks>
|
||||||
public int Chapters { get; set; }
|
public int Chapters { get; set; }
|
||||||
|
/// <remarks>AniList returns the total count of unique volumes, includes 1.1 for example</remarks>
|
||||||
public int Volumes { get; set; }
|
public int Volumes { get; set; }
|
||||||
public IList<SeriesRelationship>? Relations { get; set; } = [];
|
public IList<SeriesRelationship>? Relations { get; set; } = [];
|
||||||
public IList<SeriesCharacter>? Characters { get; set; } = [];
|
public IList<SeriesCharacter>? Characters { get; set; } = [];
|
||||||
|
@ -173,20 +173,30 @@ public class GenreRepository : IGenreRepository
|
|||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
|
||||||
|
var allLibrariesCount = await _context.Library.CountAsync();
|
||||||
|
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||||
|
|
||||||
|
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
|
||||||
|
|
||||||
var query = _context.Genre
|
var query = _context.Genre
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.WhereIf(allLibrariesCount != userLibs.Count,
|
||||||
|
genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
|
||||||
|
genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
|
||||||
.Select(g => new BrowseGenreDto
|
.Select(g => new BrowseGenreDto
|
||||||
{
|
{
|
||||||
Id = g.Id,
|
Id = g.Id,
|
||||||
Title = g.Title,
|
Title = g.Title,
|
||||||
SeriesCount = g.SeriesMetadatas
|
SeriesCount = g.SeriesMetadatas
|
||||||
.Select(sm => sm.Id)
|
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count(),
|
.Count(),
|
||||||
ChapterCount = g.Chapters
|
ChapterCount = g.Chapters
|
||||||
.Select(ch => ch.Id)
|
.Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count()
|
.Count(),
|
||||||
})
|
})
|
||||||
.OrderBy(g => g.Title);
|
.OrderBy(g => g.Title);
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ public interface IPersonRepository
|
|||||||
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
||||||
Task<bool> IsNameUnique(string name);
|
Task<bool> IsNameUnique(string name);
|
||||||
|
|
||||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId);
|
||||||
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
|
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all people with a matching name, or alias
|
/// Returns all people with a matching name, or alias
|
||||||
@ -179,20 +179,25 @@ public class PersonRepository : IPersonRepository
|
|||||||
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId)
|
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId)
|
||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||||
|
|
||||||
// Query roles from ChapterPeople
|
// Query roles from ChapterPeople
|
||||||
var chapterRoles = await _context.Person
|
var chapterRoles = await _context.Person
|
||||||
.Where(p => p.Id == personId)
|
.Where(p => p.Id == personId)
|
||||||
|
.SelectMany(p => p.ChapterPeople)
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.SelectMany(p => p.ChapterPeople.Select(cp => cp.Role))
|
.RestrictByLibrary(userLibs)
|
||||||
|
.Select(cp => cp.Role)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// Query roles from SeriesMetadataPeople
|
// Query roles from SeriesMetadataPeople
|
||||||
var seriesRoles = await _context.Person
|
var seriesRoles = await _context.Person
|
||||||
.Where(p => p.Id == personId)
|
.Where(p => p.Id == personId)
|
||||||
|
.SelectMany(p => p.SeriesMetadataPeople)
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role))
|
.RestrictByLibrary(userLibs)
|
||||||
|
.Select(smp => smp.Role)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@ -204,44 +209,53 @@ public class PersonRepository : IPersonRepository
|
|||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
|
||||||
var query = CreateFilteredPersonQueryable(userId, filter, ageRating);
|
var query = await CreateFilteredPersonQueryable(userId, filter, ageRating);
|
||||||
|
|
||||||
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IQueryable<BrowsePersonDto> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating)
|
private async Task<IQueryable<BrowsePersonDto>> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating)
|
||||||
{
|
{
|
||||||
|
var allLibrariesCount = await _context.Library.CountAsync();
|
||||||
|
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||||
|
|
||||||
|
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
|
||||||
|
|
||||||
var query = _context.Person.AsNoTracking();
|
var query = _context.Person.AsNoTracking();
|
||||||
|
|
||||||
// Apply filtering based on statements
|
// Apply filtering based on statements
|
||||||
query = BuildPersonFilterQuery(userId, filter, query);
|
query = BuildPersonFilterQuery(userId, filter, query);
|
||||||
|
|
||||||
// Apply age restriction
|
// Apply restrictions
|
||||||
query = query.RestrictAgainstAgeRestriction(ageRating);
|
query = query.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.WhereIf(allLibrariesCount != userLibs.Count,
|
||||||
|
person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) ||
|
||||||
|
person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId)));
|
||||||
|
|
||||||
// Apply sorting and limiting
|
// Apply sorting and limiting
|
||||||
var sortedQuery = query.SortBy(filter.SortOptions);
|
var sortedQuery = query.SortBy(filter.SortOptions);
|
||||||
|
|
||||||
var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo);
|
var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo);
|
||||||
|
|
||||||
// Project to DTO
|
return limitedQuery.Select(p => new BrowsePersonDto
|
||||||
var projectedQuery = limitedQuery.Select(p => new BrowsePersonDto
|
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
Name = p.Name,
|
Name = p.Name,
|
||||||
Description = p.Description,
|
Description = p.Description,
|
||||||
CoverImage = p.CoverImage,
|
CoverImage = p.CoverImage,
|
||||||
SeriesCount = p.SeriesMetadataPeople
|
SeriesCount = p.SeriesMetadataPeople
|
||||||
.Select(smp => smp.SeriesMetadata.SeriesId)
|
.Select(smp => smp.SeriesMetadata)
|
||||||
|
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count(),
|
.Count(),
|
||||||
ChapterCount = p.ChapterPeople
|
ChapterCount = p.ChapterPeople
|
||||||
.Select(cp => cp.Chapter.Id)
|
.Select(chp => chp.Chapter)
|
||||||
|
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count()
|
.Count(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return projectedQuery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IQueryable<Person> BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable<Person> query)
|
private static IQueryable<Person> BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable<Person> query)
|
||||||
@ -287,11 +301,13 @@ public class PersonRepository : IPersonRepository
|
|||||||
{
|
{
|
||||||
var normalized = name.ToNormalized();
|
var normalized = name.ToNormalized();
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||||
|
|
||||||
return await _context.Person
|
return await _context.Person
|
||||||
.Where(p => p.NormalizedName == normalized)
|
.Where(p => p.NormalizedName == normalized)
|
||||||
.Includes(includes)
|
.Includes(includes)
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.RestrictByLibrary(userLibs)
|
||||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
@ -313,14 +329,18 @@ public class PersonRepository : IPersonRepository
|
|||||||
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
|
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId)
|
||||||
{
|
{
|
||||||
List<PersonRole> notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator];
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||||
|
|
||||||
return await _context.Person
|
return await _context.Person
|
||||||
.Where(p => p.Id == personId)
|
.Where(p => p.Id == personId)
|
||||||
.SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role)))
|
.SelectMany(p => p.SeriesMetadataPeople)
|
||||||
.Select(smp => smp.SeriesMetadata)
|
.Select(smp => smp.SeriesMetadata)
|
||||||
.Select(sm => sm.Series)
|
.Select(sm => sm.Series)
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.Where(s => userLibs.Contains(s.LibraryId))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
|
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
|
||||||
.Take(20)
|
.Take(20)
|
||||||
@ -331,11 +351,13 @@ public class PersonRepository : IPersonRepository
|
|||||||
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
|
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
|
||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||||
|
|
||||||
return await _context.ChapterPeople
|
return await _context.ChapterPeople
|
||||||
.Where(cp => cp.PersonId == personId && cp.Role == role)
|
.Where(cp => cp.PersonId == personId && cp.Role == role)
|
||||||
.Select(cp => cp.Chapter)
|
.Select(cp => cp.Chapter)
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.RestrictByLibrary(userLibs)
|
||||||
.OrderBy(ch => ch.SortOrder)
|
.OrderBy(ch => ch.SortOrder)
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
|
||||||
@ -386,27 +408,31 @@ public class PersonRepository : IPersonRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.Aliases)
|
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None)
|
||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||||
|
|
||||||
return await _context.Person
|
return await _context.Person
|
||||||
.Includes(includes)
|
.Includes(includes)
|
||||||
.OrderBy(p => p.Name)
|
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.RestrictByLibrary(userLibs)
|
||||||
|
.OrderBy(p => p.Name)
|
||||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.Aliases)
|
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None)
|
||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
var userLibs = _context.Library.GetUserLibraries(userId);
|
||||||
|
|
||||||
return await _context.Person
|
return await _context.Person
|
||||||
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
|
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
|
||||||
.Includes(includes)
|
.Includes(includes)
|
||||||
.OrderBy(p => p.Name)
|
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.RestrictByLibrary(userLibs)
|
||||||
|
.OrderBy(p => p.Name)
|
||||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
@ -111,18 +111,28 @@ public class TagRepository : ITagRepository
|
|||||||
{
|
{
|
||||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
|
||||||
|
var allLibrariesCount = await _context.Library.CountAsync();
|
||||||
|
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||||
|
|
||||||
|
var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id);
|
||||||
|
|
||||||
var query = _context.Tag
|
var query = _context.Tag
|
||||||
.RestrictAgainstAgeRestriction(ageRating)
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.WhereIf(userLibs.Count != allLibrariesCount,
|
||||||
|
tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
|
||||||
|
tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
|
||||||
.Select(g => new BrowseTagDto
|
.Select(g => new BrowseTagDto
|
||||||
{
|
{
|
||||||
Id = g.Id,
|
Id = g.Id,
|
||||||
Title = g.Title,
|
Title = g.Title,
|
||||||
SeriesCount = g.SeriesMetadatas
|
SeriesCount = g.SeriesMetadatas
|
||||||
.Select(sm => sm.Id)
|
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count(),
|
.Count(),
|
||||||
ChapterCount = g.Chapters
|
ChapterCount = g.Chapters
|
||||||
.Select(ch => ch.Id)
|
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Count()
|
.Count()
|
||||||
})
|
})
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
|
|
||||||
@ -55,4 +56,16 @@ public static class EnumerableExtensions
|
|||||||
|
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<Chapter> RestrictAgainstAgeRestriction(this IEnumerable<Chapter> items, AgeRestriction restriction)
|
||||||
|
{
|
||||||
|
if (restriction.AgeRating == AgeRating.NotApplicable) return items;
|
||||||
|
var q = items.Where(s => s.AgeRating <= restriction.AgeRating);
|
||||||
|
if (!restriction.IncludeUnknowns)
|
||||||
|
{
|
||||||
|
return q.Where(s => s.AgeRating != AgeRating.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,19 @@ public static class RestrictByAgeExtensions
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<SeriesMetadataPeople> RestrictAgainstAgeRestriction(this IQueryable<SeriesMetadataPeople> queryable, AgeRestriction restriction)
|
||||||
|
{
|
||||||
|
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||||
|
var q = queryable.Where(s => s.SeriesMetadata.AgeRating <= restriction.AgeRating);
|
||||||
|
|
||||||
|
if (!restriction.IncludeUnknowns)
|
||||||
|
{
|
||||||
|
return q.Where(s => s.SeriesMetadata.AgeRating != AgeRating.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static IQueryable<Chapter> RestrictAgainstAgeRestriction(this IQueryable<Chapter> queryable, AgeRestriction restriction)
|
public static IQueryable<Chapter> RestrictAgainstAgeRestriction(this IQueryable<Chapter> queryable, AgeRestriction restriction)
|
||||||
{
|
{
|
||||||
@ -41,6 +54,19 @@ public static class RestrictByAgeExtensions
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<ChapterPeople> RestrictAgainstAgeRestriction(this IQueryable<ChapterPeople> queryable, AgeRestriction restriction)
|
||||||
|
{
|
||||||
|
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||||
|
var q = queryable.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating);
|
||||||
|
|
||||||
|
if (!restriction.IncludeUnknowns)
|
||||||
|
{
|
||||||
|
return q.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static IQueryable<AppUserCollection> RestrictAgainstAgeRestriction(this IQueryable<AppUserCollection> queryable, AgeRestriction restriction)
|
public static IQueryable<AppUserCollection> RestrictAgainstAgeRestriction(this IQueryable<AppUserCollection> queryable, AgeRestriction restriction)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Person;
|
||||||
|
|
||||||
|
namespace API.Extensions.QueryExtensions;
|
||||||
|
|
||||||
|
public static class RestrictByLibraryExtensions
|
||||||
|
{
|
||||||
|
|
||||||
|
public static IQueryable<Person> RestrictByLibrary(this IQueryable<Person> query, IQueryable<int> userLibs)
|
||||||
|
{
|
||||||
|
return query.Where(p =>
|
||||||
|
p.ChapterPeople.Any(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)) ||
|
||||||
|
p.SeriesMetadataPeople.Any(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<Chapter> RestrictByLibrary(this IQueryable<Chapter> query, IQueryable<int> userLibs)
|
||||||
|
{
|
||||||
|
return query.Where(cp => userLibs.Contains(cp.Volume.Series.LibraryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<SeriesMetadataPeople> RestrictByLibrary(this IQueryable<SeriesMetadataPeople> query, IQueryable<int> userLibs)
|
||||||
|
{
|
||||||
|
return query.Where(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<ChapterPeople> RestrictByLibrary(this IQueryable<ChapterPeople> query, IQueryable<int> userLibs)
|
||||||
|
{
|
||||||
|
return query.Where(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId));
|
||||||
|
}
|
||||||
|
}
|
@ -156,4 +156,24 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChapterBuilder WithTags(IList<Tag> tags)
|
||||||
|
{
|
||||||
|
_chapter.Tags ??= [];
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
_chapter.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterBuilder WithGenres(IList<Genre> genres)
|
||||||
|
{
|
||||||
|
_chapter.Genres ??= [];
|
||||||
|
foreach (var genre in genres)
|
||||||
|
{
|
||||||
|
_chapter.Genres.Add(genre);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,4 +108,23 @@ public class SeriesMetadataBuilder : IEntityBuilder<SeriesMetadata>
|
|||||||
_seriesMetadata.TagsLocked = lockStatus;
|
_seriesMetadata.TagsLocked = lockStatus;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SeriesMetadataBuilder WithTags(List<Tag> tags, bool lockStatus = false)
|
||||||
|
{
|
||||||
|
_seriesMetadata.Tags = tags;
|
||||||
|
_seriesMetadata.TagsLocked = lockStatus;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeriesMetadataBuilder WithMaxCount(int count)
|
||||||
|
{
|
||||||
|
_seriesMetadata.MaxCount = count;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeriesMetadataBuilder WithTotalCount(int count)
|
||||||
|
{
|
||||||
|
_seriesMetadata.TotalCount = count;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1057,6 +1057,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
var status = DeterminePublicationStatus(series, chapters, externalMetadata);
|
var status = DeterminePublicationStatus(series, chapters, externalMetadata);
|
||||||
|
|
||||||
series.Metadata.PublicationStatus = status;
|
series.Metadata.PublicationStatus = status;
|
||||||
|
series.Metadata.PublicationStatusLocked = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -1188,32 +1189,39 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
|
|
||||||
#region Rating
|
#region Rating
|
||||||
|
|
||||||
var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating);
|
// C# can't make the implicit conversation here
|
||||||
var averageUserRating = metadata.UserReviews.Average(r => r.Rating);
|
float? averageCriticRating = metadata.CriticReviews.Count > 0 ? metadata.CriticReviews.Average(r => r.Rating) : null;
|
||||||
|
float? averageUserRating = metadata.UserReviews.Count > 0 ? metadata.UserReviews.Average(r => r.Rating) : null;
|
||||||
|
|
||||||
var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id);
|
var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id);
|
||||||
_unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings);
|
_unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings);
|
||||||
|
|
||||||
chapter.ExternalRatings =
|
chapter.ExternalRatings = [];
|
||||||
[
|
|
||||||
new ExternalRating
|
if (averageUserRating != null)
|
||||||
|
{
|
||||||
|
chapter.ExternalRatings.Add(new ExternalRating
|
||||||
{
|
{
|
||||||
AverageScore = (int) averageUserRating,
|
AverageScore = (int) averageUserRating,
|
||||||
Provider = ScrobbleProvider.Cbr,
|
Provider = ScrobbleProvider.Cbr,
|
||||||
Authority = RatingAuthority.User,
|
Authority = RatingAuthority.User,
|
||||||
ProviderUrl = metadata.IssueUrl,
|
ProviderUrl = metadata.IssueUrl,
|
||||||
},
|
|
||||||
new ExternalRating
|
});
|
||||||
|
chapter.AverageExternalRating = averageUserRating.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (averageCriticRating != null)
|
||||||
|
{
|
||||||
|
chapter.ExternalRatings.Add(new ExternalRating
|
||||||
{
|
{
|
||||||
AverageScore = (int) averageCriticRating,
|
AverageScore = (int) averageCriticRating,
|
||||||
Provider = ScrobbleProvider.Cbr,
|
Provider = ScrobbleProvider.Cbr,
|
||||||
Authority = RatingAuthority.Critic,
|
Authority = RatingAuthority.Critic,
|
||||||
ProviderUrl = metadata.IssueUrl,
|
ProviderUrl = metadata.IssueUrl,
|
||||||
|
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
chapter.AverageExternalRating = averageUserRating;
|
|
||||||
|
|
||||||
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
|
madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification;
|
||||||
|
|
||||||
@ -1563,16 +1571,16 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0);
|
var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0);
|
||||||
var maxChapter = (int)chapters.Max(c => c.MaxNumber);
|
var maxChapter = (int)chapters.Max(c => c.MaxNumber);
|
||||||
|
|
||||||
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
|
if (series.Format is MangaFormat.Epub or MangaFormat.Pdf && chapters.Count == 1)
|
||||||
{
|
{
|
||||||
series.Metadata.MaxCount = 1;
|
series.Metadata.MaxCount = 1;
|
||||||
}
|
}
|
||||||
else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial)
|
else if (series.Metadata.TotalCount <= 1 && chapters is [{ IsSpecial: true }])
|
||||||
{
|
{
|
||||||
series.Metadata.MaxCount = series.Metadata.TotalCount;
|
series.Metadata.MaxCount = series.Metadata.TotalCount;
|
||||||
}
|
}
|
||||||
else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) &&
|
else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) &&
|
||||||
maxVolume <= series.Metadata.TotalCount)
|
maxVolume <= series.Metadata.TotalCount && maxVolume != Parser.DefaultChapterNumber)
|
||||||
{
|
{
|
||||||
series.Metadata.MaxCount = maxVolume;
|
series.Metadata.MaxCount = maxVolume;
|
||||||
}
|
}
|
||||||
@ -1593,8 +1601,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
{
|
{
|
||||||
status = PublicationStatus.Ended;
|
status = PublicationStatus.Ended;
|
||||||
|
|
||||||
// Check if all volumes/chapters match the total count
|
if (IsSeriesCompleted(series, chapters, externalMetadata, maxVolume))
|
||||||
if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0)
|
|
||||||
{
|
{
|
||||||
status = PublicationStatus.Completed;
|
status = PublicationStatus.Completed;
|
||||||
}
|
}
|
||||||
@ -1610,6 +1617,68 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
return PublicationStatus.OnGoing;
|
return PublicationStatus.OnGoing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the series should be marked as completed, checks loosey with chapter and series numbers.
|
||||||
|
/// Respects Specials to reach the required amount.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="series"></param>
|
||||||
|
/// <param name="chapters"></param>
|
||||||
|
/// <param name="externalMetadata"></param>
|
||||||
|
/// <param name="maxVolumes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <remarks>Updates MaxCount and TotalCount if a loosey check is used to set as completed</remarks>
|
||||||
|
public static bool IsSeriesCompleted(Series series, List<Chapter> chapters, ExternalSeriesDetailDto externalMetadata, int maxVolumes)
|
||||||
|
{
|
||||||
|
// A series is completed if exactly the amount is found
|
||||||
|
if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If volumes are collected, check if we reach the required volumes by including specials, and decimal volumes
|
||||||
|
//
|
||||||
|
// TODO BUG: If the series has specials, that are not included in the external count. But you do own them
|
||||||
|
// This may mark the series as completed pre-maturely
|
||||||
|
// Note: I've currently opted to keep this an equals to prevent the above bug from happening
|
||||||
|
// We *could* change this to >= in the future in case this is reported by users
|
||||||
|
// If we do; test IsSeriesCompleted_Volumes_TooManySpecials needs to be updated
|
||||||
|
if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == series.Volumes.Count)
|
||||||
|
{
|
||||||
|
series.Metadata.MaxCount = series.Volumes.Count;
|
||||||
|
series.Metadata.TotalCount = series.Volumes.Count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: If Kavita has specials, we should be lenient and ignore for the volume check
|
||||||
|
var volumeModifier = series.Volumes.Any(v => v.Name == Parser.SpecialVolume) ? 1 : 0;
|
||||||
|
var modifiedMinVolumeCount = series.Volumes.Count - volumeModifier;
|
||||||
|
if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == modifiedMinVolumeCount)
|
||||||
|
{
|
||||||
|
series.Metadata.MaxCount = modifiedMinVolumeCount;
|
||||||
|
series.Metadata.TotalCount = modifiedMinVolumeCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no volumes are collected, the series is completed if we reach or exceed the external chapters
|
||||||
|
if (maxVolumes == Parser.DefaultChapterNumber && series.Metadata.MaxCount >= externalMetadata.Chapters)
|
||||||
|
{
|
||||||
|
series.Metadata.TotalCount = series.Metadata.MaxCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no volumes are collected, the series is complete if we reach or exceed the external chapters while including
|
||||||
|
// prologues, and extra chapters
|
||||||
|
if (maxVolumes == Parser.DefaultChapterNumber && chapters.Count >= externalMetadata.Chapters)
|
||||||
|
{
|
||||||
|
series.Metadata.TotalCount = chapters.Count;
|
||||||
|
series.Metadata.MaxCount = chapters.Count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<MetadataFieldType, List<string>> ApplyFieldMappings(IEnumerable<string> values, MetadataFieldType sourceType, List<MetadataFieldMappingDto> mappings)
|
private static Dictionary<MetadataFieldType, List<string>> ApplyFieldMappings(IEnumerable<string> values, MetadataFieldType sourceType, List<MetadataFieldMappingDto> mappings)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<MetadataFieldType, List<string>>();
|
var result = new Dictionary<MetadataFieldType, List<string>>();
|
||||||
|
@ -215,9 +215,9 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false),
|
RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false),
|
||||||
LicenseService.Cron, RecurringJobOptions);
|
LicenseService.Cron, RecurringJobOptions);
|
||||||
|
|
||||||
// KavitaPlus Scrobbling (every hour)
|
// KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+
|
||||||
RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(),
|
RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(),
|
||||||
"0 */1 * * *", RecurringJobOptions);
|
Cron.Hourly(Rnd.Next(0, 60)), RecurringJobOptions);
|
||||||
RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(),
|
RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(),
|
||||||
Cron.Daily, RecurringJobOptions);
|
Cron.Daily, RecurringJobOptions);
|
||||||
|
|
||||||
|
@ -9,6 +9,24 @@ import {AccountService} from "./account.service";
|
|||||||
import {map} from "rxjs/operators";
|
import {map} from "rxjs/operators";
|
||||||
import {NavigationEnd, Router} from "@angular/router";
|
import {NavigationEnd, Router} from "@angular/router";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
|
import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component";
|
||||||
|
import {WikiLink} from "../_models/wiki";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NavItem used to construct the dropdown or NavLinkModal on mobile
|
||||||
|
* Priority construction
|
||||||
|
* @param routerLink A link to a page on the web app, takes priority
|
||||||
|
* @param fragment Optional fragment for routerLink
|
||||||
|
* @param href A link to an external page, must set noopener noreferrer
|
||||||
|
* @param click Callback, lowest priority. Should only be used if routerLink and href or not set
|
||||||
|
*/
|
||||||
|
interface NavItem {
|
||||||
|
transLocoKey: string;
|
||||||
|
href?: string;
|
||||||
|
fragment?: string;
|
||||||
|
routerLink?: string;
|
||||||
|
click?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -21,6 +39,33 @@ export class NavService {
|
|||||||
|
|
||||||
public localStorageSideNavKey = 'kavita--sidenav--expanded';
|
public localStorageSideNavKey = 'kavita--sidenav--expanded';
|
||||||
|
|
||||||
|
public navItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
transLocoKey: 'all-filters',
|
||||||
|
routerLink: '/all-filters/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transLocoKey: 'browse-genres',
|
||||||
|
routerLink: '/browse/genres',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transLocoKey: 'browse-tags',
|
||||||
|
routerLink: '/browse/tags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transLocoKey: 'announcements',
|
||||||
|
routerLink: '/announcements/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transLocoKey: 'help',
|
||||||
|
href: WikiLink.Guides,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transLocoKey: 'logout',
|
||||||
|
click: () => this.logout(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
private navbarVisibleSource = new ReplaySubject<boolean>(1);
|
private navbarVisibleSource = new ReplaySubject<boolean>(1);
|
||||||
/**
|
/**
|
||||||
* If the top Nav bar is rendered or not
|
* If the top Nav bar is rendered or not
|
||||||
@ -127,6 +172,13 @@ export class NavService {
|
|||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.accountService.logout();
|
||||||
|
this.hideNavBar();
|
||||||
|
this.hideSideNav();
|
||||||
|
this.router.navigateByUrl('/login');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state.
|
* Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state.
|
||||||
*/
|
*/
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
[sorts]="[{prop: 'createdUtc', dir: 'desc'}]"
|
[sorts]="[{prop: 'createdUtc', dir: 'desc'}]"
|
||||||
>
|
>
|
||||||
|
|
||||||
<ngx-datatable-column prop="select" [sortable]="false" [draggable]="false" [resizeable]="false" [width]="50">
|
<ngx-datatable-column prop="select" [sortable]="false" [draggable]="false" [resizeable]="false">
|
||||||
<ng-template let-column="column" ngx-datatable-header-template>
|
<ng-template let-column="column" ngx-datatable-header-template>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input id="select-all" type="checkbox" class="form-check-input"
|
<input id="select-all" type="checkbox" class="form-check-input"
|
||||||
|
@ -190,7 +190,7 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||||||
destinationValue: value.destinationValue,
|
destinationValue: value.destinationValue,
|
||||||
excludeFromSource: value.excludeFromSource
|
excludeFromSource: value.excludeFromSource
|
||||||
}
|
}
|
||||||
}).filter(m => m.sourceValue.length > 0);
|
}).filter(m => m.sourceValue.length > 0 && m.destinationValue.length > 0);
|
||||||
|
|
||||||
// Translate blacklist string -> Array<string>
|
// Translate blacklist string -> Array<string>
|
||||||
return {
|
return {
|
||||||
@ -231,15 +231,6 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||||||
excludeFromSource: [mapping?.excludeFromSource || false]
|
excludeFromSource: [mapping?.excludeFromSource || false]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Autofill destination value if empty when source value loses focus
|
|
||||||
mappingGroup.get('sourceValue')?.valueChanges
|
|
||||||
.pipe(
|
|
||||||
filter(() => !mappingGroup.get('destinationValue')?.value)
|
|
||||||
)
|
|
||||||
.subscribe(sourceValue => {
|
|
||||||
mappingGroup.get('destinationValue')?.setValue(sourceValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.fieldMappings.push(mappingGroup);
|
this.fieldMappings.push(mappingGroup);
|
||||||
}
|
}
|
||||||
|
@ -205,12 +205,15 @@
|
|||||||
<span class="d-none d-xs-none d-sm-none d-md-inline-block fw-bold">{{user.username | sentenceCase}}</span>
|
<span class="d-none d-xs-none d-sm-none d-md-inline-block fw-bold">{{user.username | sentenceCase}}</span>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu>
|
<div ngbDropdownMenu>
|
||||||
<a ngbDropdownItem routerLink="/all-filters/">{{t('all-filters')}}</a>
|
@for (navItem of navService.navItems; track $index) {
|
||||||
<a ngbDropdownItem routerLink="/browse/genres">{{t('browse-genres')}}</a>
|
@if (navItem.routerLink) {
|
||||||
<a ngbDropdownItem routerLink="/browse/tags">{{t('browse-tags')}}</a>
|
<a ngbDropdownItem [routerLink]="navItem.routerLink" [fragment]="navItem.fragment">{{t(navItem.transLocoKey)}}</a>
|
||||||
<a ngbDropdownItem routerLink="/announcements/">{{t('announcements')}}</a>
|
} @else if (navItem.href) {
|
||||||
<a ngbDropdownItem [href]="WikiLink.Guides" rel="noopener noreferrer" target="_blank">{{t('help')}}</a>
|
<a ngbDropdownItem [href]="navItem.href" rel="noopener noreferrer" target="_blank">{{t(navItem.transLocoKey)}}</a>
|
||||||
<a ngbDropdownItem (click)="logout()">{{t('logout')}}</a>
|
} @else if (navItem.click) {
|
||||||
|
<a ngbDropdownItem (click)="navItem.click()">{{t(navItem.transLocoKey)}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -134,13 +134,6 @@ export class NavHeaderComponent implements OnInit {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.accountService.logout();
|
|
||||||
this.navService.hideNavBar();
|
|
||||||
this.navService.hideSideNav();
|
|
||||||
this.router.navigateByUrl('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
moveFocus() {
|
moveFocus() {
|
||||||
this.document.getElementById('content')?.focus();
|
this.document.getElementById('content')?.focus();
|
||||||
}
|
}
|
||||||
@ -253,7 +246,6 @@ export class NavHeaderComponent implements OnInit {
|
|||||||
|
|
||||||
openLinkSelectionMenu() {
|
openLinkSelectionMenu() {
|
||||||
const ref = this.modalService.open(NavLinkModalComponent, {fullscreen: 'sm'});
|
const ref = this.modalService.open(NavLinkModalComponent, {fullscreen: 'sm'});
|
||||||
ref.componentInstance.logoutFn = this.logout.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,21 +6,22 @@
|
|||||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<a routerLink="/settings" [fragment]="SettingsTabId.Preferences" (click)="closeIfOnSettings()" [title]="t('settings')">{{t('settings')}}</a>
|
<a routerLink="/settings" [fragment]="SettingsTabId.Preferences" (click)="closeIfOnSettings()" [title]="t('settings')">{{t('settings')}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<a routerLink="/all-filters/">{{t('all-filters')}}</a>
|
@for (navItem of navService.navItems; track $index) {
|
||||||
</div>
|
<div class="mb-3">
|
||||||
<div class="mb-3">
|
@if (navItem.routerLink) {
|
||||||
<a routerLink="/announcements/">{{t('announcements')}}</a>
|
<a [routerLink]="navItem.routerLink" [fragment]="navItem.fragment">{{t(navItem.transLocoKey)}}</a>
|
||||||
</div>
|
} @else if (navItem.href) {
|
||||||
<div class="mb-3">
|
<a [href]="navItem.href" rel="noopener noreferrer" target="_blank">{{t(navItem.transLocoKey)}}</a>
|
||||||
<a [href]="WikiLink.Guides" rel="noopener noreferrer" target="_blank">{{t('help')}}</a>
|
} @else if (navItem.click) {
|
||||||
</div>
|
<a href="javascript:void(0);" (click)="navItem.click()">{{t(navItem.transLocoKey)}}</a>
|
||||||
<div class="mb-3">
|
}
|
||||||
<a href="javascript:void(0);" (click)="logout()">{{t('logout')}}</a>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||||
|
@ -5,6 +5,7 @@ import {Router, RouterLink} from "@angular/router";
|
|||||||
import {ReactiveFormsModule} from "@angular/forms";
|
import {ReactiveFormsModule} from "@angular/forms";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {SettingsTabId} from "../../../sidenav/preference-nav/preference-nav.component";
|
import {SettingsTabId} from "../../../sidenav/preference-nav/preference-nav.component";
|
||||||
|
import {NavService} from "../../../_services/nav.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-link-modal',
|
selector: 'app-nav-link-modal',
|
||||||
@ -25,17 +26,12 @@ export class NavLinkModalComponent {
|
|||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly modal = inject(NgbActiveModal);
|
private readonly modal = inject(NgbActiveModal);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
protected readonly navService = inject(NavService);
|
||||||
@Input({required: true}) logoutFn!: () => void;
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.logoutFn();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeIfOnSettings() {
|
closeIfOnSettings() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentUrl = this.router.url;
|
const currentUrl = this.router.url;
|
||||||
|
@ -187,9 +187,8 @@ export class PersonDetailComponent implements OnInit {
|
|||||||
filter.combination = FilterCombination.Or;
|
filter.combination = FilterCombination.Or;
|
||||||
filter.limitTo = 20;
|
filter.limitTo = 20;
|
||||||
|
|
||||||
// I might want to use roles$ to do all this
|
roles.forEach(pr => {
|
||||||
allPeople.forEach(f => {
|
filter.statements.push({comparison: FilterComparison.Contains, value: this.person!.id + '', field: personRoleForFilterField(pr)});
|
||||||
filter.statements.push({comparison: FilterComparison.Contains, value: this.person!.id + '', field: f});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user