More Testing (#2683)

This commit is contained in:
Joe Milazzo 2024-02-02 17:12:37 -06:00 committed by GitHub
parent 4ce2b4343a
commit 685f7365e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 168 additions and 36 deletions

View File

@ -61,4 +61,23 @@ public class ReviewController : BaseApiController
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
return Ok(_mapper.Map<UserReviewDto>(rating));
}
/// <summary>
/// Deletes the user's review for the given series
/// </summary>
/// <returns></returns>
[HttpDelete]
public async Task<ActionResult> DeleteReview(int seriesId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
if (user == null) return Unauthorized();
user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList();
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
return Ok();
}
}

View File

@ -132,20 +132,31 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
var seriesDetailPlusDto = new SeriesDetailPlusDto()
IEnumerable<UserReviewDto> reviews = new List<UserReviewDto>();
if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any())
{
Ratings = seriesDetailDto.ExternalRatings
.DefaultIfEmpty()
.Select(r => _mapper.Map<RatingDto>(r)),
Reviews = seriesDetailDto.ExternalReviews
.DefaultIfEmpty()
.OrderByDescending(r => r.Score)
reviews = seriesDetailDto.ExternalReviews
.Select(r =>
{
var ret = _mapper.Map<UserReviewDto>(r);
ret.IsExternal = true;
return ret;
}),
})
.OrderByDescending(r => r.Score);
}
IEnumerable<RatingDto> ratings = new List<RatingDto>();
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
{
ratings = seriesDetailDto.ExternalRatings
.Select(r => _mapper.Map<RatingDto>(r));
}
var seriesDetailPlusDto = new SeriesDetailPlusDto()
{
Ratings = ratings,
Reviews = reviews,
Recommendations = new RecommendationDto()
{
ExternalSeries = externalSeriesRecommendations,

View File

@ -114,7 +114,7 @@ public class ExternalMetadataService : IExternalMetadataService
try
{
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/series-detail")
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)
@ -298,7 +298,7 @@ public class ExternalMetadataService : IExternalMetadataService
}
try
{
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/series/detail")
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)

View File

@ -10,7 +10,7 @@ namespace API.Services;
public static class ReviewService
{
public static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
public static IEnumerable<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
{
IList<UserReviewDto> externalReviews;
var totalReviews = reviews.Count;
@ -42,8 +42,9 @@ public static class ReviewService
externalReviews = reviews;
}
return externalReviews;
return externalReviews.OrderByDescending(r => r.Score);
}
public static string GetCharacters(string body)
{
if (string.IsNullOrEmpty(body)) return body;

View File

@ -171,7 +171,7 @@ public class SeriesService : ISeriesService
} else
{
hasWebLinksChanged =
series.Metadata.WebLinks.Equals(updateSeriesMetadataDto.SeriesMetadata?.WebLinks);
series.Metadata.WebLinks != updateSeriesMetadataDto.SeriesMetadata?.WebLinks;
series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks
.Split(",")
.Where(s => !string.IsNullOrEmpty(s))

View File

@ -209,16 +209,21 @@ export class SeriesService {
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
}
getReviews(seriesId: number) {
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId);
}
deleteReview(seriesId: number) {
return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId);
}
updateReview(seriesId: number, body: string) {
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
seriesId, body
});
}
getReviews(seriesId: number) {
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId);
}
getRatings(seriesId: number) {
return this.httpClient.get<Array<Rating>>(this.baseUrl + 'rating?seriesId=' + seriesId);
}

View File

@ -1,10 +1,23 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
inject,
Input,
OnInit,
Output
} from '@angular/core';
import {CommonModule, NgOptimizedImage} from '@angular/common';
import {UserReview} from "./user-review";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component";
import {AccountService} from "../../_services/account.service";
import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component";
import {
ReviewSeriesModalCloseAction,
ReviewSeriesModalCloseEvent,
ReviewSeriesModalComponent
} from "../review-series-modal/review-series-modal.component";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {ImageComponent} from "../../shared/image/image.component";
@ -25,6 +38,7 @@ export class ReviewCardComponent implements OnInit {
protected readonly ScrobbleProvider = ScrobbleProvider;
@Input({required: true}) review!: UserReview;
@Output() refresh = new EventEmitter<ReviewSeriesModalCloseEvent>();
isMyReview: boolean = false;
@ -48,5 +62,10 @@ export class ReviewCardComponent implements OnInit {
}
const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'});
ref.componentInstance.review = this.review;
ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => {
if (res) {
this.refresh.emit(res);
}
})
}
}

View File

@ -26,6 +26,7 @@
</div>
<div class="modal-footer">
<button class="btn btn-danger" (click)="delete()">{{t('delete')}}</button>
<button class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
<button type="submit" class="btn btn-primary" (click)="save()">{{t('save')}}</button>
</div>

View File

