From 978c87e7f58b6673fe60f04f1767bc9e02ea4936 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 20 Oct 2021 09:05:43 +0200 Subject: [PATCH] Add channel filters for my videos/followers --- .../video-block-list.component.ts | 17 ++++--- .../video-comment-list.component.ts | 17 ++++--- .../users/user-list/user-list.component.ts | 9 +++- .../my-follows/my-followers.component.ts | 11 ++++- .../my-videos/my-videos.component.ts | 46 +++++++++++++++---- .../abuse-list-table.component.ts | 41 +++++++++-------- .../advanced-input-filter.component.html | 10 ++-- .../advanced-input-filter.component.ts | 8 +++- .../shared/shared-main/video/video.service.ts | 20 +++++++- server/controllers/api/users/me.ts | 4 +- server/middlewares/validators/users.ts | 27 ++++++++++- server/models/video/video.ts | 9 +++- server/tests/api/check-params/videos.ts | 14 ++++++ server/tests/api/users/users.ts | 25 ++++++++++ shared/extra-utils/videos/videos-command.ts | 3 +- 15 files changed, 207 insertions(+), 54 deletions(-) diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 3edcb1c63..7baf34ca2 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts @@ -28,12 +28,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit { inputFilters: AdvancedInputFilter[] = [ { - queryParams: { search: 'type:auto' }, - label: $localize`Automatic blocks` - }, - { - queryParams: { search: 'type:manual' }, - label: $localize`Manual blocks` + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'type:auto' }, + label: $localize`Automatic blocks` + }, + { + queryParams: { search: 'type:manual' }, + label: $localize`Manual blocks` + } + ] } ] diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts index c09ce7293..a60b228af 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts @@ -44,12 +44,17 @@ export class VideoCommentListComponent extends RestTable implements OnInit { inputFilters: AdvancedInputFilter[] = [ { - queryParams: { search: 'local:true' }, - label: $localize`Local comments` - }, - { - queryParams: { search: 'local:false' }, - label: $localize`Remote comments` + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'local:true' }, + label: $localize`Local comments` + }, + { + queryParams: { search: 'local:false' }, + label: $localize`Remote comments` + } + ] } ] diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 1030759df..548e6e80f 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts @@ -36,8 +36,13 @@ export class UserListComponent extends RestTable implements OnInit { inputFilters: AdvancedInputFilter[] = [ { - queryParams: { search: 'banned:true' }, - label: $localize`Banned users` + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'banned:true' }, + label: $localize`Banned users` + } + ] } ] diff --git a/client/src/app/+my-library/my-follows/my-followers.component.ts b/client/src/app/+my-library/my-follows/my-followers.component.ts index 413d524df..4a72b983f 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.ts +++ b/client/src/app/+my-library/my-follows/my-followers.component.ts @@ -37,12 +37,19 @@ export class MyFollowersComponent implements OnInit { } this.auth.userInformationLoaded.subscribe(() => { - this.inputFilters = this.auth.getUser().videoChannels.map(c => { + const channelFilters = this.auth.getUser().videoChannels.map(c => { return { queryParams: { search: 'channel:' + c.name }, - label: $localize`Followers of ${c.name}` + label: c.name } }) + + this.inputFilters = [ + { + title: $localize`Channel filters`, + children: channelFilters + } + ] }) } diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index b1f3baf80..a117d0915 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts @@ -9,7 +9,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms' import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' -import { VideoSortField } from '@shared/models' +import { VideoChannel, VideoSortField } from '@shared/models' import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' @Component({ @@ -47,16 +47,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { user: User - inputFilters: AdvancedInputFilter[] = [ - { - queryParams: { search: 'isLive:true' }, - label: $localize`Only live videos` - } - ] + inputFilters: AdvancedInputFilter[] disabled = false private search: string + private userChannels: VideoChannel[] = [] constructor ( protected router: Router, @@ -79,6 +75,35 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { if (this.route.snapshot.queryParams['search']) { this.search = this.route.snapshot.queryParams['search'] } + + this.authService.userInformationLoaded.subscribe(() => { + this.user = this.authService.getUser() + this.userChannels = this.user.videoChannels + + const channelFilters = this.userChannels.map(c => { + return { + queryParams: { search: 'channel:' + c.name }, + label: c.name + } + }) + + this.inputFilters = [ + { + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'isLive:true' }, + label: $localize`Only live videos` + } + ] + }, + + { + title: $localize`Channel filters`, + children: channelFilters + } + ] + }) } onSearch (search: string) { @@ -105,7 +130,12 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { getVideosObservable (page: number) { const newPagination = immutableAssign(this.pagination, { currentPage: page }) - return this.videoService.getMyVideos(newPagination, this.sort, this.search) + return this.videoService.getMyVideos({ + videoPagination: newPagination, + sort: this.sort, + userChannels: this.userChannels, + search: this.search + }) .pipe( tap(res => this.pagination.totalItems = res.total) ) diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 33e9fd8de..297993e39 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts @@ -39,24 +39,29 @@ export class AbuseListTableComponent extends RestTable implements OnInit { inputFilters: AdvancedInputFilter[] = [ { - queryParams: { search: 'state:pending' }, - label: $localize`Unsolved reports` - }, - { - queryParams: { search: 'state:accepted' }, - label: $localize`Accepted reports` - }, - { - queryParams: { search: 'state:rejected' }, - label: $localize`Refused reports` - }, - { - queryParams: { search: 'videoIs:blacklisted' }, - label: $localize`Reports with blocked videos` - }, - { - queryParams: { search: 'videoIs:deleted' }, - label: $localize`Reports with deleted videos` + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'state:pending' }, + label: $localize`Unsolved reports` + }, + { + queryParams: { search: 'state:accepted' }, + label: $localize`Accepted reports` + }, + { + queryParams: { search: 'state:rejected' }, + label: $localize`Refused reports` + }, + { + queryParams: { search: 'videoIs:blacklisted' }, + label: $localize`Reports with blocked videos` + }, + { + queryParams: { search: 'videoIs:deleted' }, + label: $localize`Reports with deleted videos` + } + ] } ] diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html index 10d1296cf..c662b9bb6 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.html +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html @@ -5,11 +5,13 @@
- + + - - {{ filter.label }} - + + {{ filter.label }} + +
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts index 8315662b4..a12dddf7a 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts @@ -5,8 +5,12 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@ import { ActivatedRoute, Params, Router } from '@angular/router' export type AdvancedInputFilter = { - label: string - queryParams: Params + title: string + + children: { + label: string + queryParams: Params + }[] } const logger = debug('peertube:AdvancedInputFilterComponent') diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 2f43f1b9d..7935569e7 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -13,6 +13,7 @@ import { UserVideoRateType, UserVideoRateUpdate, Video as VideoServerModel, + VideoChannel as VideoChannelServerModel, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFileMetadata, @@ -122,7 +123,14 @@ export class VideoService { .pipe(catchError(err => this.restExtractor.handleError(err))) } - getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable> { + getMyVideos (options: { + videoPagination: ComponentPaginationLight + sort: VideoSortField + userChannels?: VideoChannelServerModel[] + search?: string + }): Observable> { + const { videoPagination, sort, userChannels = [], search } = options + const pagination = this.restService.componentToRestPagination(videoPagination) let params = new HttpParams() @@ -133,6 +141,16 @@ export class VideoService { isLive: { prefix: 'isLive:', isBoolean: true + }, + channelId: { + prefix: 'channel:', + handler: (name: string) => { + const channel = userChannels.find(c => c.name === name) + + if (channel) return channel.id + + return undefined + } } }) diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 83b774d3c..6bacdbbb6 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -25,7 +25,7 @@ import { usersUpdateMeValidator, usersVideoRatingValidator } from '../../../middlewares' -import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' +import { deleteMeValidator, usersVideosValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' import { AccountModel } from '../../../models/account/account' import { AccountVideoRateModel } from '../../../models/account/account-video-rate' @@ -69,6 +69,7 @@ meRouter.get('/me/videos', videosSortValidator, setDefaultVideosSort, setDefaultPagination, + asyncMiddleware(usersVideosValidator), asyncMiddleware(getUserVideos) ) @@ -113,6 +114,7 @@ async function getUserVideos (req: express.Request, res: express.Response) { count: req.query.count, sort: req.query.sort, search: req.query.search, + channelId: res.locals.videoChannel?.id, isLive: req.query.isLive }, 'filter:api.user.me.videos.list.params') diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index c6eeeaf18..8f1a7801f 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -4,7 +4,7 @@ import { omit } from 'lodash' import { Hooks } from '@server/lib/plugins/hooks' import { MUserDefault } from '@server/types/models' import { HttpStatusCode, UserRegister, UserRole } from '@shared/models' -import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' +import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isUserAdminFlagsValid, @@ -31,7 +31,7 @@ import { Redis } from '../../lib/redis' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' import { ActorModel } from '../../models/actor/actor' import { UserModel } from '../../models/user/user' -import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' +import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared' const usersListValidator = [ query('blocked') @@ -318,6 +318,28 @@ const usersVideoRatingValidator = [ } ] +const usersVideosValidator = [ + query('isLive') + .optional() + .customSanitizer(toBooleanOrNull) + .custom(isBooleanValid).withMessage('Should have a valid live boolean'), + + query('channelId') + .optional() + .customSanitizer(toIntOrNull) + .custom(isIdValid).withMessage('Should have a valid channel id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking usersVideosValidator parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + + if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return + + return next() + } +] + const ensureUserRegistrationAllowed = [ async (req: express.Request, res: express.Response, next: express.NextFunction) => { const allowedParams = { @@ -513,6 +535,7 @@ export { ensureUserRegistrationAllowed, ensureUserRegistrationAllowedForIP, usersGetValidator, + usersVideosValidator, usersAskResetPasswordValidator, usersResetPasswordValidator, usersAskSendVerifyEmailValidator, diff --git a/server/models/video/video.ts b/server/models/video/video.ts index d2daf18ee..4044287ee 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -978,10 +978,12 @@ export class VideoModel extends Model>> { start: number count: number sort: string + + channelId?: number isLive?: boolean search?: string }) { - const { accountId, start, count, sort, search, isLive } = options + const { accountId, channelId, start, count, sort, search, isLive } = options function buildBaseQuery (): FindOptions { const where: WhereOptions = {} @@ -996,6 +998,10 @@ export class VideoModel extends Model>> { where.isLive = isLive } + const channelWhere = channelId + ? { id: channelId } + : {} + const baseQuery = { offset: start, limit: count, @@ -1005,6 +1011,7 @@ export class VideoModel extends Model>> { { model: VideoChannelModel, required: true, + where: channelWhere, include: [ { model: AccountModel, diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index e11ca0c82..d02b6e156 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -119,6 +119,20 @@ describe('Test videos API validator', function () { await checkBadSortPagination(server.url, path, server.accessToken) }) + it('Should fail with an invalid channel', async function () { + await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } }) + }) + + it('Should fail with an unknown channel', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { channelId: 89898 }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + it('Should success with the correct parameters', async function () { await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 }) }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 085d9d870..6c41e7d56 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -318,6 +318,8 @@ describe('Test users', function () { fixture: 'video_short.webm' } await server.videos.upload({ token: userToken, attributes }) + + await server.channels.create({ token: userToken, attributes: { name: 'other_channel' } }) }) it('Should have video quota updated', async function () { @@ -340,6 +342,29 @@ describe('Test users', function () { expect(video.previewPath).to.not.be.null }) + it('Should be able to filter by channel in my videos', async function () { + const myInfo = await server.users.getMyInfo({ token: userToken }) + const mainChannel = myInfo.videoChannels.find(c => c.name !== 'other_channel') + const otherChannel = myInfo.videoChannels.find(c => c.name === 'other_channel') + + { + const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: mainChannel.id }) + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + + const video: Video = data[0] + expect(video.name).to.equal('super user video') + expect(video.thumbnailPath).to.not.be.null + expect(video.previewPath).to.not.be.null + } + + { + const { total, data } = await server.videos.listMyVideos({ token: userToken, channelId: otherChannel.id }) + expect(total).to.equal(0) + expect(data).to.have.lengthOf(0) + } + }) + it('Should be able to search in my videos', async function () { { const { total, data } = await server.videos.listMyVideos({ token: userToken, sort: '-createdAt', search: 'user video' }) diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts index 99f56a34c..c1a9ec806 100644 --- a/shared/extra-utils/videos/videos-command.ts +++ b/shared/extra-utils/videos/videos-command.ts @@ -207,6 +207,7 @@ export class VideosCommand extends AbstractCommand { sort?: string search?: string isLive?: boolean + channelId?: number } = {}) { const path = '/api/v1/users/me/videos' @@ -214,7 +215,7 @@ export class VideosCommand extends AbstractCommand { ...options, path, - query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]), + query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]), implicitToken: true, defaultExpectedStatus: HttpStatusCode.OK_200 }) -- 2.41.0