Add ability to display all channel/account videos

This commit is contained in:
Chocobozzz 2020-11-18 15:29:38 +01:00
parent ff2cac9fa3
commit 0aa52e1707
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 161 additions and 41 deletions

View File

@ -3,7 +3,7 @@ import { concatMap, map, switchMap, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core'
import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { VideoSortField } from '@shared/models'
import { NSFWPolicyType, VideoSortField } from '@shared/models'
@Component({
selector: 'my-account-video-channels',
@ -31,6 +31,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
onChannelDataSubject = new Subject<any>()
userMiniature: User
nsfwPolicy: NSFWPolicyType
private accountSub: Subscription
@ -52,7 +53,11 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
})
this.userService.getAnonymousOrLoggedUser()
.subscribe(user => this.userMiniature = user)
.subscribe(user => {
this.userMiniature = user
this.nsfwPolicy = user.nsfwPolicy
})
}
ngOnDestroy () {
@ -65,7 +70,14 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
tap(res => this.channelPagination.totalItems = res.total),
switchMap(res => from(res.data)),
concatMap(videoChannel => {
return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
const options = {
videoChannel,
videoPagination: this.videosPagination,
sort: this.videosSort,
nsfwPolicy: this.nsfwPolicy
}
return this.videoService.getVideoChannelVideos(options)
.pipe(map(data => ({ videoChannel, videos: data.data })))
})
)

View File

