]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add overview of a user's actions in user-edit (#2558)
authorRigel Kent <sendmemail@rigelk.eu>
Fri, 27 Mar 2020 14:19:03 +0000 (15:19 +0100)
committerGitHub <noreply@github.com>
Fri, 27 Mar 2020 14:19:03 +0000 (15:19 +0100)
20 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
client/src/app/+admin/users/user-edit/user-create.component.ts
client/src/app/+admin/users/user-edit/user-edit.component.html
client/src/app/+admin/users/user-edit/user-edit.component.scss
client/src/app/+admin/users/user-edit/user-edit.ts
client/src/app/+admin/users/user-edit/user-password.component.ts
client/src/app/+admin/users/user-edit/user-update.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/+my-account/shared/actor-avatar-info.component.html
client/src/app/shared/shared.module.ts
client/src/app/shared/users/user.model.ts
client/src/app/shared/users/user.service.ts
client/src/app/shared/video/modals/video-report.component.html
client/src/sass/include/_mixins.scss
server/middlewares/validators/users.ts
server/models/account/user.ts
server/tests/api/users/users.ts
shared/extra-utils/users/users.ts
shared/models/users/user.model.ts

index b3b4f7728603568d2ef46d1ade53eb3c2dacaaa3..9991e1f6338d0263bbbb402275e46cede30c949b 100644 (file)
                         i18n-labelText labelText="Allow additional extensions"
                       >
                         <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>
                       </my-peertube-checkbox>
                     </div>
                         i18n-labelText labelText="Allow audio files upload"
                       >
                         <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>
                       </my-peertube-checkbox>
                     </div>
