Release Polish (#1586)

* Fixed a scaling issue in the epub reader, where images could scale when they shouldn't.

* Removed some caching on library/ api and added more output for a foreign key constraint

* Hooked in Restricted Profile stat collection

* Added a new boolean on age restrictions to explicitly allow unknowns or not. Since unknown is the default state of metadata, if users are allowed access to Unknown, age restricted content could leak.

* Fixed a bug where sometimes series cover generation could fail under conditions where only specials existed.

* Fixed foreign constraint issue when cleaning up series not seen at end of scan loop

* Removed an additional epub parse when scanning and handled merging differently

* Code smell
This commit is contained in:
Joe Milazzo 2022-10-17 15:33:18 -07:00 committed by GitHub
parent 78762a5626
commit 9149c4cbca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2504 additions and 145 deletions

View file

@ -0,0 +1,6 @@
import { AgeRating } from "./metadata/age-rating";
export interface AgeRestriction {
ageRating: AgeRating;
includeUnknowns: boolean;
}

View file

@ -1,5 +1,5 @@
import { AgeRestriction } from './age-restriction';
import { Library } from './library';
import { AgeRating } from './metadata/age-rating';
export interface Member {
id: number;
@ -9,8 +9,5 @@ export interface Member {
created: string; // datetime
roles: string[];
libraries: Library[];
/**
* If not applicable, will store a -1
*/
ageRestriction: AgeRating;
ageRestriction: AgeRestriction;
}

View file

@ -1,4 +1,4 @@
import { AgeRating } from './metadata/age-rating';
import { AgeRestriction } from './age-restriction';
import { Preferences } from './preferences/preferences';
// This interface is only used for login and storing/retreiving JWT from local storage
@ -10,5 +10,5 @@ export interface User {
preferences: Preferences;
apiKey: string;
email: string;
ageRestriction: AgeRating;
ageRestriction: AgeRestriction;
}

View file

@ -12,6 +12,7 @@ import { InviteUserResponse } from '../_models/invite-user-response';
import { UserUpdateEvent } from '../_models/events/user-update-event';
import { UpdateEmailResponse } from '../_models/email/update-email-response';
import { AgeRating } from '../_models/metadata/age-rating';
import { AgeRestriction } from '../_models/age-restriction';
export enum Role {
Admin = 'Admin',
@ -161,7 +162,7 @@ export class AccountService implements OnDestroy {
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
}
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRating}) {
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRestriction}) {
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/invite', model);
}
@ -198,7 +199,7 @@ export class AccountService implements OnDestroy {
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password, oldPassword}, {responseType: 'json' as 'text'});
}
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number, ageRestriction: AgeRating}) {
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number, ageRestriction: AgeRestriction}) {
return this.httpClient.post(this.baseUrl + 'account/update', model);
}
@ -206,8 +207,8 @@ export class AccountService implements OnDestroy {
return this.httpClient.post<UpdateEmailResponse>(this.baseUrl + 'account/update/email', {email});
}
updateAgeRestriction(ageRating: AgeRating) {
return this.httpClient.post(this.baseUrl + 'account/update/age-restriction', {ageRating});
updateAgeRestriction(ageRating: AgeRating, includeUnknowns: boolean) {
return this.httpClient.post(this.baseUrl + 'account/update/age-restriction', {ageRating, includeUnknowns});
}
/**

View file

@ -1,12 +1,11 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { AgeRestriction } from 'src/app/_models/age-restriction';
import { Library } from 'src/app/_models/library';
import { Member } from 'src/app/_models/member';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { AccountService } from 'src/app/_services/account.service';
// TODO: Rename this to EditUserModal
@Component({
selector: 'app-edit-user',
templateUrl: './edit-user.component.html',
@ -18,7 +17,7 @@ export class EditUserComponent implements OnInit {
selectedRoles: Array<string> = [];
selectedLibraries: Array<number> = [];
selectedRating: AgeRating = AgeRating.NotApplicable;
selectedRestriction!: AgeRestriction;
isSaving: boolean = false;
userForm: FormGroup = new FormGroup({});
@ -41,8 +40,8 @@ export class EditUserComponent implements OnInit {
this.selectedRoles = roles;
}
updateRestrictionSelection(rating: AgeRating) {
this.selectedRating = rating;
updateRestrictionSelection(restriction: AgeRestriction) {
this.selectedRestriction = restriction;
}
updateLibrarySelection(libraries: Array<Library>) {
@ -58,8 +57,7 @@ export class EditUserComponent implements OnInit {
model.userId = this.member.id;
model.roles = this.selectedRoles;
model.libraries = this.selectedLibraries;
model.ageRestriction = this.selectedRating || AgeRating.NotApplicable;
console.log('rating: ', this.selectedRating);
model.ageRestriction = this.selectedRestriction;
this.accountService.update(model).subscribe(() => {
this.modal.close(true);
});

View file

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { AgeRestriction } from 'src/app/_models/age-restriction';
import { InviteUserResponse } from 'src/app/_models/invite-user-response';
import { Library } from 'src/app/_models/library';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
@ -21,7 +22,7 @@ export class InviteUserComponent implements OnInit {
inviteForm: FormGroup = new FormGroup({});
selectedRoles: Array<string> = [];
selectedLibraries: Array<number> = [];
selectedRating: AgeRating = AgeRating.NotApplicable;
selectedRestriction: AgeRestriction = {ageRating: AgeRating.NotApplicable, includeUnknowns: false};
emailLink: string = '';
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
@ -48,7 +49,7 @@ export class InviteUserComponent implements OnInit {
email,
libraries: this.selectedLibraries,
roles: this.selectedRoles,
ageRestriction: this.selectedRating
ageRestriction: this.selectedRestriction
}).subscribe((data: InviteUserResponse) => {
this.emailLink = data.emailLink;
this.isSending = false;
@ -69,8 +70,8 @@ export class InviteUserComponent implements OnInit {
this.selectedLibraries = libraries.map(l => l.id);
}
updateRestrictionSelection(rating: AgeRating) {
this.selectedRating = rating;
updateRestrictionSelection(restriction: AgeRestriction) {
this.selectedRestriction = restriction;
}
}

View file

@ -264,8 +264,7 @@ $action-bar-height: 38px;
// This is applied to images in the backend
::ng-deep .kavita-scale-width-container {
width: auto;
// * 4 is just for extra buffer which is needed based on testing. --book-reader-content-max-height is set by us on calculation of columnHeight
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height * 4)), calc((var(--vh)*100) - ($action-bar-height * 4)) !important;
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
}
// This is applied to images in the backend

View file

@ -10,7 +10,11 @@
</div>
<ng-container *ngIf="isViewMode">
<span >{{user?.ageRestriction | ageRating | async}}</span>
<span>{{user?.ageRestriction?.ageRating| ageRating | async}}
<ng-container *ngIf="user?.ageRestriction?.ageRating !== AgeRating.NotApplicable && user?.ageRestriction?.includeUnknowns">
<span class="ms-1 me-1">+</span> Unknowns
</ng-container>
</span>
</ng-container>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isViewMode">

View file

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs';
import { AgeRestriction } from 'src/app/_models/age-restriction';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
@ -16,9 +17,9 @@ export class ChangeAgeRestrictionComponent implements OnInit {
user: User | undefined = undefined;
hasChangeAgeRestrictionAbility: Observable<boolean> = of(false);
isViewMode: boolean = true;
selectedRating: AgeRating = AgeRating.NotApplicable;
originalRating!: AgeRating;
reset: EventEmitter<AgeRating> = new EventEmitter();
selectedRestriction!: AgeRestriction;
originalRestriction!: AgeRestriction;
reset: EventEmitter<AgeRestriction> = new EventEmitter();
get AgeRating() { return AgeRating; }
@ -28,8 +29,9 @@ export class ChangeAgeRestrictionComponent implements OnInit {
ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), take(1)).subscribe(user => {
if (!user) return;
this.user = user;
this.originalRating = this.user?.ageRestriction || AgeRating.NotApplicable;
this.originalRestriction = this.user.ageRestriction;
this.cdRef.markForCheck();
});
@ -39,8 +41,8 @@ export class ChangeAgeRestrictionComponent implements OnInit {
this.cdRef.markForCheck();
}
updateRestrictionSelection(rating: AgeRating) {
this.selectedRating = rating;
updateRestrictionSelection(restriction: AgeRestriction) {
this.selectedRestriction = restriction;
}
ngOnDestroy() {
@ -50,18 +52,19 @@ export class ChangeAgeRestrictionComponent implements OnInit {
resetForm() {
if (!this.user) return;
this.reset.emit(this.originalRating);
this.reset.emit(this.originalRestriction);
this.cdRef.markForCheck();
}
saveForm() {
if (this.user === undefined) { return; }
this.accountService.updateAgeRestriction(this.selectedRating).subscribe(() => {
this.accountService.updateAgeRestriction(this.selectedRestriction.ageRating, this.selectedRestriction.includeUnknowns).subscribe(() => {
this.toastr.success('Age Restriction has been updated');
this.originalRating = this.selectedRating;
this.originalRestriction = this.selectedRestriction;
if (this.user) {
this.user.ageRestriction = this.selectedRating;
this.user.ageRestriction.ageRating = this.selectedRestriction.ageRating;
this.user.ageRestriction.includeUnknowns = this.selectedRestriction.includeUnknowns;
}
this.resetForm();
this.isViewMode = true;

View file

@ -15,5 +15,17 @@
</select>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" role="switch" formControlName="ageRestrictionIncludeUnknowns" class="form-check-input" aria-describedby="include-unknowns-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">Include Unknowns</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="includeUnknownsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #includeUnknownsTooltip>If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</ng-template>
<span class="visually-hidden" id="include-unknowns-help">If true, Unknowns will be allowed with Age Restrcition. This could lead to untagged media leaking to users with Age restrictions.</span>
</div>
</form>
</ng-container>

View file

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { AgeRestriction } from 'src/app/_models/age-restriction';
import { Member } from 'src/app/_models/member';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
@ -20,8 +21,8 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
* Show labels and description around the form
*/
@Input() showContext: boolean = true;
@Input() reset: EventEmitter<AgeRating> | undefined;
@Output() selected: EventEmitter<AgeRating> = new EventEmitter<AgeRating>();
@Input() reset: EventEmitter<AgeRestriction> | undefined;
@Output() selected: EventEmitter<AgeRestriction> = new EventEmitter<AgeRestriction>();
ageRatings: Array<AgeRatingDto> = [];
@ -32,22 +33,36 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.restrictionForm = new FormGroup({
'ageRating': new FormControl(this.member?.ageRestriction || AgeRating.NotApplicable, [])
'ageRating': new FormControl(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable || AgeRating.NotApplicable, []),
'ageRestrictionIncludeUnknowns': new FormControl(this.member?.ageRestriction.includeUnknowns, []),
});
if (this.isAdmin) {
this.restrictionForm.get('ageRating')?.disable();
this.restrictionForm.get('ageRestrictionIncludeUnknowns')?.disable();
}
if (this.reset) {
this.reset.subscribe(e => {
this.restrictionForm?.get('ageRating')?.setValue(e);
this.restrictionForm?.get('ageRating')?.setValue(e.ageRating);
this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.setValue(e.includeUnknowns);
this.cdRef.markForCheck();
});
}
this.restrictionForm.get('ageRating')?.valueChanges.subscribe(e => {
this.selected.emit(parseInt(e, 10));
this.selected.emit({
ageRating: parseInt(e, 10),
includeUnknowns: this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.value
});
});
this.restrictionForm.get('ageRestrictionIncludeUnknowns')?.valueChanges.subscribe(e => {
this.selected.emit({
ageRating: parseInt(this.restrictionForm?.get('ageRating')?.value, 10),
includeUnknowns: e
});
});
this.metadataService.getAllAgeRatings().subscribe(ratings => {
@ -60,8 +75,8 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
ngOnChanges() {
if (!this.member) return;
console.log('changes: ');
this.restrictionForm?.get('ageRating')?.setValue(this.member?.ageRestriction || AgeRating.NotApplicable);
this.restrictionForm?.get('ageRating')?.setValue(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable);
this.restrictionForm?.get('ageRestrictionIncludeUnknowns')?.setValue(this.member?.ageRestriction.includeUnknowns);
this.cdRef.markForCheck();
}