Add overview of a user's actions in user-edit (#2558)

pull/2568/head
Rigel Kent 2020-03-27 15:19:03 +01:00 committed by GitHub
parent 56d72521ec
commit 76314386ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 618 additions and 184 deletions

View File

@ -579,7 +579,7 @@
i18n-labelText labelText="Allow additional extensions" i18n-labelText labelText="Allow additional extensions"
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n>Allow your users to upload .mkv, .mov, .avi and .flv videos.</span> <span i18n>Allows users to upload .mkv, .mov, .avi and .flv videos.</span>
</ng-container> </ng-container>
</my-peertube-checkbox> </my-peertube-checkbox>
</div> </div>
@ -590,7 +590,7 @@
i18n-labelText labelText="Allow audio files upload" i18n-labelText labelText="Allow audio files upload"
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n>Allow your users to upload audio files that will be merged with the preview file on upload.</span> <span i18n>Allows users to upload audio files that will be merged with the preview file on upload.</span>
</ng-container> </ng-container>
</my-peertube-checkbox> </my-peertube-checkbox>
</div> </div>

View File

@ -50,6 +50,7 @@ input[type=submit] {
textarea { textarea {
@include peertube-textarea(500px, 150px); @include peertube-textarea(500px, 150px);
max-width: 100%;
display: block; display: block;
&.small { &.small {
@ -72,6 +73,10 @@ my-markdown-textarea ::ng-deep {
@media screen and (max-width: 1400px) { @media screen and (max-width: 1400px) {
flex-direction: column !important; flex-direction: column !important;
} }
textarea {
max-width: 100%;
}
} }
} }

View File

@ -8,6 +8,7 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
import { ConfigService } from '@app/+admin/config/shared/config.service' import { ConfigService } from '@app/+admin/config/shared/config.service'
import { UserService } from '@app/shared' import { UserService } from '@app/shared'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({ @Component({
selector: 'my-user-create', selector: 'my-user-create',
@ -21,6 +22,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
protected serverService: ServerService, protected serverService: ServerService,
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
protected configService: ConfigService, protected configService: ConfigService,
protected screenService: ScreenService,
protected auth: AuthService, protected auth: AuthService,
private userValidatorsService: UserValidatorsService, private userValidatorsService: UserValidatorsService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -1,112 +1,204 @@
<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create user</div> <nav aria-label="breadcrumb">
<div i18n class="form-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div> <ol class="breadcrumb">
<li class="breadcrumb-item">
<a routerLink="/admin/users" i18n>Users</a>
</li>
<ng-container *ngIf="isCreation()">
<li class="breadcrumb-item active" i18n>Create</li>
</ng-container>
<ng-container *ngIf="!isCreation()">
<li class="breadcrumb-item active" i18n>Edit</li>
<li class="breadcrumb-item active" aria-current="page">
<a *ngIf="user" [routerLink]="[ '/accounts', user?.username ]">{{ user?.username }}</a>
</li>
</ng-container>
</ol>
</nav>
<ng-template #dashboard>
<div *ngIf="!isCreation() && user" class="dashboard">
<div>
<a>
<div class="dashboard-num">{{ user.videosCount }} ({{ user.videoQuotaUsed | bytes: 0 }})</div>
<div class="dashboard-label" i18n>{user.videosCount, plural, =1 {Video} other {Videos}}</div>
</a>
</div>
<div>
<a>
<div class="dashboard-num">{{ user.videoChannels.length || 0 }}</div>
<div class="dashboard-label" i18n>{user.videoChannels.length, plural, =1 {Channel} other {Channels}}</div>
</a>
</div>
<div>
<a>
<div class="dashboard-num">{{ subscribersCount }}</div>
<div class="dashboard-label" i18n>{subscribersCount, plural, =1 {Subscriber} other {Subscribers}}</div>
</a>
</div>
<div>
<a>
<div class="dashboard-num">{{ user.videoAbusesCount }}</div>
<div class="dashboard-label" i18n>Incriminated in reports</div>
</a>
</div>
<div>
<a>
<div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div>
<div class="dashboard-label" i18n>Authored reports accepted</div>
</a>
</div>
<div>
<a>
<div class="dashboard-num">{{ user.videoCommentsCount }}</div>
<div class="dashboard-label" i18n>{user.videoCommentsCount, plural, =1 {Comment} other {Comments}}</div>
</a>
</div>
</div>
</ng-template>
<div class="form-row" *ngIf="!isInBigView()"> <!-- hidden on large screens, as it is then displayed on the right side of the form -->
<div class="col-12 col-xl-3"></div>
<div class="form-group-right col-12 col-xl-9">
<ng-template *ngTemplateOutlet="dashboard"></ng-template>
</div>
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> <div class="form-row mt-4"> <!-- user grid -->
<div class="form-group" *ngIf="isCreation()"> <div class="form-group col-12 col-lg-4 col-xl-3">
<label i18n for="username">Username</label> <div class="anchor" id="user"></div> <!-- user anchor -->
<input <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
type="text" id="username" i18n-placeholder placeholder="john" <div *ngIf="!isCreation() && user" class="account-title">
formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info>
>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }">
<label i18n for="email">Email</label>
<input
type="text" id="email" i18n-placeholder placeholder="mail@example.com"
formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
autocomplete="off"
>
<div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }}
</div>
</div>
<div class="form-group" *ngIf="isCreation()"> <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }">
<label i18n for="password">Password</label> <div class="form-group" *ngIf="isCreation()">
<my-help *ngIf="isPasswordOptional()"> <label i18n for="username">Username</label>
<ng-template ptTemplate="customHtml"> <input
<ng-container i18n> type="text" id="username" i18n-placeholder placeholder="john"
If you leave the password empty, an email will be sent to the user. formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
</ng-container> >
</ng-template> <div *ngIf="formErrors.username" class="form-error">
</my-help> {{ formErrors.username }}
<input </div>
type="password" id="password" autocomplete="new-password" </div>
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
> <div class="form-group">
<div *ngIf="formErrors.password" class="form-error"> <label i18n for="email">Email</label>
{{ formErrors.password }} <input
</div> type="text" id="email" i18n-placeholder placeholder="mail@example.com"
</div> formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
autocomplete="off"
>
<div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }}
</div>
</div>
<div class="form-group" *ngIf="isCreation()">
<label i18n for="password">Password</label>
<my-help *ngIf="isPasswordOptional()">
<ng-template ptTemplate="customHtml">
<ng-container i18n>
If you leave the password empty, an email will be sent to the user.
</ng-container>
</ng-template>
</my-help>
<input
type="password" id="password" autocomplete="new-password"
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div>
<div class="form-group">
<label i18n for="role">Role</label>
<div class="peertube-select-container">
<select id="role" formControlName="role">
<option *ngFor="let role of roles" [value]="role.value">
{{ role.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.role" class="form-error">
{{ formErrors.role }}
</div>
</div>
<div class="form-group">
<label i18n for="videoQuota">Video quota</label>
<div class="peertube-select-container">
<select id="videoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
</div>
</div>
<div class="form-group">
<label i18n for="videoQuotaDaily">Daily video quota</label>
<div class="peertube-select-container">
<select id="videoQuotaDaily" formControlName="videoQuotaDaily">
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
{{ videoQuotaDailyOption.label }}
</option>
</select>
</div>
</div>
<div class="form-group">
<my-peertube-checkbox
inputName="byPassAutoBlacklist" formControlName="byPassAutoBlacklist"
i18n-labelText labelText="Doesn't need review before a video goes public"
></my-peertube-checkbox>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>
<div class="form-group"> <div *ngIf="isInBigView()" class="col-7">
<label i18n for="role">Role</label> <ng-template *ngTemplateOutlet="dashboard"></ng-template>
<div class="peertube-select-container"> </div>
<select id="role" formControlName="role">
<option *ngFor="let role of roles" [value]="role.value"> </div>
{{ role.label }} </div>
</option>
</select>
<div *ngIf="!isCreation() && user" class="form-row mt-4"> <!-- danger zone grid -->
<div class="form-group col-12 col-lg-4 col-xl-3">
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
<div i18n class="account-title">DANGER ZONE</div>
</div>
<div class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }">
<div class="danger-zone">
<div class="form-group reset-password-email">
<label i18n>Send a link to reset the password by email to the user</label>
<button (click)="resetPassword()" i18n>Ask for new password</button>
</div>
<div class="form-group">
<label i18n>Manually set the user password</label>
<my-user-password [userId]="user.id"></my-user-password>
</div>
</div> </div>
<div *ngIf="formErrors.role" class="form-error">
{{ formErrors.role }}
</div>
</div>
<div class="form-group">
<label i18n for="videoQuota">Video quota</label>
<div class="peertube-select-container">
<select id="videoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
At most, this user could use ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
</div>
</div>
<div class="form-group">
<label i18n for="videoQuotaDaily">Daily video quota</label>
<div class="peertube-select-container">
<select id="videoQuotaDaily" formControlName="videoQuotaDaily">
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
{{ videoQuotaDailyOption.label }}
</option>
</select>
</div>
</div>
<div class="form-group">
<my-peertube-checkbox
inputName="byPassAutoBlacklist" formControlName="byPassAutoBlacklist"
i18n-labelText labelText="Bypass video auto blacklist"
></my-peertube-checkbox>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>
<div *ngIf="!isCreation()" class="danger-zone">
<div class="account-title" i18n>Danger Zone</div>
<div class="form-group reset-password-email">
<label i18n>Send a link to reset the password by email to the user</label>
<button (click)="resetPassword()" i18n>Ask for new password</button>
</div>
<div class="form-group">
<label i18n>Manually set the user password</label>
<my-user-password [userId]="userId"></my-user-password>
</div> </div>
</div> </div>

View File

@ -1,8 +1,13 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
.form-sub-title { label {
margin-bottom: 30px; font-weight: $font-regular;
font-size: 100%;
}
.account-title {
@include settings-big-title;
} }
input:not([type=submit]) { input:not([type=submit]) {
@ -26,18 +31,9 @@ input[type=submit], button {
font-size: 11px; font-size: 11px;
} }
.account-title {
@include in-content-small-title;
margin-top: 55px;
margin-bottom: 30px;
}
.danger-zone { .danger-zone {
.reset-password-email { .reset-password-email {
margin-bottom: 30px; margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
button { button {
display: block; display: block;
@ -45,3 +41,20 @@ input[type=submit], button {
} }
} }
} }
.breadcrumb {
@include breadcrumb;
}
.dashboard {
@include dashboard;
max-width: 900px;
}
my-actor-avatar-info ::ng-deep {
.actor-img-edit-container,
.actor-info-followers,
.actor-info-username {
display: none;
}
}

View File

@ -4,12 +4,14 @@ import { ServerConfig, USER_ROLE_LABELS, UserRole, VideoResolution } from '../..
import { ConfigService } from '@app/+admin/config/shared/config.service' import { ConfigService } from '@app/+admin/config/shared/config.service'
import { UserAdminFlag } from '@shared/models/users/user-flag.model' import { UserAdminFlag } from '@shared/models/users/user-flag.model'
import { OnInit } from '@angular/core' import { OnInit } from '@angular/core'
import { User } from '@app/shared/users/user.model'
import { ScreenService } from '@app/shared/misc/screen.service'
export abstract class UserEdit extends FormReactive implements OnInit { export abstract class UserEdit extends FormReactive implements OnInit {
videoQuotaOptions: { value: string, label: string }[] = [] videoQuotaOptions: { value: string, label: string }[] = []
videoQuotaDailyOptions: { value: string, label: string }[] = [] videoQuotaDailyOptions: { value: string, label: string }[] = []
username: string username: string
userId: number user: User
roles: { value: string, label: string }[] = [] roles: { value: string, label: string }[] = []
@ -17,6 +19,7 @@ export abstract class UserEdit extends FormReactive implements OnInit {
protected abstract serverService: ServerService protected abstract serverService: ServerService
protected abstract configService: ConfigService protected abstract configService: ConfigService
protected abstract screenService: ScreenService
protected abstract auth: AuthService protected abstract auth: AuthService
abstract isCreation (): boolean abstract isCreation (): boolean
abstract getFormButtonTitle (): string abstract getFormButtonTitle (): string
@ -29,6 +32,20 @@ export abstract class UserEdit extends FormReactive implements OnInit {
this.buildRoles() this.buildRoles()
} }
get subscribersCount () {
const forAccount = this.user
? this.user.account.followersCount
: 0
const forChannels = this.user
? this.user.videoChannels.map(c => c.followersCount).reduce((a, b) => a + b, 0)
: 0
return forAccount + forChannels
}
isInBigView () {
return this.screenService.getWindowInnerWidth() > 1600
}
buildRoles () { buildRoles () {
const authUser = this.auth.getUser() const authUser = this.auth.getUser()

View File

@ -23,8 +23,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
constructor ( constructor (
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
private userValidatorsService: UserValidatorsService, private userValidatorsService: UserValidatorsService,
private route: ActivatedRoute,
private router: Router,
private notifier: Notifier, private notifier: Notifier,
private userService: UserService, private userService: UserService,
private i18n: I18n private i18n: I18n

View File

@ -4,13 +4,15 @@ import { Subscription } from 'rxjs'
import { AuthService, Notifier } from '@app/core' import { AuthService, Notifier } from '@app/core'
import { ServerService } from '../../../core' import { ServerService } from '../../../core'
import { UserEdit } from './user-edit' import { UserEdit } from './user-edit'
import { User, UserUpdate } from '../../../../../../shared' import { User as UserType, UserUpdate, UserRole } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
import { ConfigService } from '@app/+admin/config/shared/config.service' import { ConfigService } from '@app/+admin/config/shared/config.service'
import { UserService } from '@app/shared' import { UserService } from '@app/shared'
import { UserAdminFlag } from '@shared/models/users/user-flag.model' import { UserAdminFlag } from '@shared/models/users/user-flag.model'
import { User } from '@app/shared/users/user.model'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({ @Component({
selector: 'my-user-update', selector: 'my-user-update',
@ -19,9 +21,6 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model'
}) })
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
error: string error: string
userId: number
userEmail: string
username: string
private paramsSub: Subscription private paramsSub: Subscription
@ -29,6 +28,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
protected serverService: ServerService, protected serverService: ServerService,
protected configService: ConfigService, protected configService: ConfigService,
protected screenService: ScreenService,
protected auth: AuthService, protected auth: AuthService,
private userValidatorsService: UserValidatorsService, private userValidatorsService: UserValidatorsService,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -45,7 +45,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
ngOnInit () { ngOnInit () {
super.ngOnInit() super.ngOnInit()
const defaultValues = { videoQuota: '-1', videoQuotaDaily: '-1' } const defaultValues = {
role: UserRole.USER.toString(),
videoQuota: '-1',
videoQuotaDaily: '-1'
}
this.buildForm({ this.buildForm({
email: this.userValidatorsService.USER_EMAIL, email: this.userValidatorsService.USER_EMAIL,
role: this.userValidatorsService.USER_ROLE, role: this.userValidatorsService.USER_ROLE,
@ -56,7 +61,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
this.paramsSub = this.route.params.subscribe(routeParams => { this.paramsSub = this.route.params.subscribe(routeParams => {
const userId = routeParams['id'] const userId = routeParams['id']
this.userService.getUser(userId).subscribe( this.userService.getUser(userId, true).subscribe(
user => this.onUserFetched(user), user => this.onUserFetched(user),
err => this.error = err.message err => this.error = err.message
@ -78,9 +83,9 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10)
this.userService.updateUser(this.userId, userUpdate).subscribe( this.userService.updateUser(this.user.id, userUpdate).subscribe(
() => { () => {
this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username })) this.notifier.success(this.i18n('User {{user.username}} updated.', { username: this.user.username }))
this.router.navigate([ '/admin/users/list' ]) this.router.navigate([ '/admin/users/list' ])
}, },
@ -101,10 +106,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
} }
resetPassword () { resetPassword () {
this.userService.askResetPassword(this.userEmail).subscribe( this.userService.askResetPassword(this.user.email).subscribe(
() => { () => {
this.notifier.success( this.notifier.success(
this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.user.username })
) )
}, },
@ -112,14 +117,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
) )
} }
private onUserFetched (userJson: User) { private onUserFetched (userJson: UserType) {
this.userId = userJson.id this.user = new User(userJson)
this.username = userJson.username
this.userEmail = userJson.email
this.form.patchValue({ this.form.patchValue({
email: userJson.email, email: userJson.email,
role: userJson.role, role: userJson.role.toString(),
videoQuota: userJson.videoQuota, videoQuota: userJson.videoQuota,
videoQuotaDaily: userJson.videoQuotaDaily, videoQuotaDaily: userJson.videoQuotaDaily,
byPassAutoBlacklist: userJson.adminFlags & UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST byPassAutoBlacklist: userJson.adminFlags & UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST

View File

@ -15,7 +15,6 @@ import { MyAccountProfileComponent } from '@app/+my-account/my-account-settings/
import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component'
import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
@ -63,7 +62,6 @@ import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-setti
MyAccountVideoChannelsComponent, MyAccountVideoChannelsComponent,
MyAccountVideoChannelCreateComponent, MyAccountVideoChannelCreateComponent,
MyAccountVideoChannelUpdateComponent, MyAccountVideoChannelUpdateComponent,
ActorAvatarInfoComponent,
MyAccountVideoImportsComponent, MyAccountVideoImportsComponent,
MyAccountDangerZoneComponent, MyAccountDangerZoneComponent,
MyAccountSubscriptionsComponent, MyAccountSubscriptionsComponent,

View File

@ -1,14 +1,17 @@
<ng-container *ngIf="actor"> <ng-container *ngIf="actor">
<div class="actor"> <div class="actor">
<img [src]="actor.avatarUrl" alt="Avatar" /> <div class="d-flex">
<img [src]="actor.avatarUrl" alt="Avatar" />
<div class="actor-img-edit-container"> <div class="actor-img-edit-container">
<div class="actor-img-edit-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body"> <div class="actor-img-edit-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body">
<my-global-icon iconName="edit"></my-global-icon> <my-global-icon iconName="edit"></my-global-icon>
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/> <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
</div>
</div> </div>
</div> </div>
<div class="actor-info"> <div class="actor-info">
<div class="actor-info-names"> <div class="actor-info-names">
<div class="actor-info-display-name">{{ actor.displayName }}</div> <div class="actor-info-display-name">{{ actor.displayName }}</div>

View File

@ -106,6 +106,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
@NgModule({ @NgModule({
imports: [ imports: [
@ -189,7 +190,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
PreviewUploadComponent, PreviewUploadComponent,
MyAccountVideoSettingsComponent, MyAccountVideoSettingsComponent,
MyAccountInterfaceSettingsComponent MyAccountInterfaceSettingsComponent,
ActorAvatarInfoComponent
], ],
exports: [ exports: [
@ -270,7 +272,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
VideoDurationPipe, VideoDurationPipe,
MyAccountVideoSettingsComponent, MyAccountVideoSettingsComponent,
MyAccountInterfaceSettingsComponent MyAccountInterfaceSettingsComponent,
ActorAvatarInfoComponent
], ],
providers: [ providers: [

View File

@ -51,6 +51,11 @@ export class User implements UserServerModel {
videoQuotaDaily: number videoQuotaDaily: number
videoQuotaUsed?: number videoQuotaUsed?: number
videoQuotaUsedDaily?: number videoQuotaUsedDaily?: number
videosCount?: number
videoAbusesCount?: number
videoAbusesAcceptedCount?: number
videoAbusesCreatedCount?: number
videoCommentsCount?: number
theme: string theme: string
@ -79,6 +84,11 @@ export class User implements UserServerModel {
this.videoQuotaDaily = hash.videoQuotaDaily this.videoQuotaDaily = hash.videoQuotaDaily
this.videoQuotaUsed = hash.videoQuotaUsed this.videoQuotaUsed = hash.videoQuotaUsed
this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
this.videosCount = hash.videosCount
this.videoAbusesCount = hash.videoAbusesCount
this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount
this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount
this.videoCommentsCount = hash.videoCommentsCount
this.nsfwPolicy = hash.nsfwPolicy this.nsfwPolicy = hash.nsfwPolicy
this.webTorrentEnabled = hash.webTorrentEnabled this.webTorrentEnabled = hash.webTorrentEnabled

View File

@ -234,8 +234,9 @@ export class UserService {
return this.userCache[userId] return this.userCache[userId]
} }
getUser (userId: number) { getUser (userId: number, withStats = false) {
return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId) const params = new HttpParams().append('withStats', withStats + '')
return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId, { params })
.pipe(catchError(err => this.restExtractor.handleError(err))) .pipe(catchError(err => this.restExtractor.handleError(err)))
} }

View File

@ -7,8 +7,7 @@
<div class="modal-body"> <div class="modal-body">
<div i18n class="information"> <div i18n class="information">
Your report will be sent to moderators of {{ currentHost }}. Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
<ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
</div> </div>
<form novalidate [formGroup]="form" (ngSubmit)="report()"> <form novalidate [formGroup]="form" (ngSubmit)="report()">

View File

@ -621,3 +621,85 @@
} }
} }
} }
@mixin breadcrumb {
display: flex;
flex-wrap: wrap;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
list-style: none;
background-color: var(--submenuColor);
border-radius: 0.25rem;
.breadcrumb-item {
display: flex;
a {
color: var(--mainColor);
}
& + .breadcrumb-item {
padding-left: 0.5rem;
&::before {
display: inline-block;
padding-right: 0.5rem;
color: #6c757d;
content: "/";
}
}
&.active {
color: #6c757d;
}
}
}
@mixin dashboard {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
& > div {
box-sizing: border-box;
flex: 0 0 percentage(1/3);
padding: 0 5px;
margin-bottom: 10px;
& > a {
text-decoration: none;
color: inherit;
display: block;
font-size: 18px;
&:active,
&:focus,
&:hover {
opacity: .8;
}
}
& > a,
& > div {
padding: 20px;
background: var(--submenuColor);
border-radius: 4px;
box-sizing: border-box;
height: 100%;
}
}
.dashboard-num, .dashboard-text {
text-align: center;
font-size: 130%;
line-height: 21px;
color: var(--mainForegroundColor);
line-height: 30px;
margin-bottom: 20px;
}
.dashboard-label {
font-size: 90%;
color: var(--inputPlaceholderColor);
text-align: center;
}
}

View File

@ -1,6 +1,6 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import * as express from 'express' import * as express from 'express'
import { body, param } from 'express-validator' import { body, param, query } from 'express-validator'
import { omit } from 'lodash' import { omit } from 'lodash'
import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { import {
@ -256,12 +256,13 @@ const usersUpdateMeValidator = [
const usersGetValidator = [ const usersGetValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersGet parameters', { parameters: req.params }) logger.debug('Checking usersGet parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await checkUserIdExist(req.params.id, res)) return if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
return next() return next()
} }
@ -460,9 +461,9 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function checkUserIdExist (idArg: number | string, res: express.Response) { function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
const id = parseInt(idArg + '', 10) const id = parseInt(idArg + '', 10)
return checkUserExist(() => UserModel.loadById(id), res) return checkUserExist(() => UserModel.loadById(id, withStats), res)
} }
function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {

View File

@ -19,7 +19,7 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared' import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy, VideoAbuseState } from '../../../shared'
import { User, UserRole } from '../../../shared/models/users' import { User, UserRole } from '../../../shared/models/users'
import { import {
isNoInstanceConfigWarningModal, isNoInstanceConfigWarningModal,
@ -70,8 +70,26 @@ import {
MVideoFullLight MVideoFullLight
} from '@server/typings/models' } from '@server/typings/models'
const literalVideoQuotaUsed: any = [
literal(
'(' +
'SELECT COALESCE(SUM("size"), 0) ' +
'FROM (' +
'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
') t' +
')'
),
'videoQuotaUsed'
]
enum ScopeNames { enum ScopeNames {
FOR_ME_API = 'FOR_ME_API' FOR_ME_API = 'FOR_ME_API',
WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
WITH_STATS = 'WITH_STATS'
} }
@DefaultScope(() => ({ @DefaultScope(() => ({
@ -112,6 +130,86 @@ enum ScopeNames {
required: true required: true
} }
] ]
},
[ScopeNames.WITH_VIDEOCHANNELS]: {
include: [
{
model: AccountModel,
include: [
{
model: VideoChannelModel
},
{
attributes: [ 'id', 'name', 'type' ],
model: VideoPlaylistModel.unscoped(),
required: true,
where: {
type: {
[Op.ne]: VideoPlaylistType.REGULAR
}
}
}
]
}
]
},
[ScopeNames.WITH_STATS]: {
attributes: {
include: [
literalVideoQuotaUsed,
[
literal(
'(' +
'SELECT COUNT("video"."id") ' +
'FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
')'
),
'videosCount'
],
[
literal(
'(' +
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
'FROM (' +
'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
`COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
'FROM "videoAbuse" ' +
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
') t' +
')'
),
'videoAbusesCount'
],
[
literal(
'(' +
'SELECT COUNT("videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
')'
),
'videoAbusesCreatedCount'
],
[
literal(
'(' +
'SELECT COUNT("videoComment"."id") ' +
'FROM "videoComment" ' +
'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
'WHERE "account"."userId" = "UserModel"."id"' +
')'
),
'videoCommentsCount'
]
]
}
} }
})) }))
@Table({ @Table({
@ -332,23 +430,7 @@ export class UserModel extends Model<UserModel> {
const query: FindOptions = { const query: FindOptions = {
attributes: { attributes: {
include: [ include: [ literalVideoQuotaUsed ]
[
literal(
'(' +
'SELECT COALESCE(SUM("size"), 0) ' +
'FROM (' +
'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
') t' +
')'
),
'videoQuotaUsed'
]
]
}, },
offset: start, offset: start,
limit: count, limit: count,
@ -430,8 +512,14 @@ export class UserModel extends Model<UserModel> {
return UserModel.findAll(query) return UserModel.findAll(query)
} }
static loadById (id: number): Bluebird<MUserDefault> { static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
return UserModel.findByPk(id) const scopes = [
ScopeNames.WITH_VIDEOCHANNELS
]
if (withStats) scopes.push(ScopeNames.WITH_STATS)
return UserModel.scope(scopes).findByPk(id)
} }
static loadByUsername (username: string): Bluebird<MUserDefault> { static loadByUsername (username: string): Bluebird<MUserDefault> {
@ -637,6 +725,10 @@ export class UserModel extends Model<UserModel> {
toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
const videoQuotaUsed = this.get('videoQuotaUsed') const videoQuotaUsed = this.get('videoQuotaUsed')
const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
const videosCount = this.get('videosCount')
const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
const videoCommentsCount = this.get('videoCommentsCount')
const json: User = { const json: User = {
id: this.id, id: this.id,
@ -666,6 +758,21 @@ export class UserModel extends Model<UserModel> {
videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
? parseInt(videoQuotaUsedDaily + '', 10) ? parseInt(videoQuotaUsedDaily + '', 10)
: undefined, : undefined,
videosCount: videosCount !== undefined
? parseInt(videosCount + '', 10)
: undefined,
videoAbusesCount: videoAbusesCount
? parseInt(videoAbusesCount, 10)
: undefined,
videoAbusesAcceptedCount: videoAbusesAcceptedCount
? parseInt(videoAbusesAcceptedCount, 10)
: undefined,
videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
? parseInt(videoAbusesCreatedCount + '', 10)
: undefined,
videoCommentsCount: videoCommentsCount !== undefined
? parseInt(videoCommentsCount + '', 10)
: undefined,
noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
noWelcomeModal: this.noWelcomeModal, noWelcomeModal: this.noWelcomeModal,

View File

@ -2,7 +2,7 @@
import * as chai from 'chai' import * as chai from 'chai'
import 'mocha' import 'mocha'
import { MyUser, User, UserRole, Video, VideoPlaylistType } from '../../../../shared/index' import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index'
import { import {
blockUser, blockUser,
cleanupTests, cleanupTests,
@ -33,7 +33,11 @@ import {
updateMyUser, updateMyUser,
updateUser, updateUser,
uploadVideo, uploadVideo,
userLogin userLogin,
reportVideoAbuse,
addVideoCommentThread,
updateVideoAbuse,
getVideoAbusesList
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { follow } from '../../../../shared/extra-utils/server/follows' import { follow } from '../../../../shared/extra-utils/server/follows'
import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@ -254,7 +258,7 @@ describe('Test users', function () {
const res1 = await getMyUserInformation(server.url, accessTokenUser) const res1 = await getMyUserInformation(server.url, accessTokenUser)
const userMe: MyUser = res1.body const userMe: MyUser = res1.body
const res2 = await getUserInformation(server.url, server.accessToken, userMe.id) const res2 = await getUserInformation(server.url, server.accessToken, userMe.id, true)
const userGet: User = res2.body const userGet: User = res2.body
for (const user of [ userMe, userGet ]) { for (const user of [ userMe, userGet ]) {
@ -273,6 +277,16 @@ describe('Test users', function () {
expect(userMe.specialPlaylists).to.have.lengthOf(1) expect(userMe.specialPlaylists).to.have.lengthOf(1)
expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER)
// Check stats are included with withStats
expect(userGet.videosCount).to.be.a('number')
expect(userGet.videosCount).to.equal(0)
expect(userGet.videoCommentsCount).to.be.a('number')
expect(userGet.videoCommentsCount).to.equal(0)
expect(userGet.videoAbusesCount).to.be.a('number')
expect(userGet.videoAbusesCount).to.equal(0)
expect(userGet.videoAbusesAcceptedCount).to.be.a('number')
expect(userGet.videoAbusesAcceptedCount).to.equal(0)
}) })
}) })
@ -623,7 +637,6 @@ describe('Test users', function () {
}) })
describe('Updating another user', function () { describe('Updating another user', function () {
it('Should be able to update another user', async function () { it('Should be able to update another user', async function () {
await updateUser({ await updateUser({
url: server.url, url: server.url,
@ -698,6 +711,8 @@ describe('Test users', function () {
}) })
describe('Registering a new user', function () { describe('Registering a new user', function () {
let user15AccessToken
it('Should register a new user', async function () { it('Should register a new user', async function () {
const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
@ -711,18 +726,18 @@ describe('Test users', function () {
password: 'my super password' password: 'my super password'
} }
accessToken = await userLogin(server, user15) user15AccessToken = await userLogin(server, user15)
}) })
it('Should have the correct display name', async function () { it('Should have the correct display name', async function () {
const res = await getMyUserInformation(server.url, accessToken) const res = await getMyUserInformation(server.url, user15AccessToken)
const user: User = res.body const user: User = res.body
expect(user.account.displayName).to.equal('super user 15') expect(user.account.displayName).to.equal('super user 15')
}) })
it('Should have the correct video quota', async function () { it('Should have the correct video quota', async function () {
const res = await getMyUserInformation(server.url, accessToken) const res = await getMyUserInformation(server.url, user15AccessToken)
const user = res.body const user = res.body
expect(user.videoQuota).to.equal(5 * 1024 * 1024) expect(user.videoQuota).to.equal(5 * 1024 * 1024)
@ -740,7 +755,7 @@ describe('Test users', function () {
expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
} }
await deleteMe(server.url, accessToken) await deleteMe(server.url, user15AccessToken)
{ {
const res = await getUsersList(server.url, server.accessToken) const res = await getUsersList(server.url, server.accessToken)
@ -750,6 +765,9 @@ describe('Test users', function () {
}) })
describe('User blocking', function () { describe('User blocking', function () {
let user16Id
let user16AccessToken
it('Should block and unblock a user', async function () { it('Should block and unblock a user', async function () {
const user16 = { const user16 = {
username: 'user_16', username: 'user_16',
@ -761,19 +779,95 @@ describe('Test users', function () {
username: user16.username, username: user16.username,
password: user16.password password: user16.password
}) })
const user16Id = resUser.body.user.id user16Id = resUser.body.user.id
accessToken = await userLogin(server, user16) user16AccessToken = await userLogin(server, user16)
await getMyUserInformation(server.url, accessToken, 200) await getMyUserInformation(server.url, user16AccessToken, 200)
await blockUser(server.url, user16Id, server.accessToken) await blockUser(server.url, user16Id, server.accessToken)
await getMyUserInformation(server.url, accessToken, 401) await getMyUserInformation(server.url, user16AccessToken, 401)
await userLogin(server, user16, 400) await userLogin(server, user16, 400)
await unblockUser(server.url, user16Id, server.accessToken) await unblockUser(server.url, user16Id, server.accessToken)
accessToken = await userLogin(server, user16) user16AccessToken = await userLogin(server, user16)
await getMyUserInformation(server.url, accessToken, 200) await getMyUserInformation(server.url, user16AccessToken, 200)
})
})
describe('User stats', function () {
let user17Id
let user17AccessToken
it('Should report correct initial statistics about a user', async function () {
const user17 = {
username: 'user_17',
password: 'my super password'
}
const resUser = await createUser({
url: server.url,
accessToken: server.accessToken,
username: user17.username,
password: user17.password
})
user17Id = resUser.body.user.id
user17AccessToken = await userLogin(server, user17)
const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user: User = res.body
expect(user.videosCount).to.equal(0)
expect(user.videoCommentsCount).to.equal(0)
expect(user.videoAbusesCount).to.equal(0)
expect(user.videoAbusesCreatedCount).to.equal(0)
expect(user.videoAbusesAcceptedCount).to.equal(0)
})
it('Should report correct videos count', async function () {
const videoAttributes = {
name: 'video to test user stats'
}
await uploadVideo(server.url, user17AccessToken, videoAttributes)
const res1 = await getVideosList(server.url)
videoId = res1.body.data.find(video => video.name === videoAttributes.name).id
const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user: User = res2.body
expect(user.videosCount).to.equal(1)
})
it('Should report correct video comments for user', async function () {
const text = 'super comment'
await addVideoCommentThread(server.url, user17AccessToken, videoId, text)
const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user: User = res.body
expect(user.videoCommentsCount).to.equal(1)
})
it('Should report correct video abuses counts', async function () {
const reason = 'my super bad reason'
await reportVideoAbuse(server.url, user17AccessToken, videoId, reason)
const res1 = await getVideoAbusesList(server.url, server.accessToken)
const abuseId = res1.body.data[0].id
const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user2: User = res2.body
expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED }
await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
const user3: User = res3.body
expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted
}) })
}) })

View File

@ -130,11 +130,12 @@ function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatu
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getUserInformation (url: string, accessToken: string, userId: number) { function getUserInformation (url: string, accessToken: string, userId: number, withStats = false) {
const path = '/api/v1/users/' + userId const path = '/api/v1/users/' + userId
return request(url) return request(url)
.get(path) .get(path)
.query({ withStats })
.set('Accept', 'application/json') .set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + accessToken) .set('Authorization', 'Bearer ' + accessToken)
.expect(200) .expect(200)

View File

@ -31,6 +31,11 @@ export interface User {
videoQuotaDaily: number videoQuotaDaily: number
videoQuotaUsed?: number videoQuotaUsed?: number
videoQuotaUsedDaily?: number videoQuotaUsedDaily?: number
videosCount?: number
videoAbusesCount?: number
videoAbusesAcceptedCount?: number
videoAbusesCreatedCount?: number
videoCommentsCount? : number
theme: string theme: string