Load, save, and delete chapter reviews
This commit is contained in:
parent
a3e04f3bc1
commit
85b6f107bc
13 changed files with 192 additions and 13 deletions
|
|
@ -6,6 +6,7 @@ using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Person;
|
using API.Entities.Person;
|
||||||
|
|
@ -391,6 +392,15 @@ public class ChapterController : BaseApiController
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all reviews for this chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("review")]
|
||||||
|
public async Task<IList<UserReviewDto>> ChapterReviews([FromQuery] int chapterId)
|
||||||
|
{
|
||||||
|
return await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, User.GetUserId());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,45 @@ public class ReviewController : BaseApiController
|
||||||
return Ok(_mapper.Map<UserReviewDto>(rating));
|
return Ok(_mapper.Map<UserReviewDto>(rating));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the review for a given series
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("chapter/{chapterId}")]
|
||||||
|
public async Task<ActionResult<UserReviewDto>> UpdateChapterReview(int chapterId, UpdateUserReviewDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings);
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ChapterIncludes.None);
|
||||||
|
if (chapter == null) return BadRequest();
|
||||||
|
|
||||||
|
var builder = new ChapterRatingBuilder(user.ChapterRatings.FirstOrDefault(r => r.SeriesId == dto.SeriesId));
|
||||||
|
|
||||||
|
var rating = builder
|
||||||
|
.WithSeriesId(dto.SeriesId)
|
||||||
|
.WithVolumeId(chapter.VolumeId)
|
||||||
|
.WithChapterId(chapter.Id)
|
||||||
|
.WithReview(dto.Body)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
if (rating.Id == 0)
|
||||||
|
{
|
||||||
|
user.ChapterRatings.Add(rating);
|
||||||
|
}
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Do I need this?
|
||||||
|
//BackgroundJob.Enqueue(() =>
|
||||||
|
// _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
|
||||||
|
return Ok(_mapper.Map<UserReviewDto>(rating));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes the user's review for the given series
|
/// Deletes the user's review for the given series
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -80,4 +119,24 @@ public class ReviewController : BaseApiController
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the user's review for a given chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete("chapter/{chapterId}")]
|
||||||
|
public async Task<IActionResult> DeleteChapterReview(int chapterId)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ChapterRatings);
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
user.ChapterRatings = user.ChapterRatings.Where(c => c.ChapterId != chapterId).ToList();
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
||||||
public DbSet<MangaFile> MangaFile { get; set; } = null!;
|
public DbSet<MangaFile> MangaFile { get; set; } = null!;
|
||||||
public DbSet<AppUserProgress> AppUserProgresses { get; set; } = null!;
|
public DbSet<AppUserProgress> AppUserProgresses { get; set; } = null!;
|
||||||
public DbSet<AppUserRating> AppUserRating { get; set; } = null!;
|
public DbSet<AppUserRating> AppUserRating { get; set; } = null!;
|
||||||
|
public DbSet<AppUserChapterRating> AppUserChapterRating { get; set; } = null!;
|
||||||
public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
|
public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
|
||||||
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
|
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
|
||||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
|
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ public enum AppUserIncludes
|
||||||
DashboardStreams = 2048,
|
DashboardStreams = 2048,
|
||||||
SideNavStreams = 4096,
|
SideNavStreams = 4096,
|
||||||
ExternalSources = 8192,
|
ExternalSources = 8192,
|
||||||
Collections = 16384 // 2^14
|
Collections = 16384, // 2^14
|
||||||
|
ChapterRatings = 1 << 15,
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
|
|
@ -66,6 +67,7 @@ public interface IUserRepository
|
||||||
Task<IList<string>> GetRoles(int userId);
|
Task<IList<string>> GetRoles(int userId);
|
||||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
||||||
|
Task<IList<UserReviewDto>> GetUserRatingDtosForChapterAsync(int chapterId, int userId);
|
||||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
||||||
|
|
@ -603,6 +605,19 @@ public class UserRepository : IUserRepository
|
||||||
.ToListAsync();
|
.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)
|
public async Task<AppUserPreferences?> GetPreferencesAsync(string username)
|
||||||
{
|
{
|
||||||
return await _context.AppUserPreferences
|
return await _context.AppUserPreferences
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,11 @@ public static class IncludesExtensions
|
||||||
.ThenInclude(c => c.Items);
|
.ThenInclude(c => c.Items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeFlags.HasFlag(AppUserIncludes.ChapterRatings))
|
||||||
|
{
|
||||||
|
query = query.Include(u => u.ChapterRatings);
|
||||||
|
}
|
||||||
|
|
||||||
return query.AsSplitQuery();
|
return query.AsSplitQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,16 @@ public class AutoMapperProfiles : Profile
|
||||||
.ForMember(dest => dest.Username,
|
.ForMember(dest => dest.Username,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.AppUser.UserName));
|
opt.MapFrom(src => src.AppUser.UserName));
|
||||||
|
CreateMap<AppUserChapterRating, UserReviewDto>()
|
||||||
|
.ForMember(dest => dest.LibraryId,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.Series.LibraryId))
|
||||||
|
.ForMember(dest => dest.Body,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.Review))
|
||||||
|
.ForMember(dest => dest.Username,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.AppUser.UserName));
|
||||||
|
|
||||||
CreateMap<AppUserProgress, ProgressDto>()
|
CreateMap<AppUserProgress, ProgressDto>()
|
||||||
.ForMember(dest => dest.PageNum,
|
.ForMember(dest => dest.PageNum,
|
||||||
|
|
|
||||||
50
API/Helpers/Builders/ChapterRatingBuilder.cs
Normal file
50
API/Helpers/Builders/ChapterRatingBuilder.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System;
|
||||||
|
using API.Entities;
|
||||||
|
|
||||||
|
namespace API.Helpers.Builders;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public class ChapterRatingBuilder
|
||||||
|
{
|
||||||
|
private readonly AppUserChapterRating _rating;
|
||||||
|
|
||||||
|
public AppUserChapterRating Build() => _rating;
|
||||||
|
|
||||||
|
public ChapterRatingBuilder(AppUserChapterRating? rating = null)
|
||||||
|
{
|
||||||
|
_rating = rating ?? new AppUserChapterRating();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterRatingBuilder WithSeriesId(int seriesId)
|
||||||
|
{
|
||||||
|
_rating.SeriesId = seriesId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterRatingBuilder WithChapterId(int chapterId)
|
||||||
|
{
|
||||||
|
_rating.ChapterId = chapterId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterRatingBuilder WithVolumeId(int volumeId)
|
||||||
|
{
|
||||||
|
_rating.VolumeId = volumeId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterRatingBuilder WithRating(int rating)
|
||||||
|
{
|
||||||
|
_rating.Rating = Math.Clamp(rating, 0, 5);
|
||||||
|
_rating.HasBeenRated = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChapterRatingBuilder WithReview(string review)
|
||||||
|
{
|
||||||
|
_rating.Review = review;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import {environment} from "../../environments/environment";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import {Chapter} from "../_models/chapter";
|
import {Chapter} from "../_models/chapter";
|
||||||
import {TextResonse} from "../_types/text-response";
|
import {TextResonse} from "../_types/text-response";
|
||||||
|
import {UserReview} from "../_single-module/review-card/user-review";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -29,4 +30,16 @@ export class ChapterService {
|
||||||
return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse);
|
return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapterReviews(chapterId: number) {
|
||||||
|
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'chapter/review?chapterId='+chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapterReview(seriesId: number, chapterId: number, body: string) {
|
||||||
|
return this.httpClient.post<UserReview>(this.baseUrl + 'review/chapter/'+chapterId, {seriesId, body});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChapterReview(chapterId: number) {
|
||||||
|
return this.httpClient.delete(this.baseUrl + 'review/chapter/'+chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<ng-container *transloco="let t; read:'review-series-modal'">
|
<ng-container *transloco="let t; read:'review-modal'">
|
||||||
<div>
|
<div>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {ConfirmService} from "../../shared/confirm.service";
|
import {ConfirmService} from "../../shared/confirm.service";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import {ChapterService} from "../../_services/chapter.service";
|
import {ChapterService} from "../../_services/chapter.service";
|
||||||
|
import {of} from "rxjs";
|
||||||
|
|
||||||
export enum ReviewSeriesModalCloseAction {
|
export enum ReviewSeriesModalCloseAction {
|
||||||
Create,
|
Create,
|
||||||
|
|
@ -55,12 +56,18 @@ export class ReviewModalComponent implements OnInit {
|
||||||
async delete() {
|
async delete() {
|
||||||
if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return;
|
if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return;
|
||||||
|
|
||||||
|
let obs;
|
||||||
if (this.reviewLocation === 'series') {
|
if (this.reviewLocation === 'series') {
|
||||||
this.seriesService.deleteReview(this.review.seriesId).subscribe(() => {
|
obs = this.seriesService.deleteReview(this.review.seriesId);
|
||||||
this.toastr.success(translate('toasts.review-deleted'));
|
|
||||||
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (this.reviewLocation === 'chapter') {
|
||||||
|
obs = this.chapterService.deleteChapterReview(this.review.chapterId!)
|
||||||
|
}
|
||||||
|
|
||||||
|
obs?.subscribe(() => {
|
||||||
|
this.toastr.success(translate('toasts.review-deleted'));
|
||||||
|
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
save() {
|
save() {
|
||||||
|
|
@ -69,11 +76,17 @@ export class ReviewModalComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let obs;
|
||||||
if (this.reviewLocation === 'series') {
|
if (this.reviewLocation === 'series') {
|
||||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
|
obs = this.seriesService.updateReview(this.review.seriesId, model.reviewBody);
|
||||||
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (this.reviewLocation === 'chapter') {
|
||||||
|
obs = this.chapterService.updateChapterReview(this.review.seriesId, this.review.chapterId!, model.reviewBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
obs?.subscribe(review => {
|
||||||
|
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@
|
||||||
|
|
||||||
<li [ngbNavItem]="TabID.Reviews">
|
<li [ngbNavItem]="TabID.Reviews">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
{{t(TabID.Reviews)}}
|
{{t('reviews-tab')}}
|
||||||
<span class="badge rounded-pill text-bg-secondary">{{userReviews.length + plusReviews.length}}</span>
|
<span class="badge rounded-pill text-bg-secondary">{{userReviews.length + plusReviews.length}}</span>
|
||||||
</a>
|
</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,8 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
forkJoin({
|
forkJoin({
|
||||||
series: this.seriesService.getSeries(this.seriesId),
|
series: this.seriesService.getSeries(this.seriesId),
|
||||||
chapter: this.chapterService.getChapterMetadata(this.chapterId),
|
chapter: this.chapterService.getChapterMetadata(this.chapterId),
|
||||||
libraryType: this.libraryService.getLibraryType(this.libraryId)
|
libraryType: this.libraryService.getLibraryType(this.libraryId),
|
||||||
|
reviews: this.chapterService.chapterReviews(this.chapterId),
|
||||||
}).subscribe(results => {
|
}).subscribe(results => {
|
||||||
|
|
||||||
if (results.chapter === null) {
|
if (results.chapter === null) {
|
||||||
|
|
@ -234,6 +235,8 @@ export class ChapterDetailComponent implements OnInit {
|
||||||
this.chapter = results.chapter;
|
this.chapter = results.chapter;
|
||||||
this.weblinks = this.chapter.webLinks.split(',');
|
this.weblinks = this.chapter.webLinks.split(',');
|
||||||
this.libraryType = results.libraryType;
|
this.libraryType = results.libraryType;
|
||||||
|
this.userReviews = results.reviews.filter(r => !r.isExternal);
|
||||||
|
this.plusReviews = results.reviews.filter(r => r.isExternal);
|
||||||
|
|
||||||
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
|
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
"user-reviews-plus": "External Reviews"
|
"user-reviews-plus": "External Reviews"
|
||||||
},
|
},
|
||||||
|
|
||||||
"review-series-modal": {
|
"review-modal": {
|
||||||
"title": "Edit Review",
|
"title": "Edit Review",
|
||||||
"review-label": "Review",
|
"review-label": "Review",
|
||||||
"close": "{{common.close}}",
|
"close": "{{common.close}}",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue