aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-01-18 11:23:41 +0100
committerChocobozzz <me@florianbigard.com>2022-01-18 11:23:41 +0100
commit7177b46ca1b35aa9d7ed39a06c1dcf41a4fc6180 (patch)
tree016cb0d966fe9fea8a6381eb246e966f5c4eae57
parent3b83faccfffc13adaef0b63c211b1ce4944e8b3b (diff)
downloadPeerTube-7177b46ca1b35aa9d7ed39a06c1dcf41a4fc6180.tar.gz
PeerTube-7177b46ca1b35aa9d7ed39a06c1dcf41a4fc6180.tar.zst
PeerTube-7177b46ca1b35aa9d7ed39a06c1dcf41a4fc6180.zip
Add ability to delete history element
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html12
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.scss5
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.ts15
-rw-r--r--client/src/app/shared/shared-main/users/user-history.service.ts8
-rw-r--r--server/controllers/api/users/my-history.ts23
-rw-r--r--server/middlewares/validators/user-history.ts24
-rw-r--r--server/models/user/user-video-history.ts11
-rw-r--r--server/tests/api/check-params/videos-history.ts37
-rw-r--r--server/tests/api/videos/videos-history.ts36
-rw-r--r--shared/server-commands/videos/history-command.ts19
-rw-r--r--support/doc/api/openapi.yaml17
11 files changed, 181 insertions, 26 deletions
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html
index 8e564cf93..14bf01804 100644
--- a/client/src/app/+my-library/my-history/my-history.component.html
+++ b/client/src/app/+my-library/my-history/my-history.component.html
@@ -13,9 +13,9 @@
13 <label i18n>Track watch history</label> 13 <label i18n>Track watch history</label>
14 </div> 14 </div>
15 15
16 <button class="delete-history" (click)="deleteHistory()" i18n> 16 <button class="delete-history" (click)="clearAllHistory()" i18n>
17 <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon> 17 <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
18 Delete history 18 Clear all history
19 </button> 19 </button>
20</div> 20</div>
21 21
@@ -30,4 +30,10 @@
30 [enableSelection]="false" 30 [enableSelection]="false"
31 [disabled]="disabled" 31 [disabled]="disabled"
32 #videosSelection 32 #videosSelection
33></my-videos-selection> 33>
34 <ng-template ptTemplate="rowButtons" let-video>
35 <div class="action-button">
36 <my-delete-button i18n-label label="Delete from history" (click)="deleteHistoryElement(video)"></my-delete-button>
37 </div>
38 </ng-template>
39</my-videos-selection>
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss
index cb8507569..3257b2215 100644
--- a/client/src/app/+my-library/my-history/my-history.component.scss
+++ b/client/src/app/+my-library/my-history/my-history.component.scss
@@ -53,6 +53,11 @@
53 @include row-blocks($column-responsive: false); 53 @include row-blocks($column-responsive: false);
54} 54}
55 55
56.action-button {
57 display: flex;
58 align-self: flex-end;
59}
60
56@media screen and (max-width: $small-view) { 61@media screen and (max-width: $small-view) {
57 .top-buttons { 62 .top-buttons {
58 grid-template-columns: auto 1fr auto; 63 grid-template-columns: auto 1fr auto;
diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts
index 95cfaee41..34efe5558 100644
--- a/client/src/app/+my-library/my-history/my-history.component.ts
+++ b/client/src/app/+my-library/my-history/my-history.component.ts
@@ -123,14 +123,25 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
123 }) 123 })
124 } 124 }
125 125
126 async deleteHistory () { 126 deleteHistoryElement (video: Video) {
127 this.userHistoryService.deleteUserVideoHistoryElement(video)
128 .subscribe({
129 next: () => {
130 this.videos = this.videos.filter(v => v.id !== video.id)
131 },
132
133 error: err => this.notifier.error(err.message)
134 })
135 }
136
137 async clearAllHistory () {
127 const title = $localize`Delete videos history` 138 const title = $localize`Delete videos history`
128 const message = $localize`Are you sure you want to delete all your videos history?` 139 const message = $localize`Are you sure you want to delete all your videos history?`
129 140
130 const res = await this.confirmService.confirm(message, title) 141 const res = await this.confirmService.confirm(message, title)
131 if (res !== true) return 142 if (res !== true) return
132 143
133 this.userHistoryService.deleteUserVideosHistory() 144 this.userHistoryService.clearAllUserVideosHistory()
134 .subscribe({ 145 .subscribe({
135 next: () => { 146 next: () => {
136 this.notifier.success($localize`Videos history deleted`) 147 this.notifier.success($localize`Videos history deleted`)
diff --git a/client/src/app/shared/shared-main/users/user-history.service.ts b/client/src/app/shared/shared-main/users/user-history.service.ts
index a4841897d..e28bcdca9 100644
--- a/client/src/app/shared/shared-main/users/user-history.service.ts
+++ b/client/src/app/shared/shared-main/users/user-history.service.ts
@@ -34,7 +34,13 @@ export class UserHistoryService {
34 ) 34 )
35 } 35 }
36 36
37 deleteUserVideosHistory () { 37 deleteUserVideoHistoryElement (video: Video) {
38 return this.authHttp
39 .delete(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/' + video.id)
40 .pipe(catchError(err => this.restExtractor.handleError(err)))
41 }
42
43 clearAllUserVideosHistory () {
38 return this.authHttp 44 return this.authHttp
39 .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {}) 45 .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
40 .pipe( 46 .pipe(
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
index 2fcb25acf..bc5b40f59 100644
--- a/server/controllers/api/users/my-history.ts
+++ b/server/controllers/api/users/my-history.ts
@@ -9,7 +9,8 @@ import {
9 paginationValidator, 9 paginationValidator,
10 setDefaultPagination, 10 setDefaultPagination,
11 userHistoryListValidator, 11 userHistoryListValidator,
12 userHistoryRemoveValidator 12 userHistoryRemoveAllValidator,
13 userHistoryRemoveElementValidator
13} from '../../../middlewares' 14} from '../../../middlewares'
14import { UserVideoHistoryModel } from '../../../models/user/user-video-history' 15import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
15 16
@@ -23,10 +24,16 @@ myVideosHistoryRouter.get('/me/history/videos',
23 asyncMiddleware(listMyVideosHistory) 24 asyncMiddleware(listMyVideosHistory)
24) 25)
25 26
27myVideosHistoryRouter.delete('/me/history/videos/:videoId',
28 authenticate,
29 userHistoryRemoveElementValidator,
30 asyncMiddleware(removeUserHistoryElement)
31)
32
26myVideosHistoryRouter.post('/me/history/videos/remove', 33myVideosHistoryRouter.post('/me/history/videos/remove',
27 authenticate, 34 authenticate,
28 userHistoryRemoveValidator, 35 userHistoryRemoveAllValidator,
29 asyncRetryTransactionMiddleware(removeUserHistory) 36 asyncRetryTransactionMiddleware(removeAllUserHistory)
30) 37)
31 38
32// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
@@ -45,7 +52,15 @@ async function listMyVideosHistory (req: express.Request, res: express.Response)
45 return res.json(getFormattedObjects(resultList.data, resultList.total)) 52 return res.json(getFormattedObjects(resultList.data, resultList.total))
46} 53}
47 54
48async function removeUserHistory (req: express.Request, res: express.Response) { 55async function removeUserHistoryElement (req: express.Request, res: express.Response) {
56 const user = res.locals.oauth.token.User
57
58 await UserVideoHistoryModel.removeUserHistoryElement(user, parseInt(req.params.videoId + ''))
59
60 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
61}
62
63async function removeAllUserHistory (req: express.Request, res: express.Response) {
49 const user = res.locals.oauth.token.User 64 const user = res.locals.oauth.token.User
50 const beforeDate = req.body.beforeDate || null 65 const beforeDate = req.body.beforeDate || null
51 66
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts
index f9be26627..541910be5 100644
--- a/server/middlewares/validators/user-history.ts
+++ b/server/middlewares/validators/user-history.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { body, query } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { exists, isDateValid } from '../../helpers/custom-validators/misc' 3import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './shared' 5import { areValidationErrors } from './shared'
6 6
@@ -18,13 +18,26 @@ const userHistoryListValidator = [
18 } 18 }
19] 19]
20 20
21const userHistoryRemoveValidator = [ 21const userHistoryRemoveAllValidator = [
22 body('beforeDate') 22 body('beforeDate')
23 .optional() 23 .optional()
24 .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'), 24 .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
25 25
26 (req: express.Request, res: express.Response, next: express.NextFunction) => { 26 (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) 27 logger.debug('Checking userHistoryRemoveAllValidator parameters', { parameters: req.body })
28
29 if (areValidationErrors(req, res)) return
30
31 return next()
32 }
33]
34
35const userHistoryRemoveElementValidator = [
36 param('videoId')
37 .custom(isIdValid).withMessage('Should have a valid video id'),
38
39 (req: express.Request, res: express.Response, next: express.NextFunction) => {
40 logger.debug('Checking userHistoryRemoveElementValidator parameters', { parameters: req.params })
28 41
29 if (areValidationErrors(req, res)) return 42 if (areValidationErrors(req, res)) return
30 43
@@ -36,5 +49,6 @@ const userHistoryRemoveValidator = [
36 49
37export { 50export {
38 userHistoryListValidator, 51 userHistoryListValidator,
39 userHistoryRemoveValidator 52 userHistoryRemoveElementValidator,
53 userHistoryRemoveAllValidator
40} 54}
diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts
index 92f4fe7a1..f4d0889a1 100644
--- a/server/models/user/user-video-history.ts
+++ b/server/models/user/user-video-history.ts
@@ -69,6 +69,17 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
69 }) 69 })
70 } 70 }
71 71
72 static removeUserHistoryElement (user: MUserId, videoId: number) {
73 const query: DestroyOptions = {
74 where: {
75 userId: user.id,
76 videoId
77 }
78 }
79
80 return UserVideoHistoryModel.destroy(query)
81 }
82
72 static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) { 83 static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
73 const query: DestroyOptions = { 84 const query: DestroyOptions = {
74 where: { 85 where: {
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
index 31a0752c7..82f38b7b4 100644
--- a/server/tests/api/check-params/videos-history.ts
+++ b/server/tests/api/check-params/videos-history.ts
@@ -6,6 +6,7 @@ import { HttpStatusCode } from '@shared/models'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createSingleServer, 8 createSingleServer,
9 makeDeleteRequest,
9 makeGetRequest, 10 makeGetRequest,
10 makePostBodyRequest, 11 makePostBodyRequest,
11 makePutBodyRequest, 12 makePutBodyRequest,
@@ -18,6 +19,7 @@ describe('Test videos history API validator', function () {
18 const myHistoryRemove = myHistoryPath + '/remove' 19 const myHistoryRemove = myHistoryPath + '/remove'
19 let watchingPath: string 20 let watchingPath: string
20 let server: PeerTubeServer 21 let server: PeerTubeServer
22 let videoId: number
21 23
22 // --------------------------------------------------------------- 24 // ---------------------------------------------------------------
23 25
@@ -28,8 +30,9 @@ describe('Test videos history API validator', function () {
28 30
29 await setAccessTokensToServers([ server ]) 31 await setAccessTokensToServers([ server ])
30 32
31 const { uuid } = await server.videos.upload() 33 const { id, uuid } = await server.videos.upload()
32 watchingPath = '/api/v1/videos/' + uuid + '/watching' 34 watchingPath = '/api/v1/videos/' + uuid + '/watching'
35 videoId = id
33 }) 36 })
34 37
35 describe('When notifying a user is watching a video', function () { 38 describe('When notifying a user is watching a video', function () {
@@ -106,7 +109,37 @@ describe('Test videos history API validator', function () {
106 }) 109 })
107 }) 110 })
108 111
109 describe('When removing user videos history', function () { 112 describe('When removing a specific user video history element', function () {
113 let path: string
114
115 before(function () {
116 path = myHistoryPath + '/' + videoId
117 })
118
119 it('Should fail with an unauthenticated user', async function () {
120 await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
121 })
122
123 it('Should fail with a bad videoId parameter', async function () {
124 await makeDeleteRequest({
125 url: server.url,
126 token: server.accessToken,
127 path: myHistoryRemove + '/hi',
128 expectedStatus: HttpStatusCode.BAD_REQUEST_400
129 })
130 })
131
132 it('Should succeed with the correct parameters', async function () {
133 await makeDeleteRequest({
134 url: server.url,
135 token: server.accessToken,
136 path,
137 expectedStatus: HttpStatusCode.NO_CONTENT_204
138 })
139 })
140 })
141
142 describe('When removing all user videos history', function () {
110 it('Should fail with an unauthenticated user', async function () { 143 it('Should fail with an unauthenticated user', async function () {
111 await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) 144 await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
112 }) 145 })
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
index 4e5ba13aa..8648c97f0 100644
--- a/server/tests/api/videos/videos-history.ts
+++ b/server/tests/api/videos/videos-history.ts
@@ -17,6 +17,7 @@ const expect = chai.expect
17 17
18describe('Test videos history', function () { 18describe('Test videos history', function () {
19 let server: PeerTubeServer = null 19 let server: PeerTubeServer = null
20 let video1Id: number
20 let video1UUID: string 21 let video1UUID: string
21 let video2UUID: string 22 let video2UUID: string
22 let video3UUID: string 23 let video3UUID: string
@@ -34,8 +35,9 @@ describe('Test videos history', function () {
34 command = server.history 35 command = server.history
35 36
36 { 37 {
37 const { uuid } = await server.videos.upload({ attributes: { name: 'video 1' } }) 38 const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
38 video1UUID = uuid 39 video1UUID = uuid
40 video1Id = id
39 } 41 }
40 42
41 { 43 {
@@ -68,8 +70,8 @@ describe('Test videos history', function () {
68 }) 70 })
69 71
70 it('Should watch the first and second video', async function () { 72 it('Should watch the first and second video', async function () {
71 await command.wathVideo({ videoId: video2UUID, currentTime: 8 }) 73 await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
72 await command.wathVideo({ videoId: video1UUID, currentTime: 3 }) 74 await command.watchVideo({ videoId: video1UUID, currentTime: 3 })
73 }) 75 })
74 76
75 it('Should return the correct history when listing, searching and getting videos', async function () { 77 it('Should return the correct history when listing, searching and getting videos', async function () {
@@ -122,7 +124,7 @@ describe('Test videos history', function () {
122 124
123 it('Should have these videos when listing my history', async function () { 125 it('Should have these videos when listing my history', async function () {
124 video3WatchedDate = new Date() 126 video3WatchedDate = new Date()
125 await command.wathVideo({ videoId: video3UUID, currentTime: 2 }) 127 await command.watchVideo({ videoId: video3UUID, currentTime: 2 })
126 128
127 const body = await command.list() 129 const body = await command.list()
128 130
@@ -150,7 +152,7 @@ describe('Test videos history', function () {
150 }) 152 })
151 153
152 it('Should clear my history', async function () { 154 it('Should clear my history', async function () {
153 await command.remove({ beforeDate: video3WatchedDate.toISOString() }) 155 await command.removeAll({ beforeDate: video3WatchedDate.toISOString() })
154 }) 156 })
155 157
156 it('Should have my history cleared', async function () { 158 it('Should have my history cleared', async function () {
@@ -166,7 +168,7 @@ describe('Test videos history', function () {
166 videosHistoryEnabled: false 168 videosHistoryEnabled: false
167 }) 169 })
168 170
169 await command.wathVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 }) 171 await command.watchVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
170 }) 172 })
171 173
172 it('Should re-enable videos history', async function () { 174 it('Should re-enable videos history', async function () {
@@ -174,7 +176,7 @@ describe('Test videos history', function () {
174 videosHistoryEnabled: true 176 videosHistoryEnabled: true
175 }) 177 })
176 178
177 await command.wathVideo({ videoId: video1UUID, currentTime: 8 }) 179 await command.watchVideo({ videoId: video1UUID, currentTime: 8 })
178 180
179 const body = await command.list() 181 const body = await command.list()
180 expect(body.total).to.equal(2) 182 expect(body.total).to.equal(2)
@@ -212,6 +214,26 @@ describe('Test videos history', function () {
212 expect(body.total).to.equal(0) 214 expect(body.total).to.equal(0)
213 }) 215 })
214 216
217 it('Should delete a specific history element', async function () {
218 {
219 await command.watchVideo({ videoId: video1UUID, currentTime: 4 })
220 await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
221 }
222
223 {
224 const body = await command.list()
225 expect(body.total).to.equal(2)
226 }
227
228 {
229 await command.removeElement({ videoId: video1Id })
230
231 const body = await command.list()
232 expect(body.total).to.equal(1)
233 expect(body.data[0].uuid).to.equal(video2UUID)
234 }
235 })
236
215 after(async function () { 237 after(async function () {
216 await cleanupTests([ server ]) 238 await cleanupTests([ server ])
217 }) 239 })
diff --git a/shared/server-commands/videos/history-command.ts b/shared/server-commands/videos/history-command.ts
index 13b7150c1..e9dc63462 100644
--- a/shared/server-commands/videos/history-command.ts
+++ b/shared/server-commands/videos/history-command.ts
@@ -3,7 +3,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
3 3
4export class HistoryCommand extends AbstractCommand { 4export class HistoryCommand extends AbstractCommand {
5 5
6 wathVideo (options: OverrideCommandOptions & { 6 watchVideo (options: OverrideCommandOptions & {
7 videoId: number | string 7 videoId: number | string
8 currentTime: number 8 currentTime: number
9 }) { 9 }) {
@@ -40,7 +40,22 @@ export class HistoryCommand extends AbstractCommand {
40 }) 40 })
41 } 41 }
42 42
43 remove (options: OverrideCommandOptions & { 43 removeElement (options: OverrideCommandOptions & {
44 videoId: number
45 }) {
46 const { videoId } = options
47 const path = '/api/v1/users/me/history/videos/' + videoId
48
49 return this.deleteRequest({
50 ...options,
51
52 path,
53 implicitToken: true,
54 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
55 })
56 }
57
58 removeAll (options: OverrideCommandOptions & {
44 beforeDate?: string 59 beforeDate?: string
45 } = {}) { 60 } = {}) {
46 const { beforeDate } = options 61 const { beforeDate } = options
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 5746d3a47..5bf3f13cc 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -1476,6 +1476,23 @@ paths:
1476 schema: 1476 schema:
1477 $ref: '#/components/schemas/VideoListResponse' 1477 $ref: '#/components/schemas/VideoListResponse'
1478 1478
1479 /users/me/history/videos/{videoId}:
1480 delete:
1481 summary: Delete history element
1482 security:
1483 - OAuth2: []
1484 tags:
1485 - My History
1486 parameters:
1487 - name: videoId
1488 in: path
1489 required: true
1490 schema:
1491 $ref: '#/components/schemas/Video/properties/id'
1492 responses:
1493 '204':
1494 description: successful operation
1495
1479 /users/me/history/videos/remove: 1496 /users/me/history/videos/remove:
1480 post: 1497 post:
1481 summary: Clear video history 1498 summary: Clear video history