@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers'
import { Account, AccountService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'
@Component({
selector: 'my-account-videos',
@ -18,6 +19,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
titlePage: string
loadOnInit = false
filter: VideoFilter = null
private account: Account
private accountSub: Subscription
@ -40,6 +43,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
ngOnInit () {
super.ngOnInit()
this.enableAllFilterIfPossible()
// Parent get the account for us
this.accountSub = this.accountService.accountLoaded
.pipe(first())
@ -59,9 +64,16 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
const options = {
account: this.account,
videoPagination: newPagination,
sort: this.sort,
nsfwPolicy: this.nsfwPolicy,
videoFilter: this.filter
}
return this.videoService
.getAccountVideos(this.account, newPagination, this.sort)
.getAccountVideos(options)
.pipe(
tap(({ total }) => {
this.titlePage = $localize`Published ${total} videos`
@ -69,6 +81,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
)
}
toggleModerationDisplay () {
this.filter = this.buildLocalFilter(this.filter, null)
this.reloadVideos()
}
generateSyndicationList () {
this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
}

View File

@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers'
import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'
@Component({
selector: 'my-video-channel-videos',
@ -18,6 +19,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
titlePage: string
loadOnInit = false
filter: VideoFilter = null
private videoChannel: VideoChannel
private videoChannelSub: Subscription
@ -46,6 +49,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
ngOnInit () {
super.ngOnInit()
this.enableAllFilterIfPossible()
// Parent get the video channel for us
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.pipe(first())
@ -65,9 +70,16 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
const options = {
videoChannel: this.videoChannel,
videoPagination: newPagination,
sort: this.sort,
nsfwPolicy: this.nsfwPolicy,
videoFilter: this.filter
}
return this.videoService
.getVideoChannelVideos(this.videoChannel, newPagination, this.sort, this.nsfwPolicy)
.getVideoChannelVideos(options)
.pipe(
tap(({ total }) => {
this.titlePage = total === 1
@ -80,4 +92,10 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
generateSyndicationList () {
this.syndicationItems = this.videoService.getVideoChannelFeedUrls(this.videoChannel.id)
}
toggleModerationDisplay () {
this.filter = this.buildLocalFilter(this.filter, null)
this.reloadVideos()
}
}

View File

@ -39,11 +39,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
ngOnInit () {
super.ngOnInit()
if (this.authService.isLoggedIn()) {
const user = this.authService.getUser()
this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
}
this.enableAllFilterIfPossible()
this.generateSyndicationList()
}
@ -77,7 +73,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
}
toggleModerationDisplay () {
this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local'
this.filter = this.buildLocalFilter(this.filter, 'local')
this.reloadVideos()
}

View File

@ -134,16 +134,28 @@ export class VideoService implements VideosProvider {
)
}
getAccountVideos (
getAccountVideos (parameters: {
account: Account,
videoPagination: ComponentPaginationLight,
sort: VideoSortField
): Observable<ResultList<Video>> {
nsfwPolicy?: NSFWPolicyType
videoFilter?: VideoFilter
}): Observable<ResultList<Video>> {
const { account, videoPagination, sort, videoFilter, nsfwPolicy } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
if (nsfwPolicy) {
params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
}
if (videoFilter) {
params = params.set('filter', videoFilter)
}
return this.authHttp
.get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
.pipe(
@ -152,12 +164,15 @@ export class VideoService implements VideosProvider {
)
}
getVideoChannelVideos (
getVideoChannelVideos (parameters: {
videoChannel: VideoChannel,
videoPagination: ComponentPaginationLight,
sort: VideoSortField,
nsfwPolicy?: NSFWPolicyType
): Observable<ResultList<Video>> {
videoFilter?: VideoFilter
}): Observable<ResultList<Video>> {
const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
@ -167,6 +182,10 @@ export class VideoService implements VideosProvider {
params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
}
if (videoFilter) {
params = params.set('filter', videoFilter)
}
return this.authHttp
.get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
.pipe(

View File

@ -21,7 +21,7 @@
<div class="dropdown-item">
<my-peertube-checkbox
(change)="toggleModerationDisplay()"
inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
inputName="display-unlisted-private" i18n-labelText labelText="Display all videos (private, unlisted or not yet published)"
></my-peertube-checkbox>
</div>
</div>

View File

@ -31,13 +31,21 @@ $iconSize: 16px;
.moderation-block {
div {
@include button-with-icon($iconSize, 3px, -1px);
@include button-with-icon($iconSize, 3px, -2px);
}
margin-left: .2rem;
margin-left: .4rem;
display: flex;
justify-content: flex-end;
align-items: center;
.dropdown-item {
padding: 0;
::ng-deep my-peertube-checkbox label {
padding: 3px 15px;
}
}
}
}

View File

@ -15,7 +15,7 @@ import {
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
import { GlobalIconName } from '@app/shared/shared-icons'
import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date'
import { ServerConfig, VideoSortField } from '@shared/models'
import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
import { Syndication, Video } from '../shared-main'
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
@ -205,10 +205,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
this.loadMoreVideos(true)
}
toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}
removeVideoFromArray (video: Video) {
this.videos = this.videos.filter(v => v.id !== video.id)
}
@ -268,6 +264,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
return this.groupedDateLabels[this.groupedDates[video.id]]
}
toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}
// On videos hook for children that want to do something
protected onMoreVideos () { /* empty */ }
@ -277,6 +277,28 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
this.angularState = routeParams[ 'a-state' ]
}
protected buildLocalFilter (existing: VideoFilter, base: VideoFilter) {
if (base === 'local') {
return existing === 'local'
? 'all-local' as 'all-local'
: 'local' as 'local'
}
return existing === 'all'
? null
: 'all'
}
protected enableAllFilterIfPossible () {
if (!this.authService.isLoggedIn()) return
this.authService.userInformationLoaded
.subscribe(() => {
const user = this.authService.getUser()
this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
})
}
private calcPageSizes () {
if (this.screenService.isInMobileView()) {
this.pagination.itemsPerPage = 5

View File

@ -65,6 +65,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
opacity: .9;
}
&:active {
color: pvar(--mainForegroundColor) !important;
}
&::after {
display: none;
}

View File

@ -17,7 +17,7 @@ import * as magnetUtil from 'magnet-uri'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local'
return filter === 'local' || filter === 'all-local' || filter === 'all'
}
function isVideoCategoryValid (value: any) {

View File

@ -429,7 +429,10 @@ const commonVideosFiltersValidator = [
if (areValidationErrors(req, res)) return
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
if (
(req.query.filter === 'all-local' || req.query.filter === 'all') &&
(!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
) {
res.status(401)
.json({ error: 'You are not allowed to see all local videos.' })

View File

@ -89,7 +89,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
}
// Only list public/published videos
if (!options.filter || options.filter !== 'all-local') {
if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) {
and.push(
`("video"."state" = ${VideoState.PUBLISHED} OR ` +
`("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`

View File

@ -1085,7 +1085,7 @@ export class VideoModel extends Model<VideoModel> {
historyOfUser?: MUserId
countVideos?: boolean
}) {
if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to filter all-local but no user has not the see all videos right')
}

View File

@ -78,28 +78,33 @@ describe('Test videos filters', function () {
await testEndpoints(server, server.accessToken, 'local', 200)
})
it('Should fail to list all-local with a simple user', async function () {
it('Should fail to list all-local/all with a simple user', async function () {
await testEndpoints(server, userAccessToken, 'all-local', 401)
await testEndpoints(server, userAccessToken, 'all', 401)
})
it('Should succeed to list all-local with a moderator', async function () {
it('Should succeed to list all-local/all with a moderator', async function () {
await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
await testEndpoints(server, moderatorAccessToken, 'all', 200)
})
it('Should succeed to list all-local with an admin', async function () {
it('Should succeed to list all-local/all with an admin', async function () {
await testEndpoints(server, server.accessToken, 'all-local', 200)
await testEndpoints(server, server.accessToken, 'all', 200)
})
// Because we cannot authenticate the user on the RSS endpoint
it('Should fail on the feeds endpoint with the all-local filter', async function () {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 401,
query: {
filter: 'all-local'
}
})
it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
for (const filter of [ 'all', 'all-local' ]) {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 401,
query: {
filter
}
})
}
})
it('Should succeed on the feeds endpoint with the local filter', async function () {

View File

@ -116,6 +116,20 @@ describe('Test videos filter validator', function () {
}
}
})
it('Should display all videos by the admin or the moderator', async function () {
for (const server of servers) {
for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
expect(channelVideos).to.have.lengthOf(3)
expect(accountVideos).to.have.lengthOf(3)
expect(videos).to.have.lengthOf(5)
expect(searchVideos).to.have.lengthOf(5)
}
}
})
})
after(async function () {

View File

@ -1 +1 @@
export type VideoFilter = 'local' | 'all-local'
export type VideoFilter = 'local' | 'all-local' | 'all'

View File

@ -3681,9 +3681,10 @@ components:
in: query
required: false
description: >
Special filters (local for instance) which might require special rights:
Special filters which might require special rights:
* `local` - only videos local to the instance
* `all-local` - only videos local to the instance, but showing private and unlisted videos (requires Admin privileges)
* `all` - all videos, showing private and unlisted videos (requires Admin privileges)
schema:
type: string
enum: