Add ability to delete history element
This commit is contained in:
parent
3b83faccff
commit
7177b46ca1
11 changed files with 181 additions and 26 deletions
|
@ -13,9 +13,9 @@
|
||||||
<label i18n>Track watch history</label>
|
<label i18n>Track watch history</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="delete-history" (click)="deleteHistory()" i18n>
|
<button class="delete-history" (click)="clearAllHistory()" i18n>
|
||||||
<my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
|
||||||
Delete history
|
Clear all history
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -30,4 +30,10 @@
|
||||||
[enableSelection]="false"
|
[enableSelection]="false"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
#videosSelection
|
#videosSelection
|
||||||
></my-videos-selection>
|
>
|
||||||
|
<ng-template ptTemplate="rowButtons" let-video>
|
||||||
|
<div class="action-button">
|
||||||
|
<my-delete-button i18n-label label="Delete from history" (click)="deleteHistoryElement(video)"></my-delete-button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</my-videos-selection>
|
||||||
|
|
|
@ -53,6 +53,11 @@
|
||||||
@include row-blocks($column-responsive: false);
|
@include row-blocks($column-responsive: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $small-view) {
|
@media screen and (max-width: $small-view) {
|
||||||
.top-buttons {
|
.top-buttons {
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
|
|
|
@ -123,14 +123,25 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteHistory () {
|
deleteHistoryElement (video: Video) {
|
||||||
|
this.userHistoryService.deleteUserVideoHistoryElement(video)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.videos = this.videos.filter(v => v.id !== video.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAllHistory () {
|
||||||
const title = $localize`Delete videos history`
|
const title = $localize`Delete videos history`
|
||||||
const message = $localize`Are you sure you want to delete all your videos history?`
|
const message = $localize`Are you sure you want to delete all your videos history?`
|
||||||
|
|
||||||
const res = await this.confirmService.confirm(message, title)
|
const res = await this.confirmService.confirm(message, title)
|
||||||
if (res !== true) return
|
if (res !== true) return
|
||||||
|
|
||||||
this.userHistoryService.deleteUserVideosHistory()
|
this.userHistoryService.clearAllUserVideosHistory()
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success($localize`Videos history deleted`)
|
this.notifier.success($localize`Videos history deleted`)
|
||||||
|
|
|
@ -34,7 +34,13 @@ export class UserHistoryService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUserVideosHistory () {
|
deleteUserVideoHistoryElement (video: Video) {
|
||||||
|
return this.authHttp
|
||||||
|
.delete(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/' + video.id)
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllUserVideosHistory () {
|
||||||
return this.authHttp
|
return this.authHttp
|
||||||
.post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
|
.post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
userHistoryListValidator,
|
userHistoryListValidator,
|
||||||
userHistoryRemoveValidator
|
userHistoryRemoveAllValidator,
|
||||||
|
userHistoryRemoveElementValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
|
import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
|
||||||
|
|
||||||
|
@ -23,10 +24,16 @@ myVideosHistoryRouter.get('/me/history/videos',
|
||||||
asyncMiddleware(listMyVideosHistory)
|
asyncMiddleware(listMyVideosHistory)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
myVideosHistoryRouter.delete('/me/history/videos/:videoId',
|
||||||
|
authenticate,
|
||||||
|
userHistoryRemoveElementValidator,
|
||||||
|
asyncMiddleware(removeUserHistoryElement)
|
||||||
|
)
|
||||||
|
|
||||||
myVideosHistoryRouter.post('/me/history/videos/remove',
|
myVideosHistoryRouter.post('/me/history/videos/remove',
|
||||||
authenticate,
|
authenticate,
|
||||||
userHistoryRemoveValidator,
|
userHistoryRemoveAllValidator,
|
||||||
asyncRetryTransactionMiddleware(removeUserHistory)
|
asyncRetryTransactionMiddleware(removeAllUserHistory)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -45,7 +52,15 @@ async function listMyVideosHistory (req: express.Request, res: express.Response)
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeUserHistory (req: express.Request, res: express.Response) {
|
async function removeUserHistoryElement (req: express.Request, res: express.Response) {
|
||||||
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
|
await UserVideoHistoryModel.removeUserHistoryElement(user, parseInt(req.params.videoId + ''))
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAllUserHistory (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
const beforeDate = req.body.beforeDate || null
|
const beforeDate = req.body.beforeDate || null
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body, query } from 'express-validator'
|
import { body, param, query } from 'express-validator'
|
||||||
import { exists, isDateValid } from '../../helpers/custom-validators/misc'
|
import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './shared'
|
import { areValidationErrors } from './shared'
|
||||||
|
|
||||||
|
@ -18,13 +18,26 @@ const userHistoryListValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const userHistoryRemoveValidator = [
|
const userHistoryRemoveAllValidator = [
|
||||||
body('beforeDate')
|
body('beforeDate')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
|
.custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
|
||||||
|
|
||||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
|
logger.debug('Checking userHistoryRemoveAllValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const userHistoryRemoveElementValidator = [
|
||||||
|
param('videoId')
|
||||||
|
.custom(isIdValid).withMessage('Should have a valid video id'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking userHistoryRemoveElementValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
@ -36,5 +49,6 @@ const userHistoryRemoveValidator = [
|
||||||
|
|
||||||
export {
|
export {
|
||||||
userHistoryListValidator,
|
userHistoryListValidator,
|
||||||
userHistoryRemoveValidator
|
userHistoryRemoveElementValidator,
|
||||||
|
userHistoryRemoveAllValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,17 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static removeUserHistoryElement (user: MUserId, videoId: number) {
|
||||||
|
const query: DestroyOptions = {
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
videoId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserVideoHistoryModel.destroy(query)
|
||||||
|
}
|
||||||
|
|
||||||
static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
|
static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
|
||||||
const query: DestroyOptions = {
|
const query: DestroyOptions = {
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { HttpStatusCode } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
makeDeleteRequest,
|
||||||
makeGetRequest,
|
makeGetRequest,
|
||||||
makePostBodyRequest,
|
makePostBodyRequest,
|
||||||
makePutBodyRequest,
|
makePutBodyRequest,
|
||||||
|
@ -18,6 +19,7 @@ describe('Test videos history API validator', function () {
|
||||||
const myHistoryRemove = myHistoryPath + '/remove'
|
const myHistoryRemove = myHistoryPath + '/remove'
|
||||||
let watchingPath: string
|
let watchingPath: string
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
let videoId: number
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -28,8 +30,9 @@ describe('Test videos history API validator', function () {
|
||||||
|
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
const { uuid } = await server.videos.upload()
|
const { id, uuid } = await server.videos.upload()
|
||||||
watchingPath = '/api/v1/videos/' + uuid + '/watching'
|
watchingPath = '/api/v1/videos/' + uuid + '/watching'
|
||||||
|
videoId = id
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When notifying a user is watching a video', function () {
|
describe('When notifying a user is watching a video', function () {
|
||||||
|
@ -106,7 +109,37 @@ describe('Test videos history API validator', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When removing user videos history', function () {
|
describe('When removing a specific user video history element', function () {
|
||||||
|
let path: string
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
path = myHistoryPath + '/' + videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad videoId parameter', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: myHistoryRemove + '/hi',
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct parameters', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When removing all user videos history', function () {
|
||||||
it('Should fail with an unauthenticated user', async function () {
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ const expect = chai.expect
|
||||||
|
|
||||||
describe('Test videos history', function () {
|
describe('Test videos history', function () {
|
||||||
let server: PeerTubeServer = null
|
let server: PeerTubeServer = null
|
||||||
|
let video1Id: number
|
||||||
let video1UUID: string
|
let video1UUID: string
|
||||||
let video2UUID: string
|
let video2UUID: string
|
||||||
let video3UUID: string
|
let video3UUID: string
|
||||||
|
@ -34,8 +35,9 @@ describe('Test videos history', function () {
|
||||||
command = server.history
|
command = server.history
|
||||||
|
|
||||||
{
|
{
|
||||||
const { uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
|
const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
|
||||||
video1UUID = uuid
|
video1UUID = uuid
|
||||||
|
video1Id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -68,8 +70,8 @@ describe('Test videos history', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should watch the first and second video', async function () {
|
it('Should watch the first and second video', async function () {
|
||||||
await command.wathVideo({ videoId: video2UUID, currentTime: 8 })
|
await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
|
||||||
await command.wathVideo({ videoId: video1UUID, currentTime: 3 })
|
await command.watchVideo({ videoId: video1UUID, currentTime: 3 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the correct history when listing, searching and getting videos', async function () {
|
it('Should return the correct history when listing, searching and getting videos', async function () {
|
||||||
|
@ -122,7 +124,7 @@ describe('Test videos history', function () {
|
||||||
|
|
||||||
it('Should have these videos when listing my history', async function () {
|
it('Should have these videos when listing my history', async function () {
|
||||||
video3WatchedDate = new Date()
|
video3WatchedDate = new Date()
|
||||||
await command.wathVideo({ videoId: video3UUID, currentTime: 2 })
|
await command.watchVideo({ videoId: video3UUID, currentTime: 2 })
|
||||||
|
|
||||||
const body = await command.list()
|
const body = await command.list()
|
||||||
|
|
||||||
|
@ -150,7 +152,7 @@ describe('Test videos history', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should clear my history', async function () {
|
it('Should clear my history', async function () {
|
||||||
await command.remove({ beforeDate: video3WatchedDate.toISOString() })
|
await command.removeAll({ beforeDate: video3WatchedDate.toISOString() })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have my history cleared', async function () {
|
it('Should have my history cleared', async function () {
|
||||||
|
@ -166,7 +168,7 @@ describe('Test videos history', function () {
|
||||||
videosHistoryEnabled: false
|
videosHistoryEnabled: false
|
||||||
})
|
})
|
||||||
|
|
||||||
await command.wathVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
await command.watchVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should re-enable videos history', async function () {
|
it('Should re-enable videos history', async function () {
|
||||||
|
@ -174,7 +176,7 @@ describe('Test videos history', function () {
|
||||||
videosHistoryEnabled: true
|
videosHistoryEnabled: true
|
||||||
})
|
})
|
||||||
|
|
||||||
await command.wathVideo({ videoId: video1UUID, currentTime: 8 })
|
await command.watchVideo({ videoId: video1UUID, currentTime: 8 })
|
||||||
|
|
||||||
const body = await command.list()
|
const body = await command.list()
|
||||||
expect(body.total).to.equal(2)
|
expect(body.total).to.equal(2)
|
||||||
|
@ -212,6 +214,26 @@ describe('Test videos history', function () {
|
||||||
expect(body.total).to.equal(0)
|
expect(body.total).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should delete a specific history element', async function () {
|
||||||
|
{
|
||||||
|
await command.watchVideo({ videoId: video1UUID, currentTime: 4 })
|
||||||
|
await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const body = await command.list()
|
||||||
|
expect(body.total).to.equal(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await command.removeElement({ videoId: video1Id })
|
||||||
|
|
||||||
|
const body = await command.list()
|
||||||
|
expect(body.total).to.equal(1)
|
||||||
|
expect(body.data[0].uuid).to.equal(video2UUID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||||
|
|
||||||
export class HistoryCommand extends AbstractCommand {
|
export class HistoryCommand extends AbstractCommand {
|
||||||
|
|
||||||
wathVideo (options: OverrideCommandOptions & {
|
watchVideo (options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
currentTime: number
|
currentTime: number
|
||||||
}) {
|
}) {
|
||||||
|
@ -40,7 +40,22 @@ export class HistoryCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
remove (options: OverrideCommandOptions & {
|
removeElement (options: OverrideCommandOptions & {
|
||||||
|
videoId: number
|
||||||
|
}) {
|
||||||
|
const { videoId } = options
|
||||||
|
const path = '/api/v1/users/me/history/videos/' + videoId
|
||||||
|
|
||||||
|
return this.deleteRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll (options: OverrideCommandOptions & {
|
||||||
beforeDate?: string
|
beforeDate?: string
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { beforeDate } = options
|
const { beforeDate } = options
|
||||||
|
|
|
@ -1476,6 +1476,23 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/VideoListResponse'
|
$ref: '#/components/schemas/VideoListResponse'
|
||||||
|
|
||||||
|
/users/me/history/videos/{videoId}:
|
||||||
|
delete:
|
||||||
|
summary: Delete history element
|
||||||
|
security:
|
||||||
|
- OAuth2: []
|
||||||
|
tags:
|
||||||
|
- My History
|
||||||
|
parameters:
|
||||||
|
- name: videoId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Video/properties/id'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
/users/me/history/videos/remove:
|
/users/me/history/videos/remove:
|
||||||
post:
|
post:
|
||||||
summary: Clear video history
|
summary: Clear video history
|
||||||
|
|
Loading…
Reference in a new issue