diff options
20 files changed, 606 insertions, 172 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index b3b4f7728..9991e1f63 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -579,7 +579,7 @@ | |||
579 | i18n-labelText labelText="Allow additional extensions" | 579 | i18n-labelText labelText="Allow additional extensions" |
580 | > | 580 | > |
581 | <ng-container ngProjectAs="description"> | 581 | <ng-container ngProjectAs="description"> |
582 | <span i18n>Allow your users to upload .mkv, .mov, .avi and .flv videos.</span> | 582 | <span i18n>Allows users to upload .mkv, .mov, .avi and .flv videos.</span> |
583 | </ng-container> | 583 | </ng-container> |
584 | </my-peertube-checkbox> | 584 | </my-peertube-checkbox> |
585 | </div> | 585 | </div> |
@@ -590,7 +590,7 @@ | |||
590 | i18n-labelText labelText="Allow audio files upload" | 590 | i18n-labelText labelText="Allow audio files upload" |
591 | > | 591 | > |
592 | <ng-container ngProjectAs="description"> | 592 | <ng-container ngProjectAs="description"> |
593 | <span i18n>Allow your users to upload audio files that will be merged with the preview file on upload.</span> | 593 | <span i18n>Allows users to upload audio files that will be merged with the preview file on upload.</span> |
594 | </ng-container> | 594 | </ng-container> |
595 | </my-peertube-checkbox> | 595 | </my-peertube-checkbox> |
596 | </div> | 596 | </div> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss index 8b1cdcbba..d8bc30d55 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss | |||
@@ -50,6 +50,7 @@ input[type=submit] { | |||
50 | textarea { | 50 | textarea { |
51 | @include peertube-textarea(500px, 150px); | 51 | @include peertube-textarea(500px, 150px); |
52 | 52 | ||
53 | max-width: 100%; | ||
53 | display: block; | 54 | display: block; |
54 | 55 | ||
55 | &.small { | 56 | &.small { |
@@ -72,6 +73,10 @@ my-markdown-textarea ::ng-deep { | |||
72 | @media screen and (max-width: 1400px) { | 73 | @media screen and (max-width: 1400px) { |
73 | flex-direction: column !important; | 74 | flex-direction: column !important; |
74 | } | 75 | } |
76 | |||
77 | textarea { | ||
78 | max-width: 100%; | ||
79 | } | ||
75 | } | 80 | } |
76 | } | 81 | } |
77 | 82 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index 1769c0de0..a394418cb 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts | |||
@@ -8,6 +8,7 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
9 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 9 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
10 | import { UserService } from '@app/shared' | 10 | import { UserService } from '@app/shared' |
11 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
11 | 12 | ||
12 | @Component({ | 13 | @Component({ |
13 | selector: 'my-user-create', | 14 | selector: 'my-user-create', |
@@ -21,6 +22,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
21 | protected serverService: ServerService, | 22 | protected serverService: ServerService, |
22 | protected formValidatorService: FormValidatorService, | 23 | protected formValidatorService: FormValidatorService, |
23 | protected configService: ConfigService, | 24 | protected configService: ConfigService, |
25 | protected screenService: ScreenService, | ||
24 | protected auth: AuthService, | 26 | protected auth: AuthService, |
25 | private userValidatorsService: UserValidatorsService, | 27 | private userValidatorsService: UserValidatorsService, |
26 | private route: ActivatedRoute, | 28 | private route: ActivatedRoute, |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index dbb0e36b9..6c42fde57 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -1,112 +1,204 @@ | |||
1 | <div i18n class="form-sub-title" *ngIf="isCreation() === true">Create user</div> | 1 | <nav aria-label="breadcrumb"> |
2 | <div i18n class="form-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div> | 2 | <ol class="breadcrumb"> |
3 | <li class="breadcrumb-item"> | ||
4 | <a routerLink="/admin/users" i18n>Users</a> | ||
5 | </li> | ||
3 | 6 | ||
4 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 7 | <ng-container *ngIf="isCreation()"> |
8 | <li class="breadcrumb-item active" i18n>Create</li> | ||
9 | </ng-container> | ||
10 | <ng-container *ngIf="!isCreation()"> | ||
11 | <li class="breadcrumb-item active" i18n>Edit</li> | ||
12 | <li class="breadcrumb-item active" aria-current="page"> | ||
13 | <a *ngIf="user" [routerLink]="[ '/accounts', user?.username ]">{{ user?.username }}</a> | ||
14 | </li> | ||
15 | </ng-container> | ||
16 | </ol> | ||
17 | </nav> | ||
5 | 18 | ||
6 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | 19 | <ng-template #dashboard> |
7 | <div class="form-group" *ngIf="isCreation()"> | 20 | <div *ngIf="!isCreation() && user" class="dashboard"> |
8 | <label i18n for="username">Username</label> | 21 | <div> |
9 | <input | 22 | <a> |
10 | type="text" id="username" i18n-placeholder placeholder="john" | 23 | <div class="dashboard-num">{{ user.videosCount }} ({{ user.videoQuotaUsed | bytes: 0 }})</div> |
11 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | 24 | <div class="dashboard-label" i18n>{user.videosCount, plural, =1 {Video} other {Videos}}</div> |
12 | > | 25 | </a> |
13 | <div *ngIf="formErrors.username" class="form-error"> | ||
14 | {{ formErrors.username }} | ||
15 | </div> | 26 | </div> |
16 | </div> | 27 | <div> |
17 | 28 | <a> | |
18 | <div class="form-group"> | 29 | <div class="dashboard-num">{{ user.videoChannels.length || 0 }}</div> |
19 | <label i18n for="email">Email</label> | 30 | <div class="dashboard-label" i18n>{user.videoChannels.length, plural, =1 {Channel} other {Channels}}</div> |
20 | <input | 31 | </a> |
21 | type="text" id="email" i18n-placeholder placeholder="mail@example.com" | ||
22 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
23 | autocomplete="off" | ||
24 | > | ||
25 | <div *ngIf="formErrors.email" class="form-error"> | ||
26 | {{ formErrors.email }} | ||
27 | </div> | 32 | </div> |
28 | </div> | 33 | <div> |
29 | 34 | <a> | |
30 | <div class="form-group" *ngIf="isCreation()"> | 35 | <div class="dashboard-num">{{ subscribersCount }}</div> |
31 | <label i18n for="password">Password</label> | 36 | <div class="dashboard-label" i18n>{subscribersCount, plural, =1 {Subscriber} other {Subscribers}}</div> |
32 | <my-help *ngIf="isPasswordOptional()"> | 37 | </a> |
33 | <ng-template ptTemplate="customHtml"> | 38 | </div> |
34 | <ng-container i18n> | 39 | <div> |
35 | If you leave the password empty, an email will be sent to the user. | 40 | <a> |
36 | </ng-container> | 41 | <div class="dashboard-num">{{ user.videoAbusesCount }}</div> |
37 | </ng-template> | 42 | <div class="dashboard-label" i18n>Incriminated in reports</div> |
38 | </my-help> | 43 | </a> |
39 | <input | 44 | </div> |
40 | type="password" id="password" autocomplete="new-password" | 45 | <div> |
41 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | 46 | <a> |
42 | > | 47 | <div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div> |
43 | <div *ngIf="formErrors.password" class="form-error"> | 48 | <div class="dashboard-label" i18n>Authored reports accepted</div> |
44 | {{ formErrors.password }} | 49 | </a> |
50 | </div> | ||
51 | <div> | ||
52 | <a> | ||
53 | <div class="dashboard-num">{{ user.videoCommentsCount }}</div> | ||
54 | <div class="dashboard-label" i18n>{user.videoCommentsCount, plural, =1 {Comment} other {Comments}}</div> | ||
55 | </a> | ||
45 | </div> | 56 | </div> |
46 | </div> | 57 | </div> |
58 | </ng-template> | ||
47 | 59 | ||
48 | <div class="form-group"> | 60 | <div class="form-row" *ngIf="!isInBigView()"> <!-- hidden on large screens, as it is then displayed on the right side of the form --> |
49 | <label i18n for="role">Role</label> | 61 | <div class="col-12 col-xl-3"></div> |
50 | <div class="peertube-select-container"> | ||
51 | <select id="role" formControlName="role"> | ||
52 | <option *ngFor="let role of roles" [value]="role.value"> | ||
53 | {{ role.label }} | ||
54 | </option> | ||
55 | </select> | ||
56 | </div> | ||
57 | 62 | ||
58 | <div *ngIf="formErrors.role" class="form-error"> | 63 | <div class="form-group-right col-12 col-xl-9"> |
59 | {{ formErrors.role }} | 64 | <ng-template *ngTemplateOutlet="dashboard"></ng-template> |
60 | </div> | ||
61 | </div> | 65 | </div> |
66 | </div> | ||
62 | 67 | ||
63 | <div class="form-group"> | 68 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
64 | <label i18n for="videoQuota">Video quota</label> | ||
65 | <div class="peertube-select-container"> | ||
66 | <select id="videoQuota" formControlName="videoQuota"> | ||
67 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | ||
68 | {{ videoQuotaOption.label }} | ||
69 | </option> | ||
70 | </select> | ||
71 | </div> | ||
72 | 69 | ||
73 | <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> | 70 | <div class="form-row mt-4"> <!-- user grid --> |
74 | Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> | 71 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
75 | At most, this user could use ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}. | 72 | <div class="anchor" id="user"></div> <!-- user anchor --> |
73 | <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> | ||
74 | <div *ngIf="!isCreation() && user" class="account-title"> | ||
75 | <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> | ||
76 | </div> | 76 | </div> |
77 | </div> | 77 | </div> |
78 | 78 | ||
79 | <div class="form-group"> | 79 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> |
80 | <label i18n for="videoQuotaDaily">Daily video quota</label> | 80 | |
81 | <div class="peertube-select-container"> | 81 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form" [ngClass]="{ 'col-5': isInBigView() }"> |
82 | <select id="videoQuotaDaily" formControlName="videoQuotaDaily"> | 82 | <div class="form-group" *ngIf="isCreation()"> |
83 | <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value"> | 83 | <label i18n for="username">Username</label> |
84 | {{ videoQuotaDailyOption.label }} | 84 | <input |
85 | </option> | 85 | type="text" id="username" i18n-placeholder placeholder="john" |
86 | </select> | 86 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" |
87 | > | ||
88 | <div *ngIf="formErrors.username" class="form-error"> | ||
89 | {{ formErrors.username }} | ||
90 | </div> | ||
91 | </div> | ||
92 | |||
93 | <div class="form-group"> | ||
94 | <label i18n for="email">Email</label> | ||
95 | <input | ||
96 | type="text" id="email" i18n-placeholder placeholder="mail@example.com" | ||
97 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
98 | autocomplete="off" | ||
99 | > | ||
100 | <div *ngIf="formErrors.email" class="form-error"> | ||
101 | {{ formErrors.email }} | ||
102 | </div> | ||
103 | </div> | ||
104 | |||
105 | <div class="form-group" *ngIf="isCreation()"> | ||
106 | <label i18n for="password">Password</label> | ||
107 | <my-help *ngIf="isPasswordOptional()"> | ||
108 | <ng-template ptTemplate="customHtml"> | ||
109 | <ng-container i18n> | ||
110 | If you leave the password empty, an email will be sent to the user. | ||
111 | </ng-container> | ||
112 | </ng-template> | ||
113 | </my-help> | ||
114 | <input | ||
115 | type="password" id="password" autocomplete="new-password" | ||
116 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
117 | > | ||
118 | <div *ngIf="formErrors.password" class="form-error"> | ||
119 | {{ formErrors.password }} | ||
120 | </div> | ||
121 | </div> | ||
122 | |||
123 | <div class="form-group"> | ||
124 | <label i18n for="role">Role</label> | ||
125 | <div class="peertube-select-container"> | ||
126 | <select id="role" formControlName="role"> | ||
127 | <option *ngFor="let role of roles" [value]="role.value"> | ||
128 | {{ role.label }} | ||
129 | </option> | ||
130 | </select> | ||
131 | </div> | ||
132 | |||
133 | <div *ngIf="formErrors.role" class="form-error"> | ||
134 | {{ formErrors.role }} | ||
135 | </div> | ||
136 | </div> | ||
137 | |||
138 | <div class="form-group"> | ||
139 | <label i18n for="videoQuota">Video quota</label> | ||
140 | <div class="peertube-select-container"> | ||
141 | <select id="videoQuota" formControlName="videoQuota"> | ||
142 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | ||
143 | {{ videoQuotaOption.label }} | ||
144 | </option> | ||
145 | </select> | ||
146 | </div> | ||
147 | |||
148 | <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> | ||
149 | Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br /> | ||
150 | At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}. | ||
151 | </div> | ||
152 | </div> | ||
153 | |||
154 | <div class="form-group"> | ||
155 | <label i18n for="videoQuotaDaily">Daily video quota</label> | ||
156 | <div class="peertube-select-container"> | ||
157 | <select id="videoQuotaDaily" formControlName="videoQuotaDaily"> | ||
158 | <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value"> | ||
159 | {{ videoQuotaDailyOption.label }} | ||
160 | </option> | ||
161 | </select> | ||
162 | </div> | ||
163 | </div> | ||
164 | |||
165 | <div class="form-group"> | ||
166 | <my-peertube-checkbox | ||
167 | inputName="byPassAutoBlacklist" formControlName="byPassAutoBlacklist" | ||
168 | i18n-labelText labelText="Doesn't need review before a video goes public" | ||
169 | ></my-peertube-checkbox> | ||
170 | </div> | ||
171 | |||
172 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
173 | </form> | ||
174 | |||
175 | <div *ngIf="isInBigView()" class="col-7"> | ||
176 | <ng-template *ngTemplateOutlet="dashboard"></ng-template> | ||
87 | </div> | 177 | </div> |
178 | |||
88 | </div> | 179 | </div> |
180 | </div> | ||
181 | |||
89 | 182 | ||
90 | <div class="form-group"> | 183 | <div *ngIf="!isCreation() && user" class="form-row mt-4"> <!-- danger zone grid --> |
91 | <my-peertube-checkbox | 184 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
92 | inputName="byPassAutoBlacklist" formControlName="byPassAutoBlacklist" | 185 | <div class="anchor" id="danger"></div> <!-- danger zone anchor --> |
93 | i18n-labelText labelText="Bypass video auto blacklist" | 186 | <div i18n class="account-title">DANGER ZONE</div> |
94 | ></my-peertube-checkbox> | ||
95 | </div> | 187 | </div> |
96 | 188 | ||
97 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 189 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9" [ngClass]="{ 'form-row': isInBigView() }"> |
98 | </form> | ||
99 | 190 | ||
100 | <div *ngIf="!isCreation()" class="danger-zone"> | 191 | <div class="danger-zone"> |
101 | <div class="account-title" i18n>Danger Zone</div> | 192 | <div class="form-group reset-password-email"> |
193 | <label i18n>Send a link to reset the password by email to the user</label> | ||
194 | <button (click)="resetPassword()" i18n>Ask for new password</button> | ||
195 | </div> | ||
102 | 196 | ||
103 | <div class="form-group reset-password-email"> | 197 | <div class="form-group"> |
104 | <label i18n>Send a link to reset the password by email to the user</label> | 198 | <label i18n>Manually set the user password</label> |
105 | <button (click)="resetPassword()" i18n>Ask for new password</button> | 199 | <my-user-password [userId]="user.id"></my-user-password> |
106 | </div> | 200 | </div> |
201 | </div> | ||
107 | 202 | ||
108 | <div class="form-group"> | ||
109 | <label i18n>Manually set the user password</label> | ||
110 | <my-user-password [userId]="userId"></my-user-password> | ||
111 | </div> | 203 | </div> |
112 | </div> | 204 | </div> |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index c1cc4ca45..d4c1b600e 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -1,8 +1,13 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .form-sub-title { | 4 | label { |
5 | margin-bottom: 30px; | 5 | font-weight: $font-regular; |
6 | font-size: 100%; | ||
7 | } | ||
8 | |||
9 | .account-title { | ||
10 | @include settings-big-title; | ||
6 | } | 11 | } |
7 | 12 | ||
8 | input:not([type=submit]) { | 13 | input:not([type=submit]) { |
@@ -26,18 +31,9 @@ input[type=submit], button { | |||
26 | font-size: 11px; | 31 | font-size: 11px; |
27 | } | 32 | } |
28 | 33 | ||
29 | .account-title { | ||
30 | @include in-content-small-title; | ||
31 | |||
32 | margin-top: 55px; | ||
33 | margin-bottom: 30px; | ||
34 | } | ||
35 | |||
36 | .danger-zone { | 34 | .danger-zone { |
37 | .reset-password-email { | 35 | .reset-password-email { |
38 | margin-bottom: 30px; | 36 | margin-bottom: 30px; |
39 | padding-bottom: 30px; | ||
40 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); | ||
41 | 37 | ||
42 | button { | 38 | button { |
43 | display: block; | 39 | display: block; |
@@ -45,3 +41,20 @@ input[type=submit], button { | |||
45 | } | 41 | } |
46 | } | 42 | } |
47 | } | 43 | } |
44 | |||
45 | .breadcrumb { | ||
46 | @include breadcrumb; | ||
47 | } | ||
48 | |||
49 | .dashboard { | ||
50 | @include dashboard; | ||
51 | max-width: 900px; | ||
52 | } | ||
53 | |||
54 | my-actor-avatar-info ::ng-deep { | ||
55 | .actor-img-edit-container, | ||
56 | .actor-info-followers, | ||
57 | .actor-info-username { | ||
58 | display: none; | ||
59 | } | ||
60 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 47b57d2ec..a23cd9033 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -4,12 +4,14 @@ import { ServerConfig, USER_ROLE_LABELS, UserRole, VideoResolution } from '../.. | |||
4 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
5 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 5 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
6 | import { OnInit } from '@angular/core' | 6 | import { OnInit } from '@angular/core' |
7 | import { User } from '@app/shared/users/user.model' | ||
8 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
7 | 9 | ||
8 | export abstract class UserEdit extends FormReactive implements OnInit { | 10 | export abstract class UserEdit extends FormReactive implements OnInit { |
9 | videoQuotaOptions: { value: string, label: string }[] = [] | 11 | videoQuotaOptions: { value: string, label: string }[] = [] |
10 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 12 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
11 | username: string | 13 | username: string |
12 | userId: number | 14 | user: User |
13 | 15 | ||
14 | roles: { value: string, label: string }[] = [] | 16 | roles: { value: string, label: string }[] = [] |
15 | 17 | ||
@@ -17,6 +19,7 @@ export abstract class UserEdit extends FormReactive implements OnInit { | |||
17 | 19 | ||
18 | protected abstract serverService: ServerService | 20 | protected abstract serverService: ServerService |
19 | protected abstract configService: ConfigService | 21 | protected abstract configService: ConfigService |
22 | protected abstract screenService: ScreenService | ||
20 | protected abstract auth: AuthService | 23 | protected abstract auth: AuthService |
21 | abstract isCreation (): boolean | 24 | abstract isCreation (): boolean |
22 | abstract getFormButtonTitle (): string | 25 | abstract getFormButtonTitle (): string |
@@ -29,6 +32,20 @@ export abstract class UserEdit extends FormReactive implements OnInit { | |||
29 | this.buildRoles() | 32 | this.buildRoles() |
30 | } | 33 | } |
31 | 34 | ||
35 | get subscribersCount () { | ||
36 | const forAccount = this.user | ||
37 | ? this.user.account.followersCount | ||
38 | : 0 | ||
39 | const forChannels = this.user | ||
40 | ? this.user.videoChannels.map(c => c.followersCount).reduce((a, b) => a + b, 0) | ||
41 | : 0 | ||
42 | return forAccount + forChannels | ||
43 | } | ||
44 | |||
45 | isInBigView () { | ||
46 | return this.screenService.getWindowInnerWidth() > 1600 | ||
47 | } | ||
48 | |||
32 | buildRoles () { | 49 | buildRoles () { |
33 | const authUser = this.auth.getUser() | 50 | const authUser = this.auth.getUser() |
34 | 51 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts index 5b3040440..ecad000f7 100644 --- a/client/src/app/+admin/users/user-edit/user-password.component.ts +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts | |||
@@ -23,8 +23,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit { | |||
23 | constructor ( | 23 | constructor ( |
24 | protected formValidatorService: FormValidatorService, | 24 | protected formValidatorService: FormValidatorService, |
25 | private userValidatorsService: UserValidatorsService, | 25 | private userValidatorsService: UserValidatorsService, |
26 | private route: ActivatedRoute, | ||
27 | private router: Router, | ||
28 | private notifier: Notifier, | 26 | private notifier: Notifier, |
29 | private userService: UserService, | 27 | private userService: UserService, |
30 | private i18n: I18n | 28 | private i18n: I18n |
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index 1ab2e9dbf..fbe3d6950 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -4,13 +4,15 @@ import { Subscription } from 'rxjs' | |||
4 | import { AuthService, Notifier } from '@app/core' | 4 | import { AuthService, Notifier } from '@app/core' |
5 | import { ServerService } from '../../../core' | 5 | import { ServerService } from '../../../core' |
6 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
7 | import { User, UserUpdate } from '../../../../../../shared' | 7 | import { User as UserType, UserUpdate, UserRole } from '../../../../../../shared' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
10 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 10 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
11 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 11 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
12 | import { UserService } from '@app/shared' | 12 | import { UserService } from '@app/shared' |
13 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 13 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
14 | import { User } from '@app/shared/users/user.model' | ||
15 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
14 | 16 | ||
15 | @Component({ | 17 | @Component({ |
16 | selector: 'my-user-update', | 18 | selector: 'my-user-update', |
@@ -19,9 +21,6 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model' | |||
19 | }) | 21 | }) |
20 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | 22 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { |
21 | error: string | 23 | error: string |
22 | userId: number | ||
23 | userEmail: string | ||
24 | username: string | ||
25 | 24 | ||
26 | private paramsSub: Subscription | 25 | private paramsSub: Subscription |
27 | 26 | ||
@@ -29,6 +28,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
29 | protected formValidatorService: FormValidatorService, | 28 | protected formValidatorService: FormValidatorService, |
30 | protected serverService: ServerService, | 29 | protected serverService: ServerService, |
31 | protected configService: ConfigService, | 30 | protected configService: ConfigService, |
31 | protected screenService: ScreenService, | ||
32 | protected auth: AuthService, | 32 | protected auth: AuthService, |
33 | private userValidatorsService: UserValidatorsService, | 33 | private userValidatorsService: UserValidatorsService, |
34 | private route: ActivatedRoute, | 34 | private route: ActivatedRoute, |
@@ -45,7 +45,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
45 | ngOnInit () { | 45 | ngOnInit () { |
46 | super.ngOnInit() | 46 | super.ngOnInit() |
47 | 47 | ||
48 | const defaultValues = { videoQuota: '-1', videoQuotaDaily: '-1' } | 48 | const defaultValues = { |
49 | role: UserRole.USER.toString(), | ||
50 | videoQuota: '-1', | ||
51 | videoQuotaDaily: '-1' | ||
52 | } | ||
53 | |||
49 | this.buildForm({ | 54 | this.buildForm({ |
50 | email: this.userValidatorsService.USER_EMAIL, | 55 | email: this.userValidatorsService.USER_EMAIL, |
51 | role: this.userValidatorsService.USER_ROLE, | 56 | role: this.userValidatorsService.USER_ROLE, |
@@ -56,7 +61,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
56 | 61 | ||
57 | this.paramsSub = this.route.params.subscribe(routeParams => { | 62 | this.paramsSub = this.route.params.subscribe(routeParams => { |
58 | const userId = routeParams['id'] | 63 | const userId = routeParams['id'] |
59 | this.userService.getUser(userId).subscribe( | 64 | this.userService.getUser(userId, true).subscribe( |
60 | user => this.onUserFetched(user), | 65 | user => this.onUserFetched(user), |
61 | 66 | ||
62 | err => this.error = err.message | 67 | err => this.error = err.message |
@@ -78,9 +83,9 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
78 | userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) | 83 | userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) |
79 | userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) | 84 | userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) |
80 | 85 | ||
81 | this.userService.updateUser(this.userId, userUpdate).subscribe( | 86 | this.userService.updateUser(this.user.id, userUpdate).subscribe( |
82 | () => { | 87 | () => { |
83 | this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username })) | 88 | this.notifier.success(this.i18n('User {{user.username}} updated.', { username: this.user.username })) |
84 | this.router.navigate([ '/admin/users/list' ]) | 89 | this.router.navigate([ '/admin/users/list' ]) |
85 | }, | 90 | }, |
86 | 91 | ||
@@ -101,10 +106,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
101 | } | 106 | } |
102 | 107 | ||
103 | resetPassword () { | 108 | resetPassword () { |
104 | this.userService.askResetPassword(this.userEmail).subscribe( | 109 | this.userService.askResetPassword(this.user.email).subscribe( |
105 | () => { | 110 | () => { |
106 | this.notifier.success( | 111 | this.notifier.success( |
107 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) | 112 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.user.username }) |
108 | ) | 113 | ) |
109 | }, | 114 | }, |
110 | 115 | ||
@@ -112,14 +117,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
112 | ) | 117 | ) |
113 | } | 118 | } |
114 | 119 | ||
115 | private onUserFetched (userJson: User) { | 120 | private onUserFetched (userJson: UserType) { |
116 | this.userId = userJson.id | 121 | this.user = new User(userJson) |
117 | this.username = userJson.username | ||
118 | this.userEmail = userJson.email | ||
119 | 122 | ||
120 | this.form.patchValue({ | 123 | this.form.patchValue({ |
121 | email: userJson.email, | 124 | email: userJson.email, |
122 | role: userJson.role, | 125 | role: userJson.role.toString(), |
123 | videoQuota: userJson.videoQuota, | 126 | videoQuota: userJson.videoQuota, |
124 | videoQuotaDaily: userJson.videoQuotaDaily, | 127 | videoQuotaDaily: userJson.videoQuotaDaily, |
125 | byPassAutoBlacklist: userJson.adminFlags & UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST | 128 | byPassAutoBlacklist: userJson.adminFlags & UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index db8ffac16..f8c04cb4d 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -15,7 +15,6 @@ import { MyAccountProfileComponent } from '@app/+my-account/my-account-settings/ | |||
15 | import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' | 15 | import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' |
16 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' | 16 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' |
17 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' | 17 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' |
18 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' | ||
19 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | 18 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' |
20 | import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' | 19 | import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' |
21 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' | 20 | 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 | |||
63 | MyAccountVideoChannelsComponent, | 62 | MyAccountVideoChannelsComponent, |
64 | MyAccountVideoChannelCreateComponent, | 63 | MyAccountVideoChannelCreateComponent, |
65 | MyAccountVideoChannelUpdateComponent, | 64 | MyAccountVideoChannelUpdateComponent, |
66 | ActorAvatarInfoComponent, | ||
67 | MyAccountVideoImportsComponent, | 65 | MyAccountVideoImportsComponent, |
68 | MyAccountDangerZoneComponent, | 66 | MyAccountDangerZoneComponent, |
69 | MyAccountSubscriptionsComponent, | 67 | MyAccountSubscriptionsComponent, |
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.html b/client/src/app/+my-account/shared/actor-avatar-info.component.html index 2050950be..82f5123de 100644 --- a/client/src/app/+my-account/shared/actor-avatar-info.component.html +++ b/client/src/app/+my-account/shared/actor-avatar-info.component.html | |||
@@ -1,14 +1,17 @@ | |||
1 | <ng-container *ngIf="actor"> | 1 | <ng-container *ngIf="actor"> |
2 | <div class="actor"> | 2 | <div class="actor"> |
3 | <img [src]="actor.avatarUrl" alt="Avatar" /> | 3 | <div class="d-flex"> |
4 | <img [src]="actor.avatarUrl" alt="Avatar" /> | ||
4 | 5 | ||
5 | <div class="actor-img-edit-container"> | 6 | <div class="actor-img-edit-container"> |
6 | <div class="actor-img-edit-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body"> | 7 | <div class="actor-img-edit-button" [ngbTooltip]="'(extensions: '+ avatarExtensions +', '+ maxSizeText +': '+ maxAvatarSizeInBytes +')'" placement="right" container="body"> |
7 | <my-global-icon iconName="edit"></my-global-icon> | 8 | <my-global-icon iconName="edit"></my-global-icon> |
8 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/> | 9 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/> |
10 | </div> | ||
9 | </div> | 11 | </div> |
10 | </div> | 12 | </div> |
11 | 13 | ||
14 | |||
12 | <div class="actor-info"> | 15 | <div class="actor-info"> |
13 | <div class="actor-info-names"> | 16 | <div class="actor-info-names"> |
14 | <div class="actor-info-display-name">{{ actor.displayName }}</div> | 17 | <div class="actor-info-display-name">{{ actor.displayName }}</div> |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 75aa30dab..b89f0a8d1 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -106,6 +106,7 @@ import { InputSwitchModule } from 'primeng/inputswitch' | |||
106 | 106 | ||
107 | import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' | 107 | import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' |
108 | import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' | 108 | import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' |
109 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' | ||
109 | 110 | ||
110 | @NgModule({ | 111 | @NgModule({ |
111 | imports: [ | 112 | imports: [ |
@@ -189,7 +190,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account | |||
189 | PreviewUploadComponent, | 190 | PreviewUploadComponent, |
190 | 191 | ||
191 | MyAccountVideoSettingsComponent, | 192 | MyAccountVideoSettingsComponent, |
192 | MyAccountInterfaceSettingsComponent | 193 | MyAccountInterfaceSettingsComponent, |
194 | ActorAvatarInfoComponent | ||
193 | ], | 195 | ], |
194 | 196 | ||
195 | exports: [ | 197 | exports: [ |
@@ -270,7 +272,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account | |||
270 | VideoDurationPipe, | 272 | VideoDurationPipe, |
271 | 273 | ||
272 | MyAccountVideoSettingsComponent, | 274 | MyAccountVideoSettingsComponent, |
273 | MyAccountInterfaceSettingsComponent | 275 | MyAccountInterfaceSettingsComponent, |
276 | ActorAvatarInfoComponent | ||
274 | ], | 277 | ], |
275 | 278 | ||
276 | providers: [ | 279 | providers: [ |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index a37cae749..76c57d2fb 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -51,6 +51,11 @@ export class User implements UserServerModel { | |||
51 | videoQuotaDaily: number | 51 | videoQuotaDaily: number |
52 | videoQuotaUsed?: number | 52 | videoQuotaUsed?: number |
53 | videoQuotaUsedDaily?: number | 53 | videoQuotaUsedDaily?: number |
54 | videosCount?: number | ||
55 | videoAbusesCount?: number | ||
56 | videoAbusesAcceptedCount?: number | ||
57 | videoAbusesCreatedCount?: number | ||
58 | videoCommentsCount?: number | ||
54 | 59 | ||
55 | theme: string | 60 | theme: string |
56 | 61 | ||
@@ -79,6 +84,11 @@ export class User implements UserServerModel { | |||
79 | this.videoQuotaDaily = hash.videoQuotaDaily | 84 | this.videoQuotaDaily = hash.videoQuotaDaily |
80 | this.videoQuotaUsed = hash.videoQuotaUsed | 85 | this.videoQuotaUsed = hash.videoQuotaUsed |
81 | this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily | 86 | this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily |
87 | this.videosCount = hash.videosCount | ||
88 | this.videoAbusesCount = hash.videoAbusesCount | ||
89 | this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount | ||
90 | this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount | ||
91 | this.videoCommentsCount = hash.videoCommentsCount | ||
82 | 92 | ||
83 | this.nsfwPolicy = hash.nsfwPolicy | 93 | this.nsfwPolicy = hash.nsfwPolicy |
84 | this.webTorrentEnabled = hash.webTorrentEnabled | 94 | this.webTorrentEnabled = hash.webTorrentEnabled |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index a79343646..5442a8453 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -234,8 +234,9 @@ export class UserService { | |||
234 | return this.userCache[userId] | 234 | return this.userCache[userId] |
235 | } | 235 | } |
236 | 236 | ||
237 | getUser (userId: number) { | 237 | getUser (userId: number, withStats = false) { |
238 | return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId) | 238 | const params = new HttpParams().append('withStats', withStats + '') |
239 | return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId, { params }) | ||
239 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 240 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
240 | } | 241 | } |
241 | 242 | ||
diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html index b9434da26..cc1d361b3 100644 --- a/client/src/app/shared/video/modals/video-report.component.html +++ b/client/src/app/shared/video/modals/video-report.component.html | |||
@@ -7,8 +7,7 @@ | |||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
8 | 8 | ||
9 | <div i18n class="information"> | 9 | <div i18n class="information"> |
10 | Your report will be sent to moderators of {{ currentHost }}. | 10 | 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>. |
11 | <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container> | ||
12 | </div> | 11 | </div> |
13 | 12 | ||
14 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | 13 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index e8dfb79bc..f96a43b34 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -621,3 +621,85 @@ | |||
621 | } | 621 | } |
622 | } | 622 | } |
623 | } | 623 | } |
624 | |||
625 | @mixin breadcrumb { | ||
626 | display: flex; | ||
627 | flex-wrap: wrap; | ||
628 | padding: 0.75rem 1rem; | ||
629 | margin-bottom: 1rem; | ||
630 | list-style: none; | ||
631 | background-color: var(--submenuColor); | ||
632 | border-radius: 0.25rem; | ||
633 | |||
634 | .breadcrumb-item { | ||
635 | display: flex; | ||
636 | |||
637 | a { | ||
638 | color: var(--mainColor); | ||
639 | } | ||
640 | |||
641 | & + .breadcrumb-item { | ||
642 | padding-left: 0.5rem; | ||
643 | &::before { | ||
644 | display: inline-block; | ||
645 | padding-right: 0.5rem; | ||
646 | color: #6c757d; | ||
647 | content: "/"; | ||
648 | } | ||
649 | } | ||
650 | |||
651 | &.active { | ||
652 | color: #6c757d; | ||
653 | } | ||
654 | } | ||
655 | } | ||
656 | |||
657 | @mixin dashboard { | ||
658 | display: flex; | ||
659 | flex-wrap: wrap; | ||
660 | margin: 0 -5px; | ||
661 | |||
662 | & > div { | ||
663 | box-sizing: border-box; | ||
664 | flex: 0 0 percentage(1/3); | ||
665 | padding: 0 5px; | ||
666 | margin-bottom: 10px; | ||
667 | |||
668 | & > a { | ||
669 | text-decoration: none; | ||
670 | color: inherit; | ||
671 | display: block; | ||
672 | font-size: 18px; | ||
673 | |||
674 | &:active, | ||
675 | &:focus, | ||
676 | &:hover { | ||
677 | opacity: .8; | ||
678 | } | ||
679 | } | ||
680 | |||
681 | & > a, | ||
682 | & > div { | ||
683 | padding: 20px; | ||
684 | background: var(--submenuColor); | ||
685 | border-radius: 4px; | ||
686 | box-sizing: border-box; | ||
687 | height: 100%; | ||
688 | } | ||
689 | } | ||
690 | |||
691 | .dashboard-num, .dashboard-text { | ||
692 | text-align: center; | ||
693 | font-size: 130%; | ||
694 | line-height: 21px; | ||
695 | color: var(--mainForegroundColor); | ||
696 | line-height: 30px; | ||
697 | margin-bottom: 20px; | ||
698 | } | ||
699 | |||
700 | .dashboard-label { | ||
701 | font-size: 90%; | ||
702 | color: var(--inputPlaceholderColor); | ||
703 | text-align: center; | ||
704 | } | ||
705 | } | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index adc67a046..840b9fc74 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { body, param } from 'express-validator' | 3 | import { body, param, query } from 'express-validator' |
4 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
5 | import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 5 | import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
6 | import { | 6 | import { |
@@ -256,12 +256,13 @@ const usersUpdateMeValidator = [ | |||
256 | 256 | ||
257 | const usersGetValidator = [ | 257 | const usersGetValidator = [ |
258 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 258 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
259 | query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'), | ||
259 | 260 | ||
260 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 261 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
261 | logger.debug('Checking usersGet parameters', { parameters: req.params }) | 262 | logger.debug('Checking usersGet parameters', { parameters: req.params }) |
262 | 263 | ||
263 | if (areValidationErrors(req, res)) return | 264 | if (areValidationErrors(req, res)) return |
264 | if (!await checkUserIdExist(req.params.id, res)) return | 265 | if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return |
265 | 266 | ||
266 | return next() | 267 | return next() |
267 | } | 268 | } |
@@ -460,9 +461,9 @@ export { | |||
460 | 461 | ||
461 | // --------------------------------------------------------------------------- | 462 | // --------------------------------------------------------------------------- |
462 | 463 | ||
463 | function checkUserIdExist (idArg: number | string, res: express.Response) { | 464 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { |
464 | const id = parseInt(idArg + '', 10) | 465 | const id = parseInt(idArg + '', 10) |
465 | return checkUserExist(() => UserModel.loadById(id), res) | 466 | return checkUserExist(() => UserModel.loadById(id, withStats), res) |
466 | } | 467 | } |
467 | 468 | ||
468 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | 469 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 777f09666..026bf1318 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | Table, | 19 | Table, |
20 | UpdatedAt | 20 | UpdatedAt |
21 | } from 'sequelize-typescript' | 21 | } from 'sequelize-typescript' |
22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared' | 22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy, VideoAbuseState } from '../../../shared' |
23 | import { User, UserRole } from '../../../shared/models/users' | 23 | import { User, UserRole } from '../../../shared/models/users' |
24 | import { | 24 | import { |
25 | isNoInstanceConfigWarningModal, | 25 | isNoInstanceConfigWarningModal, |
@@ -70,8 +70,26 @@ import { | |||
70 | MVideoFullLight | 70 | MVideoFullLight |
71 | } from '@server/typings/models' | 71 | } from '@server/typings/models' |
72 | 72 | ||
73 | const literalVideoQuotaUsed: any = [ | ||
74 | literal( | ||
75 | '(' + | ||
76 | 'SELECT COALESCE(SUM("size"), 0) ' + | ||
77 | 'FROM (' + | ||
78 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + | ||
79 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + | ||
80 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
81 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | ||
82 | 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' + | ||
83 | ') t' + | ||
84 | ')' | ||
85 | ), | ||
86 | 'videoQuotaUsed' | ||
87 | ] | ||
88 | |||
73 | enum ScopeNames { | 89 | enum ScopeNames { |
74 | FOR_ME_API = 'FOR_ME_API' | 90 | FOR_ME_API = 'FOR_ME_API', |
91 | WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', | ||
92 | WITH_STATS = 'WITH_STATS' | ||
75 | } | 93 | } |
76 | 94 | ||
77 | @DefaultScope(() => ({ | 95 | @DefaultScope(() => ({ |
@@ -112,6 +130,86 @@ enum ScopeNames { | |||
112 | required: true | 130 | required: true |
113 | } | 131 | } |
114 | ] | 132 | ] |
133 | }, | ||
134 | [ScopeNames.WITH_VIDEOCHANNELS]: { | ||
135 | include: [ | ||
136 | { | ||
137 | model: AccountModel, | ||
138 | include: [ | ||
139 | { | ||
140 | model: VideoChannelModel | ||
141 | }, | ||
142 | { | ||
143 | attributes: [ 'id', 'name', 'type' ], | ||
144 | model: VideoPlaylistModel.unscoped(), | ||
145 | required: true, | ||
146 | where: { | ||
147 | type: { | ||
148 | [Op.ne]: VideoPlaylistType.REGULAR | ||
149 | } | ||
150 | } | ||
151 | } | ||
152 | ] | ||
153 | } | ||
154 | ] | ||
155 | }, | ||
156 | [ScopeNames.WITH_STATS]: { | ||
157 | attributes: { | ||
158 | include: [ | ||
159 | literalVideoQuotaUsed, | ||
160 | [ | ||
161 | literal( | ||
162 | '(' + | ||
163 | 'SELECT COUNT("video"."id") ' + | ||
164 | 'FROM "video" ' + | ||
165 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
166 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
167 | 'WHERE "account"."userId" = "UserModel"."id"' + | ||
168 | ')' | ||
169 | ), | ||
170 | 'videosCount' | ||
171 | ], | ||
172 | [ | ||
173 | literal( | ||
174 | '(' + | ||
175 | `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + | ||
176 | 'FROM (' + | ||
177 | 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + | ||
178 | `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + | ||
179 | 'FROM "videoAbuse" ' + | ||
180 | 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + | ||
181 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
182 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
183 | 'WHERE "account"."userId" = "UserModel"."id"' + | ||
184 | ') t' + | ||
185 | ')' | ||
186 | ), | ||
187 | 'videoAbusesCount' | ||
188 | ], | ||
189 | [ | ||
190 | literal( | ||
191 | '(' + | ||
192 | 'SELECT COUNT("videoAbuse"."id") ' + | ||
193 | 'FROM "videoAbuse" ' + | ||
194 | 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + | ||
195 | 'WHERE "account"."userId" = "UserModel"."id"' + | ||
196 | ')' | ||
197 | ), | ||
198 | 'videoAbusesCreatedCount' | ||
199 | ], | ||
200 | [ | ||
201 | literal( | ||
202 | '(' + | ||
203 | 'SELECT COUNT("videoComment"."id") ' + | ||
204 | 'FROM "videoComment" ' + | ||
205 | 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' + | ||
206 | 'WHERE "account"."userId" = "UserModel"."id"' + | ||
207 | ')' | ||
208 | ), | ||
209 | 'videoCommentsCount' | ||
210 | ] | ||
211 | ] | ||
212 | } | ||
115 | } | 213 | } |
116 | })) | 214 | })) |
117 | @Table({ | 215 | @Table({ |
@@ -332,23 +430,7 @@ export class UserModel extends Model<UserModel> { | |||
332 | 430 | ||
333 | const query: FindOptions = { | 431 | const query: FindOptions = { |
334 | attributes: { | 432 | attributes: { |
335 | include: [ | 433 | include: [ literalVideoQuotaUsed ] |
336 | [ | ||
337 | literal( | ||
338 | '(' + | ||
339 | 'SELECT COALESCE(SUM("size"), 0) ' + | ||
340 | 'FROM (' + | ||
341 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + | ||
342 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + | ||
343 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
344 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | ||
345 | 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' + | ||
346 | ') t' + | ||
347 | ')' | ||
348 | ), | ||
349 | 'videoQuotaUsed' | ||
350 | ] | ||
351 | ] | ||
352 | }, | 434 | }, |
353 | offset: start, | 435 | offset: start, |
354 | limit: count, | 436 | limit: count, |
@@ -430,8 +512,14 @@ export class UserModel extends Model<UserModel> { | |||
430 | return UserModel.findAll(query) | 512 | return UserModel.findAll(query) |
431 | } | 513 | } |
432 | 514 | ||
433 | static loadById (id: number): Bluebird<MUserDefault> { | 515 | static loadById (id: number, withStats = false): Bluebird<MUserDefault> { |
434 | return UserModel.findByPk(id) | 516 | const scopes = [ |
517 | ScopeNames.WITH_VIDEOCHANNELS | ||
518 | ] | ||
519 | |||
520 | if (withStats) scopes.push(ScopeNames.WITH_STATS) | ||
521 | |||
522 | return UserModel.scope(scopes).findByPk(id) | ||
435 | } | 523 | } |
436 | 524 | ||
437 | static loadByUsername (username: string): Bluebird<MUserDefault> { | 525 | static loadByUsername (username: string): Bluebird<MUserDefault> { |
@@ -637,6 +725,10 @@ export class UserModel extends Model<UserModel> { | |||
637 | toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { | 725 | toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { |
638 | const videoQuotaUsed = this.get('videoQuotaUsed') | 726 | const videoQuotaUsed = this.get('videoQuotaUsed') |
639 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') | 727 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') |
728 | const videosCount = this.get('videosCount') | ||
729 | const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') | ||
730 | const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') | ||
731 | const videoCommentsCount = this.get('videoCommentsCount') | ||
640 | 732 | ||
641 | const json: User = { | 733 | const json: User = { |
642 | id: this.id, | 734 | id: this.id, |
@@ -666,6 +758,21 @@ export class UserModel extends Model<UserModel> { | |||
666 | videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined | 758 | videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined |
667 | ? parseInt(videoQuotaUsedDaily + '', 10) | 759 | ? parseInt(videoQuotaUsedDaily + '', 10) |
668 | : undefined, | 760 | : undefined, |
761 | videosCount: videosCount !== undefined | ||
762 | ? parseInt(videosCount + '', 10) | ||
763 | : undefined, | ||
764 | videoAbusesCount: videoAbusesCount | ||
765 | ? parseInt(videoAbusesCount, 10) | ||
766 | : undefined, | ||
767 | videoAbusesAcceptedCount: videoAbusesAcceptedCount | ||
768 | ? parseInt(videoAbusesAcceptedCount, 10) | ||
769 | : undefined, | ||
770 | videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined | ||
771 | ? parseInt(videoAbusesCreatedCount + '', 10) | ||
772 | : undefined, | ||
773 | videoCommentsCount: videoCommentsCount !== undefined | ||
774 | ? parseInt(videoCommentsCount + '', 10) | ||
775 | : undefined, | ||
669 | 776 | ||
670 | noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, | 777 | noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, |
671 | noWelcomeModal: this.noWelcomeModal, | 778 | noWelcomeModal: this.noWelcomeModal, |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 502eac0bb..3e1a0c19b 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { MyUser, User, UserRole, Video, VideoPlaylistType } from '../../../../shared/index' | 5 | import { MyUser, User, UserRole, Video, VideoPlaylistType, VideoAbuseState, VideoAbuseUpdate } from '../../../../shared/index' |
6 | import { | 6 | import { |
7 | blockUser, | 7 | blockUser, |
8 | cleanupTests, | 8 | cleanupTests, |
@@ -33,7 +33,11 @@ import { | |||
33 | updateMyUser, | 33 | updateMyUser, |
34 | updateUser, | 34 | updateUser, |
35 | uploadVideo, | 35 | uploadVideo, |
36 | userLogin | 36 | userLogin, |
37 | reportVideoAbuse, | ||
38 | addVideoCommentThread, | ||
39 | updateVideoAbuse, | ||
40 | getVideoAbusesList | ||
37 | } from '../../../../shared/extra-utils' | 41 | } from '../../../../shared/extra-utils' |
38 | import { follow } from '../../../../shared/extra-utils/server/follows' | 42 | import { follow } from '../../../../shared/extra-utils/server/follows' |
39 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 43 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
@@ -254,7 +258,7 @@ describe('Test users', function () { | |||
254 | const res1 = await getMyUserInformation(server.url, accessTokenUser) | 258 | const res1 = await getMyUserInformation(server.url, accessTokenUser) |
255 | const userMe: MyUser = res1.body | 259 | const userMe: MyUser = res1.body |
256 | 260 | ||
257 | const res2 = await getUserInformation(server.url, server.accessToken, userMe.id) | 261 | const res2 = await getUserInformation(server.url, server.accessToken, userMe.id, true) |
258 | const userGet: User = res2.body | 262 | const userGet: User = res2.body |
259 | 263 | ||
260 | for (const user of [ userMe, userGet ]) { | 264 | for (const user of [ userMe, userGet ]) { |
@@ -273,6 +277,16 @@ describe('Test users', function () { | |||
273 | 277 | ||
274 | expect(userMe.specialPlaylists).to.have.lengthOf(1) | 278 | expect(userMe.specialPlaylists).to.have.lengthOf(1) |
275 | expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) | 279 | expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) |
280 | |||
281 | // Check stats are included with withStats | ||
282 | expect(userGet.videosCount).to.be.a('number') | ||
283 | expect(userGet.videosCount).to.equal(0) | ||
284 | expect(userGet.videoCommentsCount).to.be.a('number') | ||
285 | expect(userGet.videoCommentsCount).to.equal(0) | ||
286 | expect(userGet.videoAbusesCount).to.be.a('number') | ||
287 | expect(userGet.videoAbusesCount).to.equal(0) | ||
288 | expect(userGet.videoAbusesAcceptedCount).to.be.a('number') | ||
289 | expect(userGet.videoAbusesAcceptedCount).to.equal(0) | ||
276 | }) | 290 | }) |
277 | }) | 291 | }) |
278 | 292 | ||
@@ -623,7 +637,6 @@ describe('Test users', function () { | |||
623 | }) | 637 | }) |
624 | 638 | ||
625 | describe('Updating another user', function () { | 639 | describe('Updating another user', function () { |
626 | |||
627 | it('Should be able to update another user', async function () { | 640 | it('Should be able to update another user', async function () { |
628 | await updateUser({ | 641 | await updateUser({ |
629 | url: server.url, | 642 | url: server.url, |
@@ -698,6 +711,8 @@ describe('Test users', function () { | |||
698 | }) | 711 | }) |
699 | 712 | ||
700 | describe('Registering a new user', function () { | 713 | describe('Registering a new user', function () { |
714 | let user15AccessToken | ||
715 | |||
701 | it('Should register a new user', async function () { | 716 | it('Should register a new user', async function () { |
702 | const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } | 717 | const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } |
703 | const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } | 718 | const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } |
@@ -711,18 +726,18 @@ describe('Test users', function () { | |||
711 | password: 'my super password' | 726 | password: 'my super password' |
712 | } | 727 | } |
713 | 728 | ||
714 | accessToken = await userLogin(server, user15) | 729 | user15AccessToken = await userLogin(server, user15) |
715 | }) | 730 | }) |
716 | 731 | ||
717 | it('Should have the correct display name', async function () { | 732 | it('Should have the correct display name', async function () { |
718 | const res = await getMyUserInformation(server.url, accessToken) | 733 | const res = await getMyUserInformation(server.url, user15AccessToken) |
719 | const user: User = res.body | 734 | const user: User = res.body |
720 | 735 | ||
721 | expect(user.account.displayName).to.equal('super user 15') | 736 | expect(user.account.displayName).to.equal('super user 15') |
722 | }) | 737 | }) |
723 | 738 | ||
724 | it('Should have the correct video quota', async function () { | 739 | it('Should have the correct video quota', async function () { |
725 | const res = await getMyUserInformation(server.url, accessToken) | 740 | const res = await getMyUserInformation(server.url, user15AccessToken) |
726 | const user = res.body | 741 | const user = res.body |
727 | 742 | ||
728 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | 743 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) |
@@ -740,7 +755,7 @@ describe('Test users', function () { | |||
740 | expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined | 755 | expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined |
741 | } | 756 | } |
742 | 757 | ||
743 | await deleteMe(server.url, accessToken) | 758 | await deleteMe(server.url, user15AccessToken) |
744 | 759 | ||
745 | { | 760 | { |
746 | const res = await getUsersList(server.url, server.accessToken) | 761 | const res = await getUsersList(server.url, server.accessToken) |
@@ -750,6 +765,9 @@ describe('Test users', function () { | |||
750 | }) | 765 | }) |
751 | 766 | ||
752 | describe('User blocking', function () { | 767 | describe('User blocking', function () { |
768 | let user16Id | ||
769 | let user16AccessToken | ||
770 | |||
753 | it('Should block and unblock a user', async function () { | 771 | it('Should block and unblock a user', async function () { |
754 | const user16 = { | 772 | const user16 = { |
755 | username: 'user_16', | 773 | username: 'user_16', |
@@ -761,19 +779,95 @@ describe('Test users', function () { | |||
761 | username: user16.username, | 779 | username: user16.username, |
762 | password: user16.password | 780 | password: user16.password |
763 | }) | 781 | }) |
764 | const user16Id = resUser.body.user.id | 782 | user16Id = resUser.body.user.id |
765 | 783 | ||
766 | accessToken = await userLogin(server, user16) | 784 | user16AccessToken = await userLogin(server, user16) |
767 | 785 | ||
768 | await getMyUserInformation(server.url, accessToken, 200) | 786 | await getMyUserInformation(server.url, user16AccessToken, 200) |
769 | await blockUser(server.url, user16Id, server.accessToken) | 787 | await blockUser(server.url, user16Id, server.accessToken) |
770 | 788 | ||
771 | await getMyUserInformation(server.url, accessToken, 401) | 789 | await getMyUserInformation(server.url, user16AccessToken, 401) |
772 | await userLogin(server, user16, 400) | 790 | await userLogin(server, user16, 400) |
773 | 791 | ||
774 | await unblockUser(server.url, user16Id, server.accessToken) | 792 | await unblockUser(server.url, user16Id, server.accessToken) |
775 | accessToken = await userLogin(server, user16) | 793 | user16AccessToken = await userLogin(server, user16) |
776 | await getMyUserInformation(server.url, accessToken, 200) | 794 | await getMyUserInformation(server.url, user16AccessToken, 200) |
795 | }) | ||
796 | }) | ||
797 | |||
798 | describe('User stats', function () { | ||
799 | let user17Id | ||
800 | let user17AccessToken | ||
801 | |||
802 | it('Should report correct initial statistics about a user', async function () { | ||
803 | const user17 = { | ||
804 | username: 'user_17', | ||
805 | password: 'my super password' | ||
806 | } | ||
807 | const resUser = await createUser({ | ||
808 | url: server.url, | ||
809 | accessToken: server.accessToken, | ||
810 | username: user17.username, | ||
811 | password: user17.password | ||
812 | }) | ||
813 | |||
814 | user17Id = resUser.body.user.id | ||
815 | user17AccessToken = await userLogin(server, user17) | ||
816 | |||
817 | const res = await getUserInformation(server.url, server.accessToken, user17Id, true) | ||
818 | const user: User = res.body | ||
819 | |||
820 | expect(user.videosCount).to.equal(0) | ||
821 | expect(user.videoCommentsCount).to.equal(0) | ||
822 | expect(user.videoAbusesCount).to.equal(0) | ||
823 | expect(user.videoAbusesCreatedCount).to.equal(0) | ||
824 | expect(user.videoAbusesAcceptedCount).to.equal(0) | ||
825 | }) | ||
826 | |||
827 | it('Should report correct videos count', async function () { | ||
828 | const videoAttributes = { | ||
829 | name: 'video to test user stats' | ||
830 | } | ||
831 | await uploadVideo(server.url, user17AccessToken, videoAttributes) | ||
832 | const res1 = await getVideosList(server.url) | ||
833 | videoId = res1.body.data.find(video => video.name === videoAttributes.name).id | ||
834 | |||
835 | const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) | ||
836 | const user: User = res2.body | ||
837 | |||
838 | expect(user.videosCount).to.equal(1) | ||
839 | }) | ||
840 | |||
841 | it('Should report correct video comments for user', async function () { | ||
842 | const text = 'super comment' | ||
843 | await addVideoCommentThread(server.url, user17AccessToken, videoId, text) | ||
844 | |||
845 | const res = await getUserInformation(server.url, server.accessToken, user17Id, true) | ||
846 | const user: User = res.body | ||
847 | |||
848 | expect(user.videoCommentsCount).to.equal(1) | ||
849 | }) | ||
850 | |||
851 | it('Should report correct video abuses counts', async function () { | ||
852 | const reason = 'my super bad reason' | ||
853 | await reportVideoAbuse(server.url, user17AccessToken, videoId, reason) | ||
854 | |||
855 | const res1 = await getVideoAbusesList(server.url, server.accessToken) | ||
856 | const abuseId = res1.body.data[0].id | ||
857 | |||
858 | const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) | ||
859 | const user2: User = res2.body | ||
860 | |||
861 | expect(user2.videoAbusesCount).to.equal(1) // number of incriminations | ||
862 | expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created | ||
863 | |||
864 | const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED } | ||
865 | await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body) | ||
866 | |||
867 | const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) | ||
868 | const user3: User = res3.body | ||
869 | |||
870 | expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted | ||
777 | }) | 871 | }) |
778 | }) | 872 | }) |
779 | 873 | ||
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index 248af2d6e..54b506bce 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts | |||
@@ -130,11 +130,12 @@ function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatu | |||
130 | .expect('Content-Type', /json/) | 130 | .expect('Content-Type', /json/) |
131 | } | 131 | } |
132 | 132 | ||
133 | function getUserInformation (url: string, accessToken: string, userId: number) { | 133 | function getUserInformation (url: string, accessToken: string, userId: number, withStats = false) { |
134 | const path = '/api/v1/users/' + userId | 134 | const path = '/api/v1/users/' + userId |
135 | 135 | ||
136 | return request(url) | 136 | return request(url) |
137 | .get(path) | 137 | .get(path) |
138 | .query({ withStats }) | ||
138 | .set('Accept', 'application/json') | 139 | .set('Accept', 'application/json') |
139 | .set('Authorization', 'Bearer ' + accessToken) | 140 | .set('Authorization', 'Bearer ' + accessToken) |
140 | .expect(200) | 141 | .expect(200) |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index efb451014..a9c9bce30 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -31,6 +31,11 @@ export interface User { | |||
31 | videoQuotaDaily: number | 31 | videoQuotaDaily: number |
32 | videoQuotaUsed?: number | 32 | videoQuotaUsed?: number |
33 | videoQuotaUsedDaily?: number | 33 | videoQuotaUsedDaily?: number |
34 | videosCount?: number | ||
35 | videoAbusesCount?: number | ||
36 | videoAbusesAcceptedCount?: number | ||
37 | videoAbusesCreatedCount?: number | ||
38 | videoCommentsCount? : number | ||
34 | 39 | ||
35 | theme: string | 40 | theme: string |
36 | 41 | ||