@ -12,7 +12,21 @@ import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
import { SeriesService } from 'src/app/_services/series.service';
import {UserReview} from "../review-card/user-review";
import {CommonModule} from "@angular/common";
import {TranslocoDirective} from "@ngneat/transloco";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {ConfirmService} from "../../shared/confirm.service";
import {ToastrService} from "ngx-toastr";
export enum ReviewSeriesModalCloseAction {
Create,
Edit,
Delete,
Close
}
export interface ReviewSeriesModalCloseEvent {
success: boolean,
review: UserReview;
action: ReviewSeriesModalCloseAction
}
@Component({
selector: 'app-review-series-modal',
@ -27,6 +41,8 @@ export class ReviewSeriesModalComponent implements OnInit {
protected readonly modal = inject(NgbActiveModal);
private readonly seriesService = inject(SeriesService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly confirmService = inject(ConfirmService);
private readonly toastr = inject(ToastrService);
protected readonly minLength = 5;
@Input({required: true}) review!: UserReview;
@ -40,16 +56,23 @@ export class ReviewSeriesModalComponent implements OnInit {
}
close() {
this.modal.close({success: false, review: null});
this.modal.close({success: false, review: this.review, action: ReviewSeriesModalCloseAction.Close});
}
async delete() {
if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return;
this.seriesService.deleteReview(this.review.seriesId).subscribe(() => {
this.toastr.success(translate('toasts.review-deleted'));
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
});
}
save() {
const model = this.reviewGroup.value;
if (model.reviewBody.length < this.minLength) {
return;
}
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
this.modal.close({success: true, review: review});
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
});
}
}

View File

@ -138,7 +138,7 @@
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
[clickableTitle]="true" (sectionClick)="openReviewModal()">
<ng-template #carouselItem let-item let-position="idx">
<app-review-card [review]="item"></app-review-card>
<app-review-card [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
</ng-template>
</app-carousel-reel>
</div>

View File

@ -4,7 +4,8 @@ import {
DOCUMENT,
NgClass,
NgFor,
NgIf, NgOptimizedImage,
NgIf,
NgOptimizedImage,
NgStyle,
NgSwitch,
NgSwitchCase,
@ -44,7 +45,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {catchError, forkJoin, Observable, of} from 'rxjs';
import {filter, map, take, tap} from 'rxjs/operators';
import {map, take} from 'rxjs/operators';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {CardDetailDrawerComponent} from 'src/app/cards/card-detail-drawer/card-detail-drawer.component';
import {EditSeriesModalComponent} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
@ -75,7 +76,11 @@ import {ReaderService} from 'src/app/_services/reader.service';
import {ReadingListService} from 'src/app/_services/reading-list.service';
import {ScrollService} from 'src/app/_services/scroll.service';
import {SeriesService} from 'src/app/_services/series.service';
import {ReviewSeriesModalComponent} from '../../../_single-module/review-series-modal/review-series-modal.component';
import {
ReviewSeriesModalCloseAction,
ReviewSeriesModalCloseEvent,
ReviewSeriesModalComponent
} from '../../../_single-module/review-series-modal/review-series-modal.component';
import {PageLayoutMode} from 'src/app/_models/page-layout-mode';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {UserReview} from "../../../_single-module/review-card/user-review";
@ -137,7 +142,13 @@ const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent, NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe]
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle,
TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent,
EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet,
LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent,
NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe]
})
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
@ -704,7 +715,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
if (data.ratings) {
this.ratings = [...data.ratings];
}
// Recommendations
if (data.recommendations) {
@ -854,18 +865,36 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
modalRef.closed.subscribe((closeResult) => {
// BUG: This never executes!
console.log('Close Result: ')
if (closeResult.success && closeResult.review !== null) {
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
console.log('update index: ', index, ' with review ', closeResult.review);
this.reviews[index] = closeResult.review;
this.cdRef.markForCheck();
}
this.updateOrDeleteReview(closeResult);
});
}
updateOrDeleteReview(closeResult: ReviewSeriesModalCloseEvent) {
if (closeResult.action === ReviewSeriesModalCloseAction.Close) return;
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
if (closeResult.action === ReviewSeriesModalCloseAction.Edit) {
if (index === -1 ) {
// A new series was added:
this.reviews = [closeResult.review, ...this.reviews];
this.cdRef.markForCheck();
return;
}
// An edit occurred
this.reviews[index] = closeResult.review;
this.cdRef.markForCheck();
return;
}
if (closeResult.action === ReviewSeriesModalCloseAction.Delete) {
// An edit occurred
this.reviews = [...this.reviews.filter(r => r.username !== closeResult.review!.username)];
this.cdRef.markForCheck();
return;
}
}
performAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') {

View File

@ -65,6 +65,7 @@
"review-label": "Review",
"close": "{{common.close}}",
"save": "{{common.save}}",
"delete": "{{common.delete}}",
"min-length": "Review must be at least {{count}} characters",
"required": "{{validation.required-field}}"
},
@ -2011,6 +2012,8 @@
"mark-unread": "Marked as Unread",
"series-removed-want-to-read": "Series removed from Want to Read list",
"series-deleted": "Series deleted",
"delete-review": "Are you sure you want to delete your review?",
"review-deleted": "Review deleted",
"file-send-to": "File(s) emailed to {{name}}",
"theme-missing": "The active theme no longer exists. Please refresh the page.",
"email-sent": "Email sent to {{email}}",

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.13.15"
"version": "0.7.13.16"
},
"servers": [
{
@ -7193,6 +7193,27 @@
}
}
}
},
"delete": {
"tags": [
"Review"
],
"summary": "Deletes the user's review for the given series",
"parameters": [
{
"name": "seriesId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/Scrobbling/anilist-token": {