]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to delete history element
authorChocobozzz <me@florianbigard.com>
Tue, 18 Jan 2022 10:23:41 +0000 (11:23 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 18 Jan 2022 10:23:41 +0000 (11:23 +0100)
client/src/app/+my-library/my-history/my-history.component.html
client/src/app/+my-library/my-history/my-history.component.scss
client/src/app/+my-library/my-history/my-history.component.ts
client/src/app/shared/shared-main/users/user-history.service.ts
server/controllers/api/users/my-history.ts
server/middlewares/validators/user-history.ts
server/models/user/user-video-history.ts
server/tests/api/check-params/videos-history.ts
server/tests/api/videos/videos-history.ts
shared/server-commands/videos/history-command.ts
support/doc/api/openapi.yaml

index 8e564cf9327211e1bde0176366bc91a1c9eca756..14bf01804dbfedff6707c8c917635daddfff5c5b 100644 (file)
@@ -13,9 +13,9 @@
     <label i18n>Track watch history</label>
   </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>
-    Delete history
+    Clear all history
   </button>
 </div>
 
   [enableSelection]="false"
   [disabled]="disabled"
   #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>
index cb85075697be8287dede42a983bf52e670b04ff4..3257b2215379577db6ec3d23bd8f00fbb501f67c 100644 (file)
   @include row-blocks($column-responsive: false);
 }
 
