Chapter/Issue level Reviews and Ratings (#3778)
Co-authored-by: Joseph Milazzo <josephmajora@gmail.com>
This commit is contained in:
parent
3b8997e46e
commit
4f7625ea77
60 changed files with 5097 additions and 497 deletions
|
@ -78,6 +78,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
|
||||
public DbSet<MetadataSettings> MetadataSettings { get; set; } = null!;
|
||||
public DbSet<MetadataFieldMapping> MetadataFieldMapping { get; set; } = null!;
|
||||
public DbSet<AppUserChapterRating> AppUserChapterRating { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
|
3536
API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs
generated
Normal file
3536
API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
165
API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs
Normal file
165
API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs
Normal file
|
@ -0,0 +1,165 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChapterRatingAndReviews : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Authority",
|
||||
table: "ExternalReview",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ChapterId",
|
||||
table: "ExternalReview",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Authority",
|
||||
table: "ExternalRating",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ChapterId",
|
||||
table: "ExternalRating",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "AverageExternalRating",
|
||||
table: "Chapter",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserChapterRating",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Rating = table.Column<float>(type: "REAL", nullable: false),
|
||||
HasBeenRated = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Review = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserChapterRating", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserChapterRating_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserChapterRating_Chapter_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserChapterRating_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalReview_ChapterId",
|
||||
table: "ExternalReview",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExternalRating_ChapterId",
|
||||
table: "ExternalRating",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserChapterRating_AppUserId",
|
||||
table: "AppUserChapterRating",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserChapterRating_ChapterId",
|
||||
table: "AppUserChapterRating",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserChapterRating_SeriesId",
|
||||
table: "AppUserChapterRating",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ExternalRating_Chapter_ChapterId",
|
||||
table: "ExternalRating",
|
||||
column: "ChapterId",
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ExternalReview_Chapter_ChapterId",
|
||||
table: "ExternalReview",
|
||||
column: "ChapterId",
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ExternalRating_Chapter_ChapterId",
|
||||
table: "ExternalRating");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ExternalReview_Chapter_ChapterId",
|
||||
table: "ExternalReview");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserChapterRating");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExternalReview_ChapterId",
|
||||
table: "ExternalReview");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExternalRating_ChapterId",
|
||||
table: "ExternalRating");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Authority",
|
||||
table: "ExternalReview");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChapterId",
|
||||
table: "ExternalReview");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Authority",
|
||||
table: "ExternalRating");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChapterId",
|
||||
table: "ExternalRating");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AverageExternalRating",
|
||||
table: "Chapter");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -195,6 +195,41 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AppUserBookmark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserChapterRating", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HasBeenRated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("Rating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Review")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserChapterRating");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserCollection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -752,6 +787,9 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("AlternateSeries")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("AverageExternalRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("AvgHoursToRead")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
|
@ -1316,9 +1354,15 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Authority")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AverageScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FavoriteCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1333,6 +1377,8 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("ExternalRating");
|
||||
});
|
||||
|
||||
|
@ -1379,12 +1425,18 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Authority")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BodyJustText")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Provider")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1414,6 +1466,8 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("ExternalReview");
|
||||
});
|
||||
|
||||
|
@ -2618,6 +2672,33 @@ namespace API.Data.Migrations
|
|||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserChapterRating", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("ChapterRatings")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany("Ratings")
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserCollection", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
@ -2905,6 +2986,20 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", null)
|
||||
.WithMany("ExternalRatings")
|
||||
.HasForeignKey("ChapterId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", null)
|
||||
.WithMany("ExternalReviews")
|
||||
.HasForeignKey("ChapterId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
|
@ -3332,6 +3427,8 @@ namespace API.Data.Migrations
|
|||
{
|
||||
b.Navigation("Bookmarks");
|
||||
|
||||
b.Navigation("ChapterRatings");
|
||||
|
||||
b.Navigation("Collections");
|
||||
|
||||
b.Navigation("DashboardStreams");
|
||||
|
@ -3363,10 +3460,16 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Navigation("ExternalRatings");
|
||||
|
||||
b.Navigation("ExternalReviews");
|
||||
|
||||
b.Navigation("Files");
|
||||
|
||||
b.Navigation("People");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
||||
b.Navigation("UserProgress");
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
|
@ -24,7 +25,8 @@ public enum ChapterIncludes
|
|||
Files = 4,
|
||||
People = 8,
|
||||
Genres = 16,
|
||||
Tags = 32
|
||||
Tags = 32,
|
||||
ExternalReviews = 1 << 6,
|
||||
}
|
||||
|
||||
public interface IChapterRepository
|
||||
|
@ -48,6 +50,9 @@ public interface IChapterRepository
|
|||
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
|
||||
IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
|
||||
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
|
||||
Task<int> GetAverageUserRating(int chapterId, int userId);
|
||||
Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId);
|
||||
Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId);
|
||||
}
|
||||
public class ChapterRepository : IChapterRepository
|
||||
{
|
||||
|
@ -310,4 +315,39 @@ public class ChapterRepository : IChapterRepository
|
|||
.ThenInclude(cp => cp.Person)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetAverageUserRating(int chapterId, int userId)
|
||||
{
|
||||
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||
var countOfRatingsThatAreUser = await _context.AppUserChapterRating
|
||||
.Where(r => r.ChapterId == chapterId && r.HasBeenRated)
|
||||
.CountAsync(u => u.AppUserId == userId);
|
||||
if (countOfRatingsThatAreUser == 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var avg = (await _context.AppUserChapterRating
|
||||
.Where(r => r.ChapterId == chapterId && r.HasBeenRated)
|
||||
.AverageAsync(r => (int?) r.Rating));
|
||||
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
||||
}
|
||||
|
||||
public async Task<IList<UserReviewDto>> GetExternalChapterReviews(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.SelectMany(c => c.ExternalReviews)
|
||||
// Don't use ProjectTo, it fails to map int to float (??)
|
||||
.Select(r => _mapper.Map<UserReviewDto>(r))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<RatingDto>> GetExternalChapterRatings(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.SelectMany(c => c.ExternalRatings)
|
||||
.ProjectTo<RatingDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@ public enum AppUserIncludes
|
|||
DashboardStreams = 2048,
|
||||
SideNavStreams = 4096,
|
||||
ExternalSources = 8192,
|
||||
Collections = 16384 // 2^14
|
||||
Collections = 16384, // 2^14
|
||||
ChapterRatings = 1 << 15,
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
@ -65,7 +66,9 @@ public interface IUserRepository
|
|||
Task<bool> IsUserAdminAsync(AppUser? user);
|
||||
Task<IList<string>> GetRoles(int userId);
|
||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||
Task<AppUserChapterRating?> GetUserChapterRatingAsync(int userId, int chapterId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId);
|
||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
||||
|
@ -587,7 +590,14 @@ public class UserRepository : IUserRepository
|
|||
{
|
||||
return await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
|
||||
.SingleOrDefaultAsync();
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUserChapterRating?> GetUserChapterRatingAsync(int userId, int chapterId)
|
||||
{
|
||||
return await _context.AppUserChapterRating
|
||||
.Where(r => r.AppUserId == userId && r.ChapterId == chapterId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId)
|
||||
|
@ -603,6 +613,19 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserChapterRating
|
||||
.Include(r => r.AppUser)
|
||||
.Where(r => r.ChapterId == chapterId)
|
||||
.Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId)
|
||||
.OrderBy(r => r.AppUserId == userId)
|
||||
.ThenBy(r => r.Rating)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<UserReviewDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUserPreferences?> GetPreferencesAsync(string username)
|
||||
{
|
||||
return await _context.AppUserPreferences
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue