Kavita/API/Extensions/QueryableExtensions.cs
Joe Milazzo 3f24dc7392
Reading List Fixes (#1784)
* Add ability to save readinglist comicinfo fields in Chapter.

* Added the appropriate fields and migration for Reading List generation.

* Started the reading list code

* Started building out the CBL import code with some initial unit tests.

* Fixed first unit test

* Started refactoring control code into services and writing unit tests for ReadingLists. Found a logic issue around reading list title between create/update. Will be corrected in this branch with unit tests.

* Can't figure out how to mock UserManager, so had to uncomment a few tests.

* Tooltip for total pages read shows the full number

* Tweaked the math a bit for average reading per week.

* Fixed up the reading list unit tests. Fixed an issue where when inserting chapters into a blank reading list, the initial reading list item would have an order of 1 instead of 0.

* Cleaned up the code to allow the reading list code to be localized easily and fixed up a bug in last PR.

* Fixed a sorting issue on reading activity

* Tweaked the code around reading list actionables not showing due to some weird filter.

* Fixed edit library settings not opening on library detail page

* Fixed a bug where reading activity dates would be out of order due to a bug in how charts works. A temp hack has been added.

* Disable promotion in edit reading list modal since non-admins can (and should have) been able to use it.

* Fixed a bug where non-admins couldn't update their OWN reading lists. Made uploading a cover image for readinglists now check against the user's reading list access to allow non-admin's to set images.

* Fixed an issue introduced earlier in PR where adding chapters to reading list could cause order to get skewed.

* Fixed another regression from earlier commit

* Hooked in Import CBL flow. No functionality yet.

* Code is a mess. Shifting how the whole import process is going to be done. Commiting so I can pivot drastically.

* Very rough code for first step is done.

* Ui has started, I've run out of steam for this feature.

* Cleaned up the UI code a bit to make the step tracker nature easier without a dedicated component.

* Much flow implementation and tweaking to how validation checks and what is sent back.

* Removed import via cbl code as it's not done. Pushing to next release.
2023-02-12 08:20:51 -08:00

238 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions;
public static class QueryableExtensions
{
public static IQueryable<Series> RestrictAgainstAgeRestriction(this IQueryable<Series> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(s => s.Metadata.AgeRating <= restriction.AgeRating);
if (!restriction.IncludeUnknowns)
{
return q.Where(s => s.Metadata.AgeRating != AgeRating.Unknown);
}
return q;
}
public static IQueryable<CollectionTag> RestrictAgainstAgeRestriction(this IQueryable<CollectionTag> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Genre> RestrictAgainstAgeRestriction(this IQueryable<Genre> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Tag> RestrictAgainstAgeRestriction(this IQueryable<Tag> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<Person> RestrictAgainstAgeRestriction(this IQueryable<Person> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
if (restriction.IncludeUnknowns)
{
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating));
}
return queryable.Where(c => c.SeriesMetadatas.All(sm =>
sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown));
}
public static IQueryable<ReadingList> RestrictAgainstAgeRestriction(this IQueryable<ReadingList> queryable, AgeRestriction restriction)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(rl => rl.AgeRating <= restriction.AgeRating);
if (!restriction.IncludeUnknowns)
{
return q.Where(rl => rl.AgeRating != AgeRating.Unknown);
}
return q;
}
public static Task<AgeRestriction> GetUserAgeRestriction(this DbSet<AppUser> queryable, int userId)
{
if (userId < 1)
{
return Task.FromResult(new AgeRestriction()
{
AgeRating = AgeRating.NotApplicable,
IncludeUnknowns = true
});
}
return queryable
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u =>
new AgeRestriction(){
AgeRating = u.AgeRestriction,
IncludeUnknowns = u.AgeRestrictionIncludeUnknowns
})
.SingleAsync();
}
public static IQueryable<CollectionTag> Includes(this IQueryable<CollectionTag> queryable,
CollectionTagIncludes includes)
{
if (includes.HasFlag(CollectionTagIncludes.SeriesMetadata))
{
queryable = queryable.Include(c => c.SeriesMetadatas);
}
return queryable.AsSplitQuery();
}
public static IQueryable<Chapter> Includes(this IQueryable<Chapter> queryable,
ChapterIncludes includes)
{
if (includes.HasFlag(ChapterIncludes.Volumes))
{
queryable = queryable.Include(v => v.Volume);
}
if (includes.HasFlag(ChapterIncludes.Files))
{
queryable = queryable
.Include(c => c.Files);
}
return queryable.AsSplitQuery();
}
public static IQueryable<Series> Includes(this IQueryable<Series> query,
SeriesIncludes includeFlags)
{
if (includeFlags.HasFlag(SeriesIncludes.Library))
{
query = query.Include(u => u.Library);
}
if (includeFlags.HasFlag(SeriesIncludes.Volumes))
{
query = query.Include(s => s.Volumes);
}
if (includeFlags.HasFlag(SeriesIncludes.Chapters))
{
query = query
.Include(s => s.Volumes)
.ThenInclude(v => v.Chapters);
}
if (includeFlags.HasFlag(SeriesIncludes.Related))
{
query = query.Include(s => s.Relations)
.ThenInclude(r => r.TargetSeries)
.Include(s => s.RelationOf);
}
if (includeFlags.HasFlag(SeriesIncludes.Metadata))
{
query = query.Include(s => s.Metadata)
.ThenInclude(m => m.CollectionTags.OrderBy(g => g.NormalizedTitle))
.Include(s => s.Metadata)
.ThenInclude(m => m.Genres.OrderBy(g => g.NormalizedTitle))
.Include(s => s.Metadata)
.ThenInclude(m => m.People)
.Include(s => s.Metadata)
.ThenInclude(m => m.Tags.OrderBy(g => g.NormalizedTitle));
}
return query.AsSplitQuery();
}
/// <summary>
/// Applies restriction based on if the Library has restrictions (like include in search)
/// </summary>
/// <param name="query"></param>
/// <param name="context"></param>
/// <returns></returns>
public static IQueryable<Library> IsRestricted(this IQueryable<Library> query, QueryContext context)
{
if (context.HasFlag(QueryContext.None)) return query;
if (context.HasFlag(QueryContext.Dashboard))
{
query = query.Where(l => l.IncludeInDashboard);
}
if (context.HasFlag(QueryContext.Recommended))
{
query = query.Where(l => l.IncludeInRecommended);
}
if (context.HasFlag(QueryContext.Search))
{
query = query.Where(l => l.IncludeInSearch);
}
return query;
}
/// <summary>
/// Returns all libraries for a given user
/// </summary>
/// <param name="library"></param>
/// <param name="userId"></param>
/// <param name="queryContext"></param>
/// <returns></returns>
public static IQueryable<int> GetUserLibraries(this IQueryable<Library> library, int userId, QueryContext queryContext = QueryContext.None)
{
return library
.Include(l => l.AppUsers)
.Where(lib => lib.AppUsers.Any(user => user.Id == userId))
.IsRestricted(queryContext)
.AsNoTracking()
.AsSplitQuery()
.Select(lib => lib.Id);
}
public static IEnumerable<DateTime> Range(this DateTime startDate, int numberOfDays) =>
Enumerable.Range(0, numberOfDays).Select(e => startDate.AddDays(e));
}