index 8b1cdcbbad7ff50b2f3282886e6e1f0507a8215c..d8bc30d5596fbd1f9745920e2cd0e36b0299cda9 100644 (file)
@@ -50,6 +50,7 @@ input[type=submit] {
 textarea {
   @include peertube-textarea(500px, 150px);
 
+  max-width: 100%;
   display: block;
 
   &.small {
@@ -72,6 +73,10 @@ my-markdown-textarea ::ng-deep {
     @media screen and (max-width: 1400px) {
       flex-direction: column !important;
     }
+
+    textarea {
+      max-width: 100%;
+    }
   }
 }
 
index 1769c0de0c3a117e6eab3c9362efe87d4d088197..a394418cb1867115783611e31b41353a621ea89b 100644 (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 { ConfigService } from '@app/+admin/config/shared/config.service'
 import { UserService } from '@app/shared'
+import { ScreenService } from '@app/shared/misc/screen.service'
 
 @Component({
   selector: 'my-user-create',
@@ -21,6 +22,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
     protected serverService: ServerService,
     protected formValidatorService: FormValidatorService,
     protected configService: ConfigService,
+    protected screenService: ScreenService,
     protected auth: AuthService,
     private userValidatorsService: UserValidatorsService,
     private route: ActivatedRoute,
index dbb0e36b903dd97a1e273fa77907df0681f51091..6c42fde570b908e0efb0c16ebac58144e49e25ff 100644 (file)
-<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create user</div>
-<div i18n class="form-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
+<nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item">
+      <a routerLink="/admin/users" i18n>Users</a>
+    </li>
 
-<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+    <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>
 
-<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
-  <div class="form-group" *ngIf="isCreation()">
-    <label i18n for="username">Username</label>
-    <input
-      type="text" id="username" i18n-placeholder placeholder="john"
-      formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
-    >
-    <div *ngIf="formErrors.username" class="form-error">
-      {{ formErrors.username }}
+<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>
-
-  <div class="form-group">
-    <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>
+      <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>
-
-  <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>
+      <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-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 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 *ngIf="formErrors.role" class="form-error">
-      {{ formErrors.role }}
-    </div>
+  <div class="form-group-right col-12 col-xl-9">
+    <ng-template *ngTemplateOutlet="dashboard"></ng-template>
   </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 *ngIf="error" class="alert alert-danger">{{ error }}</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 class="form-row mt-4"> <!-- user grid -->
+  <div class="form-group col-12 col-lg-4 col-xl-3">
+    <div class="anchor" id="user"></div> <!-- user anchor -->
+    <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
+    <div *ngIf="!isCreation() && user" class="account-title">
+      <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info>
     </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 class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }">
+
+    <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }">
+      <div class="form-group" *ngIf="isCreation()">
+        <label i18n for="username">Username</label>
+        <input
+          type="text" id="username" i18n-placeholder placeholder="john"
+          formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
+        >
+        <div *ngIf="formErrors.username" class="form-error">
+          {{ formErrors.username }}
+        </div>
+      </div>
+  
+      <div class="form-group">
+        <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()">
+        <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 *ngIf="isInBigView()" class="col-7">
+      <ng-template *ngTemplateOutlet="dashboard"></ng-template>
     </div>
+
   </div>
+</div>
+
 
-  <div class="form-group">
-    <my-peertube-checkbox
-      inputName="byPassAutoBlacklist" formControlName="byPassAutoBlacklist"
-      i18n-labelText labelText="Bypass video auto blacklist"
-    ></my-peertube-checkbox>
+<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>
 
-  <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
-</form>
+  <div class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }">
 
-<div *ngIf="!isCreation()" class="danger-zone">
-  <div class="account-title" i18n>Danger Zone</div>
+    <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 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 class="form-group">
-    <label i18n>Manually set the user password</label>
-    <my-user-password [userId]="userId"></my-user-password>
   </div>
 </div>
index c1cc4ca4575e484cfd1d5d376237a9648c61e987..d4c1b600e83888dc2d92a6219c109845a8df376c 100644 (file)
@@ -1,8 +1,13 @@
 @import '_variables';
 @import '_mixins';
 
-.form-sub-title {
-  margin-bottom: 30px;
+label {
+  font-weight: $font-regular;
+  font-size: 100%;
+}
+
+.account-title {
+  @include settings-big-title;
 }
 
 input:not([type=submit]) {
@@ -26,18 +31,9 @@ input[type=submit], button {
   font-size: 11px;
 }
 
-.account-title {
-  @include in-content-small-title;
-
-  margin-top: 55px;
-  margin-bottom: 30px;
-}
-
 .danger-zone {
   .reset-password-email {
     margin-bottom: 30px;
-    padding-bottom: 30px;
-    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 
     button {
       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;
+  }
+}
index 47b57d2ec6e696250a7ab78fd09bd3aaa4cf30b3..a23cd9033c4ff0414bcb86ce872cfedf27f53ec0 100644 (file)
@@ -4,12 +4,14 @@ import { ServerConfig, USER_ROLE_LABELS, UserRole, VideoResolution } from '../..
 import { ConfigService } from '@app/+admin/config/shared/config.service'
 import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 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 {
   videoQuotaOptions: { value: string, label: string }[] = []
   videoQuotaDailyOptions: { value: string, label: string }[] = []
   username: string
-  userId: number
+  user: User
 
   roles: { value: string, label: string }[] = []
 
@@ -17,6 +19,7 @@ export abstract class UserEdit extends FormReactive implements OnInit {
 
   protected abstract serverService: ServerService
   protected abstract configService: ConfigService
+  protected abstract screenService: ScreenService
   protected abstract auth: AuthService
   abstract isCreation (): boolean
   abstract getFormButtonTitle (): string
@@ -29,6 +32,20 @@ export abstract class UserEdit extends FormReactive implements OnInit {
     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 () {
     const authUser = this.auth.getUser()
 
index 5b30404405460ecd5e899353db483ee63abd0308..ecad000f79b0327100266a1ea25e54f359d91aec 100644 (file)
@@ -23,8 +23,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
   constructor (
     protected formValidatorService: FormValidatorService,
     private userValidatorsService: UserValidatorsService,
-    private route: ActivatedRoute,
-    private router: Router,
     private notifier: Notifier,
     private userService: UserService,
     private i18n: I18n
index 1ab2e9dbfec7e88f3ff1dbe817d4ada37a1eb342..fbe3d695035fd84a4615a4029227973493723e6d 100644 (file)
@@ -4,13 +4,15 @@ import { Subscription } from 'rxjs'
 import { AuthService, Notifier } from '@app/core'
 import { ServerService } from '../../../core'
 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 { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
 import { ConfigService } from '@app/+admin/config/shared/config.service'
 import { UserService } from '@app/shared'
 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({
   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 {
   error: string
-  userId: number
-  userEmail: string
-  username: string
 
   private paramsSub: Subscription
 
@@ -29,6 +28,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
     protected formValidatorService: FormValidatorService,
     protected serverService: ServerService,
     protected configService: ConfigService,
+    protected screenService: ScreenService,
     protected auth: AuthService,
     private userValidatorsService: UserValidatorsService,
     private route: ActivatedRoute,
@@ -45,7 +45,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
   ngOnInit () {
     super.ngOnInit()
 
-    const defaultValues = { videoQuota: '-1', videoQuotaDaily: '-1' }
+    const defaultValues = {
+      role: UserRole.USER.toString(),
+      videoQuota: '-1',
+      videoQuotaDaily: '-1'
+    }
+
     this.buildForm({
       email: this.userValidatorsService.USER_EMAIL,
       role: this.userValidatorsService.USER_ROLE,
@@ -56,7 +61,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
 
     this.paramsSub = this.route.params.subscribe(routeParams => {
       const userId = routeParams['id']
-      this.userService.getUser(userId).subscribe(
+      this.userService.getUser(userId, true).subscribe(
         user => this.onUserFetched(user),
 
         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.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' ])
       },
 
@@ -101,10 +106,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
   }
 
   resetPassword () {
-    this.userService.askResetPassword(this.userEmail).subscribe(
+    this.userService.askResetPassword(this.user.email).subscribe(
       () => {
         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) {
-    this.userId = userJson.id
-    this.username = userJson.username
-    this.userEmail = userJson.email
+  private onUserFetched (userJson: UserType) {
+    this.user = new User(userJson)
 
     this.form.patchValue({
       email: userJson.email,
-      role: userJson.role,
+      role: userJson.role.toString(),
       videoQuota: userJson.videoQuota,
       videoQuotaDaily: userJson.videoQuotaDaily,
       byPassAutoBlacklist: userJson.adminFlags & UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST
index db8ffac1699391d1e6be9509aa375e1a0db881c6..f8c04cb4d23299604b618943ad3caa17a8799265 100644 (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 { 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 { 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 { 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'
@@ -63,7 +62,6 @@ import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-setti
     MyAccountVideoChannelsComponent,
     MyAccountVideoChannelCreateComponent,
     MyAccountVideoChannelUpdateComponent,
-    ActorAvatarInfoComponent,
     MyAccountVideoImportsComponent,
     MyAccountDangerZoneComponent,
     MyAccountSubscriptionsComponent,
index 2050950be0d1d5b90293a83f11dc145d17bf3016..82f5123de98841a3de3bcd4c31c5778174e88785 100644 (file)
@@ -1,14 +1,17 @@
 <ng-container *ngIf="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-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body">
-        <my-global-icon iconName="edit"></my-global-icon>
-        <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
+      <div class="actor-img-edit-container">
+        <div class="actor-img-edit-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body">
+          <my-global-icon iconName="edit"></my-global-icon>
+          <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
+        </div>
       </div>
     </div>
 
+
     <div class="actor-info">
       <div class="actor-info-names">
         <div class="actor-info-display-name">{{ actor.displayName }}</div>
index 75aa30dab7acca52c842c91f9cc340db0b5115bb..b89f0a8d105f6546df647bdd5f30e6f8aab52439 100644 (file)
@@ -106,6 +106,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
 
 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 { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
 
 @NgModule({
   imports: [
@@ -189,7 +190,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
     PreviewUploadComponent,
 
     MyAccountVideoSettingsComponent,
-    MyAccountInterfaceSettingsComponent
+    MyAccountInterfaceSettingsComponent,
+    ActorAvatarInfoComponent
   ],
 
   exports: [
@@ -270,7 +272,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
     VideoDurationPipe,
 
     MyAccountVideoSettingsComponent,
-    MyAccountInterfaceSettingsComponent
+    MyAccountInterfaceSettingsComponent,
+    ActorAvatarInfoComponent
   ],
 
   providers: [
index a37cae749c1e64d596111c4a680651e1cb09ed8e..76c57d2fb840037337b494a856fb7bb3ed3a1273 100644 (file)
@@ -51,6 +51,11 @@ export class User implements UserServerModel {
   videoQuotaDaily: number
   videoQuotaUsed?: number
   videoQuotaUsedDaily?: number
+  videosCount?: number
+  videoAbusesCount?: number
+  videoAbusesAcceptedCount?: number
+  videoAbusesCreatedCount?: number
+  videoCommentsCount?: number
 
   theme: string
 
@@ -79,6 +84,11 @@ export class User implements UserServerModel {
     this.videoQuotaDaily = hash.videoQuotaDaily
     this.videoQuotaUsed = hash.videoQuotaUsed
     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.webTorrentEnabled = hash.webTorrentEnabled
index a7934364686476d95f1a7f834616add83874fcaa..5442a8453ca077d7c32c36ccf913f69a96bd5260 100644 (file)
@@ -234,8 +234,9 @@ export class UserService {
     return this.userCache[userId]
   }
 
-  getUser (userId: number) {
-    return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId)
+  getUser (userId: number, withStats = false) {
+    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)))
   }
 
index b9434da26ce461f7d8dcc76d6d2c4bb3aedbd7e8..cc1d361b3d1dffe91569d66a67f217eabc295b35 100644 (file)
@@ -7,8 +7,7 @@
   <div class="modal-body">
 
     <div i18n class="information">
-      Your report will be sent to moderators of {{ currentHost }}.
-      <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
+      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>.
     </div>
 
     <form novalidate [formGroup]="form" (ngSubmit)="report()">
index e8dfb79bc2ec427c83ab64822ff36dc85e441db9..f96a43b34387113b78fed75cf2d0799d8d486c60 100644 (file)
     }
   }
 }
+
+@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;
+  }
+}
index adc67a0463ecda2dfac02c10ae0adef06492a659..840b9fc744379e2a13039efafeb5d31b98824e30 100644 (file)
@@ -1,6 +1,6 @@
 import * as Bluebird from 'bluebird'
 import * as express from 'express'
-import { body, param } from 'express-validator'
+import { body, param, query } from 'express-validator'
 import { omit } from 'lodash'
 import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import {
@@ -256,12 +256,13 @@ const usersUpdateMeValidator = [
 
 const usersGetValidator = [
   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) => {
     logger.debug('Checking usersGet parameters', { parameters: req.params })
 
     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()
   }
@@ -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)
-  return checkUserExist(() => UserModel.loadById(id), res)
+  return checkUserExist(() => UserModel.loadById(id, withStats), res)
 }
 
 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
index 777f0966680094e86e2b24613d6e5e6b523625fb..026bf1318f1d1e23e4b24166964a0a13da20c0d6 100644 (file)
@@ -19,7 +19,7 @@ import {
   Table,
   UpdatedAt
 } 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 {
   isNoInstanceConfigWarningModal,
@@ -70,8 +70,26 @@ import {
   MVideoFullLight
 } 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 {
-  FOR_ME_API = 'FOR_ME_API'
+  FOR_ME_API = 'FOR_ME_API',
+  WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
+  WITH_STATS = 'WITH_STATS'
 }
 
 @DefaultScope(() => ({
@@ -112,6 +130,86 @@ enum ScopeNames {
         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({
@@ -332,23 +430,7 @@ export class UserModel extends Model<UserModel> {
 
     const query: FindOptions = {
       attributes: {
-        include: [
-          [
-            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'
-          ]
-        ]
+        include: [ literalVideoQuotaUsed ]
       },
       offset: start,
       limit: count,
@@ -430,8 +512,14 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findAll(query)
   }
 
-  static loadById (id: number): Bluebird<MUserDefault> {
-    return UserModel.findByPk(id)
+  static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
+    const scopes = [
+      ScopeNames.WITH_VIDEOCHANNELS
+    ]
+
+    if (withStats) scopes.push(ScopeNames.WITH_STATS)
+
+    return UserModel.scope(scopes).findByPk(id)
   }
 
   static loadByUsername (username: string): Bluebird<MUserDefault> {
@@ -637,6 +725,10 @@ export class UserModel extends Model<UserModel> {
   toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
     const videoQuotaUsed = this.get('videoQuotaUsed')
     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 = {
       id: this.id,
@@ -666,6 +758,21 @@ export class UserModel extends Model<UserModel> {
       videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
         ? parseInt(videoQuotaUsedDaily + '', 10)
         : 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,
       noWelcomeModal: this.noWelcomeModal,
index 502eac0bb42e4560ba6dbb0588b07ac10831e914..3e1a0c19b90fcb345cf79b32396b22a5f5591e16 100644 (file)
@@ -2,7 +2,7 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { MyUser, User, UserRole, Video, VideoPlaylistType } from '../../../../shared/index'
+import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index'
 import {
   blockUser,
   cleanupTests,
@@ -33,7 +33,11 @@ import {
   updateMyUser,
   updateUser,
   uploadVideo,
-  userLogin
+  userLogin,
+  reportVideoAbuse,
+  addVideoCommentThread,
+  updateVideoAbuse,
+  getVideoAbusesList
 } from '../../../../shared/extra-utils'
 import { follow } from '../../../../shared/extra-utils/server/follows'
 import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@@ -254,7 +258,7 @@ describe('Test users', function () {
       const res1 = await getMyUserInformation(server.url, accessTokenUser)
       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
 
       for (const user of [ userMe, userGet ]) {
@@ -273,6 +277,16 @@ describe('Test users', function () {
 
       expect(userMe.specialPlaylists).to.have.lengthOf(1)
       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 () {
-
     it('Should be able to update another user', async function () {
       await updateUser({
         url: server.url,
@@ -698,6 +711,8 @@ describe('Test users', function () {
   })
 
   describe('Registering a new user', function () {
+    let user15AccessToken
+
     it('Should register a new user', async function () {
       const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
       const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
@@ -711,18 +726,18 @@ describe('Test users', function () {
         password: 'my super password'
       }
 
-      accessToken = await userLogin(server, user15)
+      user15AccessToken = await userLogin(server, user15)
     })
 
     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
 
       expect(user.account.displayName).to.equal('super user 15')
     })
 
     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
 
       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
       }
 
-      await deleteMe(server.url, accessToken)
+      await deleteMe(server.url, user15AccessToken)
 
       {
         const res = await getUsersList(server.url, server.accessToken)
@@ -750,6 +765,9 @@ describe('Test users', function () {
   })
 
   describe('User blocking', function () {
+    let user16Id
+    let user16AccessToken
+
     it('Should block and unblock a user', async function () {
       const user16 = {
         username: 'user_16',
@@ -761,19 +779,95 @@ describe('Test users', function () {
         username: user16.username,
         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 getMyUserInformation(server.url, accessToken, 401)
+      await getMyUserInformation(server.url, user16AccessToken, 401)
       await userLogin(server, user16, 400)
 
       await unblockUser(server.url, user16Id, server.accessToken)
-      accessToken = await userLogin(server, user16)
-      await getMyUserInformation(server.url, accessToken, 200)
+      user16AccessToken = await userLogin(server, user16)
+      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
     })
   })
 
index 248af2d6e3e92b65efefa9d3f95a4fde48642491..54b506bce04810bcc0fd55e52605fe1ce42c25dd 100644 (file)
@@ -130,11 +130,12 @@ function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatu
           .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
 
   return request(url)
     .get(path)
+    .query({ withStats })
     .set('Accept', 'application/json')
     .set('Authorization', 'Bearer ' + accessToken)
     .expect(200)
index efb4510148e12ddedc6f1b2c84a1b9ad3f89851a..a9c9bce30da1464ceab717f0e9db6424cd7fa7dc 100644 (file)
@@ -31,6 +31,11 @@ export interface User {
   videoQuotaDaily: number
   videoQuotaUsed?: number
   videoQuotaUsedDaily?: number
+  videosCount?: number
+  videoAbusesCount?: number
+  videoAbusesAcceptedCount?: number
+  videoAbusesCreatedCount?: number
+  videoCommentsCount? : number
 
   theme: string