+.action-button {
+  display: flex;
+  align-self: flex-end;
+}
+
 @media screen and (max-width: $small-view) {
   .top-buttons {
     grid-template-columns: auto 1fr auto;
index 95cfaee417d08c260eff84e997de97899fa8d40f..34efe5558322bdab9a010add630c2dcdd41aa169 100644 (file)
@@ -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 message = $localize`Are you sure you want to delete all your videos history?`
 
     const res = await this.confirmService.confirm(message, title)
     if (res !== true) return
 
-    this.userHistoryService.deleteUserVideosHistory()
+    this.userHistoryService.clearAllUserVideosHistory()
         .subscribe({
           next: () => {
             this.notifier.success($localize`Videos history deleted`)
index a4841897dba9e1bdff6bebce7ea3792299a7c90a..e28bcdca943c1dc31679979ad0c4f4f4b3fda6f8 100644 (file)
@@ -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
                .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
                .pipe(
index 2fcb25acf5d3540bf9696f61ed9ee842da500ba5..bc5b40f59ab3446fd1affa7699e312fa264d3ec8 100644 (file)
@@ -9,7 +9,8 @@ import {
   paginationValidator,
   setDefaultPagination,
   userHistoryListValidator,
-  userHistoryRemoveValidator
+  userHistoryRemoveAllValidator,
+  userHistoryRemoveElementValidator
 } from '../../../middlewares'
 import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
 
@@ -23,10 +24,16 @@ myVideosHistoryRouter.get('/me/history/videos',
   asyncMiddleware(listMyVideosHistory)
 )
 
+myVideosHistoryRouter.delete('/me/history/videos/:videoId',
+  authenticate,
+  userHistoryRemoveElementValidator,
+  asyncMiddleware(removeUserHistoryElement)
+)
+
 myVideosHistoryRouter.post('/me/history/videos/remove',
   authenticate,
-  userHistoryRemoveValidator,
-  asyncRetryTransactionMiddleware(removeUserHistory)
+  userHistoryRemoveAllValidator,
+  asyncRetryTransactionMiddleware(removeAllUserHistory)
 )
 
 // ---------------------------------------------------------------------------
@@ -45,7 +52,15 @@ async function listMyVideosHistory (req: express.Request, res: express.Response)
   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 beforeDate = req.body.beforeDate || null
 
index f9be26627848806e8046bb30c1679cee76a4832f..541910be557d0c36c10b0e91491fcc568a814636 100644 (file)
@@ -1,6 +1,6 @@
 import express from 'express'
-import { body, query } from 'express-validator'
-import { exists, isDateValid } from '../../helpers/custom-validators/misc'
+import { body, param, query } from 'express-validator'
+import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc'
 import { logger } from '../../helpers/logger'
 import { areValidationErrors } from './shared'
 
@@ -18,13 +18,26 @@ const userHistoryListValidator = [
   }
 ]
 
-const userHistoryRemoveValidator = [
+const userHistoryRemoveAllValidator = [
   body('beforeDate')
     .optional()
     .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
 
   (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
 
@@ -36,5 +49,6 @@ const userHistoryRemoveValidator = [
 
 export {
   userHistoryListValidator,
-  userHistoryRemoveValidator
+  userHistoryRemoveElementValidator,
+  userHistoryRemoveAllValidator
 }
index 92f4fe7a10587dd29c67d46fdf9285e7bd0bc6ce..f4d0889a104c7bb8df09d80c065cce55024f4c8d 100644 (file)
@@ -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) {
     const query: DestroyOptions = {
       where: {
index 31a0752c7bec2b1c4cd5be7a15219ed6ffe6a825..82f38b7b4c2fafe87621486f7123859ecf8123e9 100644 (file)
@@ -6,6 +6,7 @@ import { HttpStatusCode } from '@shared/models'
 import {
   cleanupTests,
   createSingleServer,
+  makeDeleteRequest,
   makeGetRequest,
   makePostBodyRequest,
   makePutBodyRequest,
@@ -18,6 +19,7 @@ describe('Test videos history API validator', function () {
   const myHistoryRemove = myHistoryPath + '/remove'
   let watchingPath: string
   let server: PeerTubeServer
+  let videoId: number
 
   // ---------------------------------------------------------------
 
@@ -28,8 +30,9 @@ describe('Test videos history API validator', function () {
 
     await setAccessTokensToServers([ server ])
 
-    const { uuid } = await server.videos.upload()
+    const { id, uuid } = await server.videos.upload()
     watchingPath = '/api/v1/videos/' + uuid + '/watching'
+    videoId = id
   })
 
   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 () {
       await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
     })
index 4e5ba13aa4569213f9e588bcf329193a1c407207..8648c97f092a8158ad9cf4449024f795a421e200 100644 (file)
@@ -17,6 +17,7 @@ const expect = chai.expect
 
 describe('Test videos history', function () {
   let server: PeerTubeServer = null
+  let video1Id: number
   let video1UUID: string
   let video2UUID: string
   let video3UUID: string
@@ -34,8 +35,9 @@ describe('Test videos history', function () {
     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
+      video1Id = id
     }
 
     {
@@ -68,8 +70,8 @@ describe('Test videos history', function () {
   })
 
   it('Should watch the first and second video', async function () {
-    await command.wathVideo({ videoId: video2UUID, currentTime: 8 })
-    await command.wathVideo({ videoId: video1UUID, currentTime: 3 })
+    await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
+    await command.watchVideo({ videoId: video1UUID, currentTime: 3 })
   })
 
   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 () {
     video3WatchedDate = new Date()
-    await command.wathVideo({ videoId: video3UUID, currentTime: 2 })
+    await command.watchVideo({ videoId: video3UUID, currentTime: 2 })
 
     const body = await command.list()
 
@@ -150,7 +152,7 @@ describe('Test videos history', 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 () {
@@ -166,7 +168,7 @@ describe('Test videos history', function () {
       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 () {
@@ -174,7 +176,7 @@ describe('Test videos history', function () {
       videosHistoryEnabled: true
     })
 
-    await command.wathVideo({ videoId: video1UUID, currentTime: 8 })
+    await command.watchVideo({ videoId: video1UUID, currentTime: 8 })
 
     const body = await command.list()
     expect(body.total).to.equal(2)
@@ -212,6 +214,26 @@ describe('Test videos history', function () {
     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 () {
     await cleanupTests([ server ])
   })
index 13b7150c1c1d526727b96259a8c1db69035c7834..e9dc634626f39a29da5d5be7c14c6551e355b571 100644 (file)
@@ -3,7 +3,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
 
 export class HistoryCommand extends AbstractCommand {
 
-  wathVideo (options: OverrideCommandOptions & {
+  watchVideo (options: OverrideCommandOptions & {
     videoId: number | string
     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
   } = {}) {
     const { beforeDate } = options
index 5746d3a47f87c6220bc92bc6f104df838c90e13b..5bf3f13cc533dc725b71add01724dbfa7cf658c6 100644 (file)
@@ -1476,6 +1476,23 @@ paths:
               schema:
                 $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:
     post:
       summary: Clear video history