Collection Rework (#2830)
This commit is contained in:
parent
0dacc061f1
commit
deaaccb96a
93 changed files with 5413 additions and 1120 deletions
|
@ -36,6 +36,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
|
||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
|
||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
|
||||
[Obsolete]
|
||||
public DbSet<CollectionTag> CollectionTag { get; set; } = null!;
|
||||
public DbSet<AppUserBookmark> AppUserBookmark { get; set; } = null!;
|
||||
public DbSet<ReadingList> ReadingList { get; set; } = null!;
|
||||
|
@ -64,6 +65,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
|
||||
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
|
||||
public DbSet<SeriesBlacklist> SeriesBlacklist { get; set; } = null!;
|
||||
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
@ -149,6 +151,10 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
.WithOne(s => s.ExternalSeriesMetadata)
|
||||
.HasForeignKey<ExternalSeriesMetadata>(em => em.SeriesId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<AppUserCollection>()
|
||||
.Property(b => b.AgeRating)
|
||||
.HasDefaultValue(AgeRating.Unknown);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.8.0 refactored User Collections
|
||||
/// </summary>
|
||||
public static class MigrateCollectionTagToUserCollections
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateCollectionTagToUserCollections"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateCollectionTagToUserCollections migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
// Find the first user that is an admin
|
||||
var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections);
|
||||
if (defaultAdmin == null)
|
||||
{
|
||||
await CompleteMigration(dataContext, logger);
|
||||
return;
|
||||
}
|
||||
|
||||
// For all collectionTags, move them over to said user
|
||||
var existingCollections = await dataContext.CollectionTag
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.Includes(CollectionTagIncludes.SeriesMetadataWithSeries)
|
||||
.ToListAsync();
|
||||
foreach (var existingCollectionTag in existingCollections)
|
||||
{
|
||||
var collection = new AppUserCollection()
|
||||
{
|
||||
Title = existingCollectionTag.Title,
|
||||
NormalizedTitle = existingCollectionTag.Title.Normalize(),
|
||||
CoverImage = existingCollectionTag.CoverImage,
|
||||
CoverImageLocked = existingCollectionTag.CoverImageLocked,
|
||||
Promoted = existingCollectionTag.Promoted,
|
||||
AgeRating = AgeRating.Unknown,
|
||||
Summary = existingCollectionTag.Summary,
|
||||
Items = existingCollectionTag.SeriesMetadatas.Select(s => s.Series).ToList()
|
||||
};
|
||||
|
||||
collection.AgeRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(collection.Items.Select(s => s.Id));
|
||||
defaultAdmin.Collections.Add(collection);
|
||||
}
|
||||
unitOfWork.UserRepository.Update(defaultAdmin);
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
await CompleteMigration(dataContext, logger);
|
||||
}
|
||||
|
||||
private static async Task CompleteMigration(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateCollectionTagToUserCollections",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateCollectionTagToUserCollections migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
3019
API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs
generated
Normal file
3019
API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
92
API/Data/Migrations/20240331172900_UserBasedCollections.cs
Normal file
92
API/Data/Migrations/20240331172900_UserBasedCollections.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UserBasedCollections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserCollection",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
NormalizedTitle = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Summary = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Promoted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CoverImage = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CoverImageLocked = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AgeRating = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastSyncUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Source = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SourceUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserCollection", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserCollection_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserCollectionSeries",
|
||||
columns: table => new
|
||||
{
|
||||
CollectionsId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ItemsId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserCollectionSeries", x => new { x.CollectionsId, x.ItemsId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserCollectionSeries_AppUserCollection_CollectionsId",
|
||||
column: x => x.CollectionsId,
|
||||
principalTable: "AppUserCollection",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserCollectionSeries_Series_ItemsId",
|
||||
column: x => x.ItemsId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserCollection_AppUserId",
|
||||
table: "AppUserCollection",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserCollectionSeries_ItemsId",
|
||||
table: "AppUserCollectionSeries",
|
||||
column: "ItemsId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserCollectionSeries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserCollection");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -189,6 +189,66 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AppUserBookmark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserCollection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AgeRating")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModifiedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSyncUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1918,6 +1978,21 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserCollectionSeries", b =>
|
||||
{
|
||||
b.Property<int>("CollectionsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CollectionsId", "ItemsId");
|
||||
|
||||
b.HasIndex("ItemsId");
|
||||
|
||||
b.ToTable("AppUserCollectionSeries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
{
|
||||
b.Property<int>("AppUsersId")
|
||||
|
@ -2178,6 +2253,17 @@ namespace API.Data.Migrations
|
|||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserCollection", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("Collections")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
@ -2626,6 +2712,21 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserCollectionSeries", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUserCollection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CollectionsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
|
@ -2836,6 +2937,8 @@ namespace API.Data.Migrations
|
|||
{
|
||||
b.Navigation("Bookmarks");
|
||||
|
||||
b.Navigation("Collections");
|
||||
|
||||
b.Navigation("DashboardStreams");
|
||||
|
||||
b.Navigation("Devices");
|
||||
|
|
|
@ -3,43 +3,61 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.DTOs.Collection;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum CollectionTagIncludes
|
||||
{
|
||||
None = 1,
|
||||
SeriesMetadata = 2,
|
||||
SeriesMetadataWithSeries = 4
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CollectionIncludes
|
||||
{
|
||||
None = 1,
|
||||
Series = 2,
|
||||
}
|
||||
|
||||
public interface ICollectionTagRepository
|
||||
{
|
||||
void Add(CollectionTag tag);
|
||||
void Remove(CollectionTag tag);
|
||||
Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync();
|
||||
Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId);
|
||||
void Remove(AppUserCollection tag);
|
||||
Task<string?> GetCoverImageAsync(int collectionTagId);
|
||||
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId);
|
||||
Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
void Update(CollectionTag tag);
|
||||
Task<int> RemoveTagsWithoutSeries();
|
||||
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None);
|
||||
void Update(AppUserCollection tag);
|
||||
Task<int> RemoveCollectionsWithoutSeries();
|
||||
|
||||
Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None);
|
||||
/// <summary>
|
||||
/// Returns all of the user's collections with the option of other user's promoted
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="includePromoted"></param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false);
|
||||
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false);
|
||||
|
||||
Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles,
|
||||
CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> TagExists(string title);
|
||||
Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<bool> CollectionExists(string title, int userId);
|
||||
Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<IList<string>> GetRandomCoverImagesAsync(int collectionId);
|
||||
Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None);
|
||||
Task UpdateCollectionAgeRating(AppUserCollection tag);
|
||||
Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None);
|
||||
}
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
|
@ -52,17 +70,12 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Add(CollectionTag tag)
|
||||
public void Remove(AppUserCollection tag)
|
||||
{
|
||||
_context.CollectionTag.Add(tag);
|
||||
_context.AppUserCollection.Remove(tag);
|
||||
}
|
||||
|
||||
public void Remove(CollectionTag tag)
|
||||
{
|
||||
_context.CollectionTag.Remove(tag);
|
||||
}
|
||||
|
||||
public void Update(CollectionTag tag)
|
||||
public void Update(AppUserCollection tag)
|
||||
{
|
||||
_context.Entry(tag).State = EntityState.Modified;
|
||||
}
|
||||
|
@ -70,38 +83,53 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
/// <summary>
|
||||
/// Removes any collection tags without any series
|
||||
/// </summary>
|
||||
public async Task<int> RemoveTagsWithoutSeries()
|
||||
public async Task<int> RemoveCollectionsWithoutSeries()
|
||||
{
|
||||
var tagsToDelete = await _context.CollectionTag
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.Where(c => c.SeriesMetadatas.Count == 0)
|
||||
var tagsToDelete = await _context.AppUserCollection
|
||||
.Include(c => c.Items)
|
||||
.Where(c => c.Items.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
_context.RemoveRange(tagsToDelete);
|
||||
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.Includes(includes)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles, CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => normalizedTitles.Contains(c.NormalizedTitle))
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.Includes(includes)
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
|
||||
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
|
||||
.OrderBy(uc => uc.Title)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
|
||||
.Where(uc => uc.Items.Any(s => s.Id == seriesId))
|
||||
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
|
||||
.OrderBy(uc => uc.Title)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> GetCoverImageAsync(int collectionTagId)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.Id == collectionTagId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
|
@ -109,12 +137,13 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return (await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Select(t => t.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync())!;
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
[Obsolete("use TagExists with userId")]
|
||||
public async Task<bool> TagExists(string title)
|
||||
{
|
||||
var normalized = title.ToNormalized();
|
||||
|
@ -122,10 +151,24 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
/// <summary>
|
||||
/// If any tag exists for that given user's collections
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> CollectionExists(string title, int userId)
|
||||
{
|
||||
var normalized = title.ToNormalized();
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId)
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
@ -139,12 +182,41 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.Select(sm => sm.Series.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.OrderBy(_ => random.Next())
|
||||
.Take(4)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.AppUserId == userId)
|
||||
.Includes(includes)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateCollectionAgeRating(AppUserCollection tag)
|
||||
{
|
||||
var maxAgeRating = await _context.AppUserCollection
|
||||
.Where(t => t.Id == tag.Id)
|
||||
.SelectMany(uc => uc.Items.Select(s => s.Metadata))
|
||||
.Select(sm => sm.AgeRating)
|
||||
.MaxAsync();
|
||||
tag.AgeRating = maxAgeRating;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => tags.Contains(c.Id))
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
|
||||
|
@ -168,9 +240,9 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
}
|
||||
|
||||
|
||||
public async Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.Id == tagId)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
|
@ -190,16 +262,12 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId)
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> SearchTagDtosAsync(string searchQuery, int userId)
|
||||
{
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
return await _context.CollectionTag
|
||||
.Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
return await _context.AppUserCollection
|
||||
.Search(searchQuery, userId, userRating)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.Constants;
|
|||
using API.Data.Misc;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Collection;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering;
|
||||
|
@ -141,7 +142,7 @@ public interface ISeriesRepository
|
|||
MangaFormat format);
|
||||
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
||||
Task<AgeRating?> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
|
||||
Task<AgeRating> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
|
||||
/// <summary>
|
||||
/// This is only used for <see cref="MigrateUserProgressLibraryId"/>
|
||||
/// </summary>
|
||||
|
@ -342,10 +343,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await _context.Library.GetUserLibraries(userId, queryContext).ToListAsync();
|
||||
}
|
||||
|
||||
return new List<int>()
|
||||
{
|
||||
libraryId
|
||||
};
|
||||
return [libraryId];
|
||||
}
|
||||
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, IList<int> libraryIds, string searchQuery)
|
||||
|
@ -362,12 +360,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
.ToList();
|
||||
|
||||
result.Libraries = await _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.IsRestricted(QueryContext.Search)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.Name.ToLower())
|
||||
.Search(searchQuery, userId, libraryIds)
|
||||
.Take(maxRecords)
|
||||
.OrderBy(l => l.Name.ToLower())
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -419,53 +414,33 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
|
||||
result.ReadingLists = await _context.ReadingList
|
||||
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
||||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(r => r.NormalizedTitle)
|
||||
.Search(searchQuery, userId, userRating)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Collections = await _context.CollectionTag
|
||||
.Where(c => (EF.Functions.Like(c.Title, $"%{searchQuery}%"))
|
||||
|| (EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%")))
|
||||
.Where(c => c.Promoted || isAdmin)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsSplitQuery()
|
||||
result.Collections = await _context.AppUserCollection
|
||||
.Search(searchQuery, userId, userRating)
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Persons = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.SearchPeople(searchQuery, seriesIds)
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedName)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Genres = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.SearchGenres(searchQuery, seriesIds)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
result.Tags = await _context.SeriesMetadata
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.SearchTags(searchQuery, seriesIds)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
@ -740,6 +715,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task AddSeriesModifiers(int userId, IList<SeriesDto> series)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
|
@ -968,6 +944,20 @@ public class SeriesRepository : ISeriesRepository
|
|||
out var seriesIds, out var hasAgeRating, out var hasTagsFilter, out var hasLanguageFilter,
|
||||
out var hasPublicationFilter, out var hasSeriesNameFilter, out var hasReleaseYearMinFilter, out var hasReleaseYearMaxFilter);
|
||||
|
||||
IList<int> collectionSeries = [];
|
||||
if (hasCollectionTagFilter)
|
||||
{
|
||||
collectionSeries = await _context.AppUserCollection
|
||||
.Where(uc => uc.Promoted || uc.AppUserId == userId)
|
||||
.Where(uc => filter.CollectionTags.Contains(uc.Id))
|
||||
.SelectMany(uc => uc.Items)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Select(s => s.Id)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.AsNoTracking()
|
||||
// This new style can handle any filterComparision coming from the user
|
||||
|
@ -979,7 +969,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
.HasAgeRating(hasAgeRating, FilterComparison.Contains, filter.AgeRating)
|
||||
.HasPublicationStatus(hasPublicationFilter, FilterComparison.Contains, filter.PublicationStatus)
|
||||
.HasTags(hasTagsFilter, FilterComparison.Contains, filter.Tags)
|
||||
.HasCollectionTags(hasCollectionTagFilter, FilterComparison.Contains, filter.Tags)
|
||||
.HasCollectionTags(hasCollectionTagFilter, FilterComparison.Contains, filter.Tags, collectionSeries)
|
||||
.HasGenre(hasGenresFilter, FilterComparison.Contains, filter.Genres)
|
||||
.HasFormat(filter.Formats != null && filter.Formats.Count > 0, FilterComparison.Contains, filter.Formats!)
|
||||
.HasAverageReadTime(true, FilterComparison.GreaterThanEqual, 0)
|
||||
|
@ -1045,6 +1035,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Select(u => u.CollapseSeriesRelationships)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
|
||||
|
||||
query ??= _context.Series
|
||||
.AsNoTracking();
|
||||
|
||||
|
@ -1062,6 +1054,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
query = ApplyWantToReadFilter(filter, query, userId);
|
||||
|
||||
|
||||
query = await ApplyCollectionFilter(filter, query, userId, userRating);
|
||||
|
||||
|
||||
query = BuildFilterQuery(userId, filter, query);
|
||||
|
||||
|
||||
|
@ -1078,6 +1073,50 @@ public class SeriesRepository : ISeriesRepository
|
|||
.AsSplitQuery(), filter.LimitTo);
|
||||
}
|
||||
|
||||
private async Task<IQueryable<Series>> ApplyCollectionFilter(FilterV2Dto filter, IQueryable<Series> query, int userId, AgeRestriction userRating)
|
||||
{
|
||||
var collectionStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.CollectionTags);
|
||||
if (collectionStmt == null) return query;
|
||||
|
||||
var value = (IList<int>) FilterFieldValueConverter.ConvertValue(collectionStmt.Field, collectionStmt.Value);
|
||||
|
||||
if (value.Count == 0)
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
var collectionSeries = await _context.AppUserCollection
|
||||
.Where(uc => uc.Promoted || uc.AppUserId == userId)
|
||||
.Where(uc => value.Contains(uc.Id))
|
||||
.SelectMany(uc => uc.Items)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Select(s => s.Id)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
if (collectionStmt.Comparison != FilterComparison.MustContains)
|
||||
return query.HasCollectionTags(true, collectionStmt.Comparison, value, collectionSeries);
|
||||
|
||||
var collectionSeriesTasks = value.Select(async collectionId =>
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.Promoted || uc.AppUserId == userId)
|
||||
.Where(uc => uc.Id == collectionId)
|
||||
.SelectMany(uc => uc.Items)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var collectionSeriesLists = await Task.WhenAll(collectionSeriesTasks);
|
||||
|
||||
// Find the common series among all collections
|
||||
var commonSeries = collectionSeriesLists.Aggregate((common, next) => common.Intersect(next).ToList());
|
||||
|
||||
// Filter the original query based on the common series
|
||||
return query.Where(s => commonSeries.Contains(s.Id));
|
||||
}
|
||||
|
||||
private IQueryable<Series> ApplyWantToReadFilter(FilterV2Dto filter, IQueryable<Series> query, int userId)
|
||||
{
|
||||
var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead);
|
||||
|
@ -1175,7 +1214,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
FilterField.AgeRating => query.HasAgeRating(true, statement.Comparison, (IList<AgeRating>) value),
|
||||
FilterField.UserRating => query.HasRating(true, statement.Comparison, (int) value, userId),
|
||||
FilterField.Tags => query.HasTags(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.CollectionTags => query.HasCollectionTags(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Translators => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Characters => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Publisher => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
|
@ -1190,6 +1228,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.CollectionTags =>
|
||||
// This is handled in the code before this as it's handled in a more general, combined manner
|
||||
query,
|
||||
FilterField.Libraries =>
|
||||
// This is handled in the code before this as it's handled in a more general, combined manner
|
||||
query,
|
||||
|
@ -1241,7 +1282,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
public async Task<SeriesMetadataDto?> GetSeriesMetadata(int seriesId)
|
||||
{
|
||||
var metadataDto = await _context.SeriesMetadata
|
||||
return await _context.SeriesMetadata
|
||||
.Where(metadata => metadata.SeriesId == seriesId)
|
||||
.Include(m => m.Genres.OrderBy(g => g.NormalizedTitle))
|
||||
.Include(m => m.Tags.OrderBy(g => g.NormalizedTitle))
|
||||
|
@ -1250,42 +1291,20 @@ public class SeriesRepository : ISeriesRepository
|
|||
.ProjectTo<SeriesMetadataDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (metadataDto != null)
|
||||
{
|
||||
metadataDto.CollectionTags = await _context.CollectionTag
|
||||
.Include(t => t.SeriesMetadatas)
|
||||
.Where(t => t.SeriesMetadatas.Select(s => s.SeriesId).Contains(seriesId))
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Title.ToLower())
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
return metadataDto;
|
||||
}
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams)
|
||||
{
|
||||
var userLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
var userLibraries = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
var query = _context.CollectionTag
|
||||
var query = _context.AppUserCollection
|
||||
.Where(s => s.Id == collectionId)
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.ThenInclude(m => m.Series)
|
||||
.SelectMany(c => c.SeriesMetadatas.Select(sm => sm.Series).Where(s => userLibraries.Contains(s.LibraryId)))
|
||||
.Include(c => c.Items)
|
||||
.SelectMany(c => c.Items.Where(s => userLibraries.Contains(s.LibraryId)))
|
||||
.OrderBy(s => s.LibraryId)
|
||||
.ThenBy(s => s.SortName.ToLower())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
.AsSplitQuery();
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
@ -2072,18 +2091,20 @@ public class SeriesRepository : ISeriesRepository
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the highest Age Rating for a list of Series
|
||||
/// Returns the highest Age Rating for a list of Series. Defaults to <see cref="AgeRating.Unknown"/>
|
||||
/// </summary>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AgeRating?> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds)
|
||||
public async Task<AgeRating> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds)
|
||||
{
|
||||
return await _context.Series
|
||||
var ret = await _context.Series
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
.Include(s => s.Metadata)
|
||||
.Select(s => s.Metadata.AgeRating)
|
||||
.OrderBy(s => s)
|
||||
.LastOrDefaultAsync();
|
||||
if (ret == null) return AgeRating.Unknown;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -38,7 +38,8 @@ public enum AppUserIncludes
|
|||
SmartFilters = 1024,
|
||||
DashboardStreams = 2048,
|
||||
SideNavStreams = 4096,
|
||||
ExternalSources = 8192 // 2^13
|
||||
ExternalSources = 8192,
|
||||
Collections = 16384 // 2^14
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
@ -57,6 +58,7 @@ public interface IUserRepository
|
|||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser? user);
|
||||
Task<IList<string>> GetRoles(int userId);
|
||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||
|
@ -78,7 +80,7 @@ public interface IUserRepository
|
|||
Task<bool> HasAccessToSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||
Task<AppUser?> GetUserByConfirmationToken(string token);
|
||||
Task<AppUser> GetDefaultAdminUser();
|
||||
Task<AppUser> GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None);
|
||||
Task<IEnumerable<AppUserRating>> GetSeriesWithRatings(int userId);
|
||||
Task<IEnumerable<AppUserRating>> GetSeriesWithReviews(int userId);
|
||||
Task<bool> HasHoldOnSeries(int userId, int seriesId);
|
||||
|
@ -298,11 +300,13 @@ public class UserRepository : IUserRepository
|
|||
/// Returns the first admin account created
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetDefaultAdminUser()
|
||||
public async Task<AppUser> GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None)
|
||||
{
|
||||
return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole))
|
||||
return await _context.AppUser
|
||||
.Includes(includes)
|
||||
.Where(u => u.UserRoles.Any(r => r.Role.Name == PolicyConstants.AdminRole))
|
||||
.OrderBy(u => u.Created)
|
||||
.First();
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserRating>> GetSeriesWithRatings(int userId)
|
||||
|
@ -482,7 +486,7 @@ public class UserRepository : IUserRepository
|
|||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc);
|
||||
}
|
||||
|
||||
public async Task<bool> IsUserAdminAsync(AppUser? user)
|
||||
|
@ -491,6 +495,14 @@ public class UserRepository : IUserRepository
|
|||
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetRoles(int userId)
|
||||
{
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null || _userManager == null) return ArraySegment<string>.Empty; // userManager is null on Unit Tests only
|
||||
|
||||
return await _userManager.GetRolesAsync(user);
|
||||
}
|
||||
|
||||
public async Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId)
|
||||
{
|
||||
return await _context.AppUserRating
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue