Add like/dislike system for videos
authorChocobozzz <florian.bigard@gmail.com>
Wed, 8 Mar 2017 20:35:43 +0000 (21:35 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Wed, 8 Mar 2017 20:35:43 +0000 (21:35 +0100)
31 files changed:
client/src/app/shared/users/user.service.ts
client/src/app/videos/shared/index.ts
client/src/app/videos/shared/rate-type.type.ts [new file with mode: 0644]
client/src/app/videos/shared/sort-field.type.ts
client/src/app/videos/shared/video.model.ts
client/src/app/videos/shared/video.service.ts
client/src/app/videos/video-watch/video-watch.component.html
client/src/app/videos/video-watch/video-watch.component.scss
client/src/app/videos/video-watch/video-watch.component.ts
server/controllers/api/remote/videos.js
server/controllers/api/users.js
server/controllers/api/videos.js
server/helpers/custom-validators/remote/videos.js
server/helpers/custom-validators/videos.js
server/initializers/constants.js
server/initializers/migrations/0020-video-likes.js [new file with mode: 0644]
server/initializers/migrations/0025-video-dislikes.js [new file with mode: 0644]
server/lib/friends.js
server/lib/request-video-qadu-scheduler.js
server/middlewares/validators/users.js
server/middlewares/validators/videos.js
server/models/request-video-event.js
server/models/user-video-rate.js [new file with mode: 0644]
server/models/video.js
server/tests/api/check-params/users.js
server/tests/api/check-params/videos.js
server/tests/api/multiple-pods.js
server/tests/api/single-pod.js
server/tests/api/users.js
server/tests/utils/users.js
server/tests/utils/videos.js

index 4cf100f0dd157f20db082f7801575b0f52973e43..865e04d485693795bcf287512f3cd2f45dc8c67b 100644 (file)
@@ -8,7 +8,7 @@ import { RestExtractor } from '../rest';
 
 @Injectable()
 export class UserService {
-  private static BASE_USERS_URL = '/api/v1/users/';
+  static BASE_USERS_URL = '/api/v1/users/';
 
   constructor(
     private authHttp: AuthHttp,
index 67d16ead155876bf31f47d54dc653fc3faa3cab6..beaa528c01c9e851fe58127933ca12561a13716d 100644 (file)
@@ -1,4 +1,5 @@
 export * from './loader';
 export * from './sort-field.type';
+export * from './rate-type.type';
 export * from './video.model';
 export * from './video.service';
diff --git a/client/src/app/videos/shared/rate-type.type.ts b/client/src/app/videos/shared/rate-type.type.ts
new file mode 100644 (file)
index 0000000..88034d1
--- /dev/null
@@ -0,0 +1 @@
+export type RateType = 'like' | 'dislike';
index 74908e344ec22bbf5f9a7992115af86758c5c902..7bda3112a182aa9e4fb22350a65f7f1ab4f6f909 100644 (file)
@@ -1,3 +1,3 @@
-export type SortField = "name" | "-name"
-                      | "duration" | "-duration"
-                      | "createdAt" | "-createdAt";
+export type SortField = 'name' | '-name'
+                      | 'duration' | '-duration'
+                      | 'createdAt' | '-createdAt';
index 8e676708b2908373a10eb6536a11f8f42beff61d..3eef936eba59ac6f8f00e6c9e4907a3c15b855c8 100644 (file)
@@ -12,6 +12,8 @@ export class Video {
   tags: string[];
   thumbnailPath: string;
   views: number;
+  likes: number;
+  dislikes: number;
 
   private static createByString(author: string, podHost: string) {
     return author + '@' + podHost;
@@ -38,7 +40,9 @@ export class Video {
     podHost: string,
     tags: string[],
     thumbnailPath: string,
-    views: number
+    views: number,
+    likes: number,
+    dislikes: number,
   }) {
     this.author  = hash.author;
     this.createdAt = new Date(hash.createdAt);
@@ -52,6 +56,8 @@ export class Video {
     this.tags = hash.tags;
     this.thumbnailPath = hash.thumbnailPath;
     this.views = hash.views;
+    this.likes = hash.likes;
+    this.dislikes = hash.dislikes;
 
     this.by = Video.createByString(hash.author, hash.podHost);
   }
index 7094d9a34210e4e7498e426adcec6b240379de1c..8bb5a293309aa5e193c1e2325f4e9804c0adfb8e 100644 (file)
@@ -6,8 +6,16 @@ import 'rxjs/add/operator/map';
 
 import { Search } from '../../shared';
 import { SortField } from './sort-field.type';
+import { RateType } from './rate-type.type';
 import { AuthService } from '../../core';
-import { AuthHttp, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
+import {
+  AuthHttp,
+  RestExtractor,
+  RestPagination,
+  RestService,
+  ResultList,
+  UserService
+} from '../../shared';
 import { Video } from './video.model';
 
 @Injectable()
@@ -56,14 +64,41 @@ export class VideoService {
   }
 
   reportVideo(id: string, reason: string) {
+    const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
     const body = {
       reason
     };
-    const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
 
     return this.authHttp.post(url, body)
-                    .map(this.restExtractor.extractDataBool)
-                    .catch((res) => this.restExtractor.handleError(res));
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+
+  setVideoLike(id: string) {
+    return this.setVideoRate(id, 'like');
+  }
+
+  setVideoDislike(id: string) {
+    return this.setVideoRate(id, 'dislike');
+  }
+
+  getUserVideoRating(id: string) {
+    const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating';
+
+    return this.authHttp.get(url)
+                        .map(this.restExtractor.extractDataGet)
+                        .catch((res) => this.restExtractor.handleError(res));
+  }
+
+  private setVideoRate(id: string, rateType: RateType) {
+    const url = VideoService.BASE_VIDEO_URL + id + '/rate';
+    const body = {
+      rating: rateType
+    };
+
+    return this.authHttp.put(url, body)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
   private extractVideos(result: ResultList) {
index 24d741ff9af2739f9d59c3f040f423d11dd9ab89..67094359e2ed9a309c505bb78b0d2296c9339be3 100644 (file)
@@ -32,7 +32,7 @@
 
 <div *ngIf="video !== null" id="video-info">
   <div class="row" id="video-name-actions">
-    <div class="col-md-8">
+    <div class="col-md-6">
       <div class="row">
         <div id="video-name" class="col-md-12">
           {{ video.name }}
       </div>
     </div>
 
-    <div id="video-actions" class="col-md-4 text-right">
+    <div id="video-actions" class="col-md-6 text-right">
+      <div id="rates">
+        <button
+          id="likes" class="btn btn-default"
+          [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'like' }" (click)="setLike()"
+        >
+          <span class="glyphicon glyphicon-thumbs-up"></span> {{ video.likes }}
+        </button>
+
+        <button
+          id="dislikes" class="btn btn-default"
+          [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'dislike' }" (click)="setDislike()"
+        >
+          <span class=" glyphicon glyphicon-thumbs-down"></span> {{ video.dislikes }}
+        </button>
+      </div>
+
       <button id="share" class="btn btn-default" (click)="showShareModal()">
         <span class="glyphicon glyphicon-share"></span> Share
       </button>
index 0b8af52ced5197332e68c3a9381e92eda25ba747..5f322a19493bf943e4d688bcbe961d67f748f25e 100644 (file)
       top: 2px;
     }
 
+    #rates {
+      display: inline-block;
+      margin-right: 20px;
+
+      // Remove focus style
+      .btn:focus {
+        outline: 0;
+      }
+
+      .activated-btn {
+        color: #333;
+        background-color: #e6e6e6;
+        border-color: #8c8c8c;
+      }
+
+      .not-interactive-btn {
+        cursor: default;
+
+        &:hover, &:focus, &:active {
+          color: #333;
+          background-color: #fff;
+          border-color: #ccc;
+          box-shadow: none;
+          outline: 0;
+        }
+      }
+    }
+
     #share, #more {
       font-weight: bold;
       opacity: 0.85;
index d1abc81bc6a3ee759d682bee8802403368b62afb..ed6b301024d3c00b18143a906952ea1ffa06b8a5 100644 (file)
@@ -10,7 +10,7 @@ import { AuthService } from '../../core';
 import { VideoMagnetComponent } from './video-magnet.component';
 import { VideoShareComponent } from './video-share.component';
 import { VideoReportComponent } from './video-report.component';
-import { Video, VideoService } from '../shared';
+import { RateType, Video, VideoService } from '../shared';
 import { WebTorrentService } from './webtorrent.service';
 
 @Component({
@@ -33,6 +33,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   player: VideoJSPlayer;
   playerElement: Element;
   uploadSpeed: number;
+  userRating: RateType = null;
   video: Video = null;
   videoNotFound = false;
 
@@ -61,6 +62,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           this.video = video;
           this.setOpenGraphTags();
           this.loadVideo();
+          this.checkUserRating();
         },
         error => {
           this.videoNotFound = true;
@@ -136,6 +138,40 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     });
   }
 
+  setLike() {
+    if (this.isUserLoggedIn() === false) return;
+    // Already liked this video
+    if (this.userRating === 'like') return;
+
+    this.videoService.setVideoLike(this.video.id)
+                     .subscribe(
+                      () => {
+                        // Update the video like attribute
+                        this.updateVideoRating(this.userRating, 'like');
+                        this.userRating = 'like';
+                      },
+
+                      err => this.notificationsService.error('Error', err.text)
+                     );
+  }
+
+  setDislike() {
+    if (this.isUserLoggedIn() === false) return;
+    // Already disliked this video
+    if (this.userRating === 'dislike') return;
+
+    this.videoService.setVideoDislike(this.video.id)
+                     .subscribe(
+                      () => {
+                        // Update the video dislike attribute
+                        this.updateVideoRating(this.userRating, 'dislike');
+                        this.userRating = 'dislike';
+                      },
+
+                      err => this.notificationsService.error('Error', err.text)
+                     );
+  }
+
   showReportModal(event: Event) {
     event.preventDefault();
     this.videoReportModal.show();
@@ -154,6 +190,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.authService.isLoggedIn();
   }
 
+  private checkUserRating() {
+    // Unlogged users do not have ratings
+    if (this.isUserLoggedIn() === false) return;
+
+    this.videoService.getUserVideoRating(this.video.id)
+                     .subscribe(
+                       ratingObject => {
+                         if (ratingObject) {
+                           this.userRating = ratingObject.rating;
+                         }
+                       },
+
+                       err => this.notificationsService.error('Error', err.text)
+                      );
+  }
+
+  private updateVideoRating(oldRating: RateType, newRating: RateType) {
+    let likesToIncrement = 0;
+    let dislikesToIncrement = 0;
+
+    if (oldRating) {
+      if (oldRating === 'like') likesToIncrement--;
+      if (oldRating === 'dislike') dislikesToIncrement--;
+    }
+
+    if (newRating === 'like') likesToIncrement++;
+    if (newRating === 'dislike') dislikesToIncrement++;
+
+    this.video.likes += likesToIncrement;
+    this.video.dislikes += dislikesToIncrement;
+  }
+
   private loadTooLong() {
     this.error = true;
     console.error('The video load seems to be abnormally long.');
index 39c9579c1a21fd3c5346b9b6f3c4b5a7184b8656..98891c99eb2d7f2e987c83a8a823d98f5ebbddd6 100644 (file)
@@ -11,6 +11,7 @@ const secureMiddleware = middlewares.secure
 const videosValidators = middlewares.validators.remote.videos
 const signatureValidators = middlewares.validators.remote.signature
 const logger = require('../../../helpers/logger')
+const friends = require('../../../lib/friends')
 const databaseUtils = require('../../../helpers/database-utils')
 
 const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
@@ -129,18 +130,22 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
       const options = { transaction: t }
 
       let columnToUpdate
+      let qaduType
 
       switch (eventData.eventType) {
         case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
           columnToUpdate = 'views'
+          qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
           break
 
         case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
           columnToUpdate = 'likes'
+          qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES
           break
 
         case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
           columnToUpdate = 'dislikes'
+          qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
           break
 
         default:
@@ -151,6 +156,19 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
       query[columnToUpdate] = eventData.count
 
       videoInstance.increment(query, options).asCallback(function (err) {
+        return callback(err, t, videoInstance, qaduType)
+      })
+    },
+
+    function sendQaduToFriends (t, videoInstance, qaduType, callback) {
+      const qadusParams = [
+        {
+          videoId: videoInstance.id,
+          type: qaduType
+        }
+      ]
+
+      friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
         return callback(err, t)
       })
     },
@@ -159,7 +177,6 @@ function processVideosEvents (eventData, fromPod, finalCallback) {
 
   ], function (err, t) {
     if (err) {
-      console.log(err)
       logger.debug('Cannot process a video event.', { error: err })
       return databaseUtils.rollbackTransaction(err, t, finalCallback)
     }
@@ -278,7 +295,10 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
         duration: videoToCreateData.duration,
         createdAt: videoToCreateData.createdAt,
         // FIXME: updatedAt does not seems to be considered by Sequelize
-        updatedAt: videoToCreateData.updatedAt
+        updatedAt: videoToCreateData.updatedAt,
+        views: videoToCreateData.views,
+        likes: videoToCreateData.likes,
+        dislikes: videoToCreateData.dislikes
       }
 
       const video = db.Video.build(videoData)
@@ -372,6 +392,9 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
       videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
       videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
       videoInstance.set('extname', videoAttributesToUpdate.extname)
+      videoInstance.set('views', videoAttributesToUpdate.views)
+      videoInstance.set('likes', videoAttributesToUpdate.likes)
+      videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
 
       videoInstance.save(options).asCallback(function (err) {
         return callback(err, t, videoInstance, tagInstances)
index 324c99b4cce8ab9d3730ed36d049e160149f50eb..f854b30826697269d12520bb7b4d1da73dbfcb0a 100644 (file)
@@ -18,7 +18,16 @@ const validatorsUsers = middlewares.validators.users
 
 const router = express.Router()
 
-router.get('/me', oAuth.authenticate, getUserInformation)
+router.get('/me',
+  oAuth.authenticate,
+  getUserInformation
+)
+
+router.get('/me/videos/:videoId/rating',
+  oAuth.authenticate,
+  validatorsUsers.usersVideoRating,
+  getUserVideoRating
+)
 
 router.get('/',
   validatorsPagination.pagination,
@@ -80,6 +89,22 @@ function getUserInformation (req, res, next) {
   })
 }
 
+function getUserVideoRating (req, res, next) {
+  const videoId = req.params.videoId
+  const userId = res.locals.oauth.token.User.id
+
+  db.UserVideoRate.load(userId, videoId, function (err, ratingObj) {
+    if (err) return next(err)
+
+    const rating = ratingObj ? ratingObj.type : 'none'
+
+    res.json({
+      videoId,
+      rating
+    })
+  })
+}
+
 function listUsers (req, res, next) {
   db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
     if (err) return next(err)
index 5a67d1121fcb9c3f2f7df73ec548cb422b5e9f42..9acdb8fd26ff7e9f48ab9768937eb5549211af40 100644 (file)
@@ -60,6 +60,12 @@ router.post('/:id/abuse',
   reportVideoAbuseRetryWrapper
 )
 
+router.put('/:id/rate',
+  oAuth.authenticate,
+  validatorsVideos.videoRate,
+  rateVideoRetryWrapper
+)
+
 router.get('/',
   validatorsPagination.pagination,
   validatorsSort.videosSort,
@@ -104,6 +110,147 @@ module.exports = router
 
 // ---------------------------------------------------------------------------
 
+function rateVideoRetryWrapper (req, res, next) {
+  const options = {
+    arguments: [ req, res ],
+    errorMessage: 'Cannot update the user video rate.'
+  }
+
+  databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
+    if (err) return next(err)
+
+    return res.type('json').status(204).end()
+  })
+}
+
+function rateVideo (req, res, finalCallback) {
+  const rateType = req.body.rating
+  const videoInstance = res.locals.video
+  const userInstance = res.locals.oauth.token.User
+
+  waterfall([
+    databaseUtils.startSerializableTransaction,
+
+    function findPreviousRate (t, callback) {
+      db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
+        return callback(err, t, previousRate)
+      })
+    },
+
+    function insertUserRateIntoDB (t, previousRate, callback) {
+      const options = { transaction: t }
+
+      let likesToIncrement = 0
+      let dislikesToIncrement = 0
+
+      if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
+      else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
+
+      // There was a previous rate, update it
+      if (previousRate) {
+        // We will remove the previous rate, so we will need to remove it from the video attribute
+        if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
+        else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
+
+        previousRate.type = rateType
+
+        previousRate.save(options).asCallback(function (err) {
+          return callback(err, t, likesToIncrement, dislikesToIncrement)
+        })
+      } else { // There was not a previous rate, insert a new one
+        const query = {
+          userId: userInstance.id,
+          videoId: videoInstance.id,
+          type: rateType
+        }
+
+        db.UserVideoRate.create(query, options).asCallback(function (err) {
+          return callback(err, t, likesToIncrement, dislikesToIncrement)
+        })
+      }
+    },
+
+    function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
+      const options = { transaction: t }
+      const incrementQuery = {
+        likes: likesToIncrement,
+        dislikes: dislikesToIncrement
+      }
+
+      // Even if we do not own the video we increment the attributes
+      // It is usefull for the user to have a feedback
+      videoInstance.increment(incrementQuery, options).asCallback(function (err) {
+        return callback(err, t, likesToIncrement, dislikesToIncrement)
+      })
+    },
+
+    function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
+      // No need for an event type, we own the video
+      if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
+
+      const eventsParams = []
+
+      if (likesToIncrement !== 0) {
+        eventsParams.push({
+          videoId: videoInstance.id,
+          type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
+          count: likesToIncrement
+        })
+      }
+
+      if (dislikesToIncrement !== 0) {
+        eventsParams.push({
+          videoId: videoInstance.id,
+          type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
+          count: dislikesToIncrement
+        })
+      }
+
+      friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
+        return callback(err, t, likesToIncrement, dislikesToIncrement)
+      })
+    },
+
+    function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
+      // We do not own the video, there is no need to send a quick and dirty update to friends
+      // Our rate was already sent by the addEvent function
+      if (videoInstance.isOwned() === false) return callback(null, t)
+
+      const qadusParams = []
+
+      if (likesToIncrement !== 0) {
+        qadusParams.push({
+          videoId: videoInstance.id,
+          type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
+        })
+      }
+
+      if (dislikesToIncrement !== 0) {
+        qadusParams.push({
+          videoId: videoInstance.id,
+          type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
+        })
+      }
+
+      friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
+        return callback(err, t)
+      })
+    },
+
+    databaseUtils.commitTransaction
+
+  ], function (err, t) {
+    if (err) {
+      // This is just a debug because we will retry the insert
+      logger.debug('Cannot add the user video rate.', { error: err })
+      return databaseUtils.rollbackTransaction(err, t, finalCallback)
+    }
+
+    logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
+    return finalCallback(null)
+  })
+}
+
 // Wrapper to video add that retry the function if there is a database error
 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
 function addVideoRetryWrapper (req, res, next) {
@@ -155,8 +302,7 @@ function addVideo (req, res, videoFile, finalCallback) {
         extname: path.extname(videoFile.filename),
         description: videoInfos.description,
         duration: videoFile.duration,
-        authorId: author.id,
-        views: videoInfos.views
+        authorId: author.id
       }
 
       const video = db.Video.build(videoData)
@@ -332,11 +478,19 @@ function getVideo (req, res, next) {
 
       // FIXME: make a real view system
       // For example, only add a view when a user watch a video during 30s etc
-      friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS)
+      const qaduParams = {
+        videoId: videoInstance.id,
+        type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
+      }
+      friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
     })
   } else {
     // Just send the event to our friends
-    friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS)
+    const eventParams = {
+      videoId: videoInstance.id,
+      type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
+    }
+    friends.addEventToRemoteVideo(eventParams)
   }
 
   // Do not wait the view system
index ba2d0bb93bde1c1c3bc53e648dfdb01cd38f185a..e1636e0e626b9618a8ca32816b984c6138dc8f86 100644 (file)
@@ -92,7 +92,9 @@ function isCommonVideoAttributesValid (video) {
          videosValidators.isVideoTagsValid(video.tags) &&
          videosValidators.isVideoRemoteIdValid(video.remoteId) &&
          videosValidators.isVideoExtnameValid(video.extname) &&
-         videosValidators.isVideoViewsValid(video.views)
+         videosValidators.isVideoViewsValid(video.views) &&
+         videosValidators.isVideoLikesValid(video.likes) &&
+         videosValidators.isVideoDislikesValid(video.dislikes)
 }
 
 function isRequestTypeAddValid (value) {
index c5a1f3cb5684d24af5932671497c2e528b9d10ae..648c7540bf33a7b1582f0a6318f192fe12e969cf 100644 (file)
@@ -1,6 +1,7 @@
 'use strict'
 
 const validator = require('express-validator').validator
+const values = require('lodash/values')
 
 const constants = require('../../initializers/constants')
 const usersValidators = require('./users')
@@ -26,6 +27,7 @@ const videosValidators = {
   isVideoFile,
   isVideoViewsValid,
   isVideoLikesValid,
+  isVideoRatingTypeValid,
   isVideoDislikesValid,
   isVideoEventCountValid
 }
@@ -103,6 +105,10 @@ function isVideoEventCountValid (value) {
   return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT)
 }
 
+function isVideoRatingTypeValid (value) {
+  return values(constants.VIDEO_RATE_TYPES).indexOf(value) !== -1
+}
+
 function isVideoFile (value, files) {
   // Should have files
   if (!files) return false
index 2d5bb84cc66bc417c019b2e79ca94faccc271d9e..16a2dd320767b872b54ff6b11e4b9b07ff569e8d 100644 (file)
@@ -5,7 +5,7 @@ const path = require('path')
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 15
+const LAST_MIGRATION_VERSION = 25
 
 // ---------------------------------------------------------------------------
 
@@ -95,6 +95,11 @@ const CONSTRAINTS_FIELDS = {
   }
 }
 
+const VIDEO_RATE_TYPES = {
+  LIKE: 'like',
+  DISLIKE: 'dislike'
+}
+
 // ---------------------------------------------------------------------------
 
 // Score a pod has when we create it as a friend
@@ -249,7 +254,8 @@ module.exports = {
   STATIC_MAX_AGE,
   STATIC_PATHS,
   THUMBNAILS_SIZE,
-  USER_ROLES
+  USER_ROLES,
+  VIDEO_RATE_TYPES
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/initializers/migrations/0020-video-likes.js b/server/initializers/migrations/0020-video-likes.js
new file mode 100644 (file)
index 0000000..6db62cb
--- /dev/null
@@ -0,0 +1,19 @@
+'use strict'
+
+// utils = { transaction, queryInterface, sequelize, Sequelize }
+exports.up = function (utils, finalCallback) {
+  const q = utils.queryInterface
+  const Sequelize = utils.Sequelize
+
+  const data = {
+    type: Sequelize.INTEGER,
+    allowNull: false,
+    defaultValue: 0
+  }
+
+  q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback)
+}
+
+exports.down = function (options, callback) {
+  throw new Error('Not implemented.')
+}
diff --git a/server/initializers/migrations/0025-video-dislikes.js b/server/initializers/migrations/0025-video-dislikes.js
new file mode 100644 (file)
index 0000000..40d2e73
--- /dev/null
@@ -0,0 +1,19 @@
+'use strict'
+
+// utils = { transaction, queryInterface, sequelize, Sequelize }
+exports.up = function (utils, finalCallback) {
+  const q = utils.queryInterface
+  const Sequelize = utils.Sequelize
+
+  const data = {
+    type: Sequelize.INTEGER,
+    allowNull: false,
+    defaultValue: 0
+  }
+
+  q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback)
+}
+
+exports.down = function (options, callback) {
+  throw new Error('Not implemented.')
+}
index 7bd087d8cfdb630f3371183e880eed40c6d39b70..23accfa453168ff72dda8efbf884a41b35f3bea3 100644 (file)
@@ -3,6 +3,7 @@
 const each = require('async/each')
 const eachLimit = require('async/eachLimit')
 const eachSeries = require('async/eachSeries')
+const series = require('async/series')
 const request = require('request')
 const waterfall = require('async/waterfall')
 
@@ -28,7 +29,9 @@ const friends = {
   updateVideoToFriends,
   reportAbuseVideoToFriend,
   quickAndDirtyUpdateVideoToFriends,
+  quickAndDirtyUpdatesVideoToFriends,
   addEventToRemoteVideo,
+  addEventsToRemoteVideo,
   hasFriends,
   makeFriends,
   quitFriends,
@@ -84,24 +87,52 @@ function reportAbuseVideoToFriend (reportData, video) {
   createRequest(options)
 }
 
-function quickAndDirtyUpdateVideoToFriends (videoId, type, transaction, callback) {
+function quickAndDirtyUpdateVideoToFriends (qaduParams, transaction, callback) {
   const options = {
-    videoId,
-    type,
+    videoId: qaduParams.videoId,
+    type: qaduParams.type,
     transaction
   }
   return createVideoQaduRequest(options, callback)
 }
 
-function addEventToRemoteVideo (videoId, type, transaction, callback) {
+function quickAndDirtyUpdatesVideoToFriends (qadusParams, transaction, finalCallback) {
+  const tasks = []
+
+  qadusParams.forEach(function (qaduParams) {
+    const fun = function (callback) {
+      quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback)
+    }
+
+    tasks.push(fun)
+  })
+
+  series(tasks, finalCallback)
+}
+
+function addEventToRemoteVideo (eventParams, transaction, callback) {
   const options = {
-    videoId,
-    type,
+    videoId: eventParams.videoId,
+    type: eventParams.type,
     transaction
   }
   createVideoEventRequest(options, callback)
 }
 
+function addEventsToRemoteVideo (eventsParams, transaction, finalCallback) {
+  const tasks = []
+
+  eventsParams.forEach(function (eventParams) {
+    const fun = function (callback) {
+      addEventToRemoteVideo(eventParams, transaction, callback)
+    }
+
+    tasks.push(fun)
+  })
+
+  series(tasks, finalCallback)
+}
+
 function hasFriends (callback) {
   db.Pod.countAll(function (err, count) {
     if (err) return callback(err)
index ac50cfc111e7495f50bfe0585996146a8b9e85ad..a85d35160a7a36ab75a3426900881f6b98c0f3d1 100644 (file)
@@ -44,14 +44,17 @@ module.exports = class RequestVideoQaduScheduler extends BaseRequestScheduler {
           }
         }
 
-        const videoData = {}
+        // Maybe another attribute was filled for this video
+        let videoData = requestsToMakeGrouped[hashKey].videos[video.id]
+        if (!videoData) videoData = {}
+
         switch (request.type) {
           case constants.REQUEST_VIDEO_QADU_TYPES.LIKES:
             videoData.likes = video.likes
             break
 
           case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES:
-            videoData.likes = video.dislikes
+            videoData.dislikes = video.dislikes
             break
 
           case constants.REQUEST_VIDEO_QADU_TYPES.VIEWS:
index 3089370ffe40ad963e139ef28ab0e155821f3db4..ce83fc0746b88f9f4e5b6d6ae6531ee7c8a4dba1 100644 (file)
@@ -7,7 +7,8 @@ const logger = require('../../helpers/logger')
 const validatorsUsers = {
   usersAdd,
   usersRemove,
-  usersUpdate
+  usersUpdate,
+  usersVideoRating
 }
 
 function usersAdd (req, res, next) {
@@ -62,6 +63,25 @@ function usersUpdate (req, res, next) {
   checkErrors(req, res, next)
 }
 
+function usersVideoRating (req, res, next) {
+  req.checkParams('videoId', 'Should have a valid video id').notEmpty().isUUID(4)
+
+  logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
+
+  checkErrors(req, res, function () {
+    db.Video.load(req.params.videoId, function (err, video) {
+      if (err) {
+        logger.error('Error in user request validator.', { error: err })
+        return res.sendStatus(500)
+      }
+
+      if (!video) return res.status(404).send('Video not found')
+
+      next()
+    })
+  })
+}
+
 // ---------------------------------------------------------------------------
 
 module.exports = validatorsUsers
index 5c3f3ecf3ebdefc61b6d1b2bf26b6507f96369fa..7dc79c56fe1384ab811e96033927cd985a6d488c 100644 (file)
@@ -13,7 +13,9 @@ const validatorsVideos = {
   videosRemove,
   videosSearch,
 
-  videoAbuseReport
+  videoAbuseReport,
+
+  videoRate
 }
 
 function videosAdd (req, res, next) {
@@ -119,6 +121,17 @@ function videoAbuseReport (req, res, next) {
   })
 }
 
+function videoRate (req, res, next) {
+  req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
+  req.checkBody('rating', 'Should have a valid rate type').isVideoRatingTypeValid()
+
+  logger.debug('Checking videoRate parameters', { parameters: req.body })
+
+  checkErrors(req, res, function () {
+    checkVideoExists(req.params.id, res, next)
+  })
+}
+
 // ---------------------------------------------------------------------------
 
 module.exports = validatorsVideos
index ef3ebcb3a1aae897f335ce910205af6f5961bb0f..9ebeaec904fbf57ac7e30b7c25653543918dc6ef 100644 (file)
@@ -83,6 +83,9 @@ function listWithLimitAndRandom (limitPods, limitRequestsPerPod, callback) {
     if (podIds.length === 0) return callback(null, [])
 
     const query = {
+      order: [
+        [ 'id', 'ASC' ]
+      ],
       include: [
         {
           model: self.sequelize.models.Video,
diff --git a/server/models/user-video-rate.js b/server/models/user-video-rate.js
new file mode 100644 (file)
index 0000000..84007d7
--- /dev/null
@@ -0,0 +1,77 @@
+'use strict'
+
+/*
+  User rates per video.
+
+*/
+
+const values = require('lodash/values')
+
+const constants = require('../initializers/constants')
+
+// ---------------------------------------------------------------------------
+
+module.exports = function (sequelize, DataTypes) {
+  const UserVideoRate = sequelize.define('UserVideoRate',
+    {
+      type: {
+        type: DataTypes.ENUM(values(constants.VIDEO_RATE_TYPES)),
+        allowNull: false
+      }
+    },
+    {
+      indexes: [
+        {
+          fields: [ 'videoId', 'userId', 'type' ],
+          unique: true
+        }
+      ],
+      classMethods: {
+        associate,
+
+        load
+      }
+    }
+  )
+
+  return UserVideoRate
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+  this.belongsTo(models.Video, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'CASCADE'
+  })
+
+  this.belongsTo(models.User, {
+    foreignKey: {
+      name: 'userId',
+      allowNull: false
+    },
+    onDelete: 'CASCADE'
+  })
+}
+
+function load (userId, videoId, transaction, callback) {
+  if (!callback) {
+    callback = transaction
+    transaction = null
+  }
+
+  const query = {
+    where: {
+      userId,
+      videoId
+    }
+  }
+
+  const options = {}
+  if (transaction) options.transaction = transaction
+
+  return this.findOne(query, options).asCallback(callback)
+}
index fb46aca869706a05a18e13a88d1700991ccf3761..182555c85a41f4794393f1b1dc97a184d7923ebb 100644 (file)
@@ -89,6 +89,24 @@ module.exports = function (sequelize, DataTypes) {
           min: 0,
           isInt: true
         }
+      },
+      likes: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        defaultValue: 0,
+        validate: {
+          min: 0,
+          isInt: true
+        }
+      },
+      dislikes: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        defaultValue: 0,
+        validate: {
+          min: 0,
+          isInt: true
+        }
       }
     },
     {
@@ -113,6 +131,9 @@ module.exports = function (sequelize, DataTypes) {
         },
         {
           fields: [ 'views' ]
+        },
+        {
+          fields: [ 'likes' ]
         }
       ],
       classMethods: {
@@ -349,6 +370,8 @@ function toFormatedJSON () {
     author: this.Author.name,
     duration: this.duration,
     views: this.views,
+    likes: this.likes,
+    dislikes: this.dislikes,
     tags: map(this.Tags, 'name'),
     thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
     createdAt: this.createdAt,
@@ -381,7 +404,9 @@ function toAddRemoteJSON (callback) {
       createdAt: self.createdAt,
       updatedAt: self.updatedAt,
       extname: self.extname,
-      views: self.views
+      views: self.views,
+      likes: self.likes,
+      dislikes: self.dislikes
     }
 
     return callback(null, remoteVideo)
@@ -400,7 +425,9 @@ function toUpdateRemoteJSON (callback) {
     createdAt: this.createdAt,
     updatedAt: this.updatedAt,
     extname: this.extname,
-    views: this.views
+    views: this.views,
+    likes: this.likes,
+    dislikes: this.dislikes
   }
 
   return json
index 6edb546602d46c61b506fcc2a2ad1be0b0b0619c..11e2bada4da357918cfe171d139842811d4eeb21 100644 (file)
@@ -9,11 +9,13 @@ const loginUtils = require('../../utils/login')
 const requestsUtils = require('../../utils/requests')
 const serversUtils = require('../../utils/servers')
 const usersUtils = require('../../utils/users')
+const videosUtils = require('../../utils/videos')
 
 describe('Test users API validators', function () {
   const path = '/api/v1/users/'
   let userId = null
   let rootId = null
+  let videoId = null
   let server = null
   let userAccessToken = null
 
@@ -47,6 +49,23 @@ describe('Test users API validators', function () {
 
         usersUtils.createUser(server.url, server.accessToken, username, password, next)
       },
+      function (next) {
+        const name = 'my super name for pod'
+        const description = 'my super description for pod'
+        const tags = [ 'tag' ]
+        const file = 'video_short2.webm'
+        videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, next)
+      },
+      function (next) {
+        videosUtils.getVideosList(server.url, function (err, res) {
+          if (err) throw err
+
+          const videos = res.body.data
+          videoId = videos[0].id
+
+          next()
+        })
+      },
       function (next) {
         const user = {
           username: 'user1',
@@ -289,6 +308,63 @@ describe('Test users API validators', function () {
     })
   })
 
+  describe('When getting my video rating', function () {
+    it('Should fail with a non authenticated user', function (done) {
+      request(server.url)
+        .get(path + 'me/videos/' + videoId + '/rating')
+        .set('Authorization', 'Bearer faketoken')
+        .set('Accept', 'application/json')
+        .expect(401, done)
+    })
+
+    it('Should fail with an incorrect video uuid', function (done) {
+      request(server.url)
+        .get(path + 'me/videos/blabla/rating')
+        .set('Authorization', 'Bearer ' + userAccessToken)
+        .set('Accept', 'application/json')
+        .expect(400, done)
+    })
+
+    it('Should fail with an unknown video', function (done) {
+      request(server.url)
+        .get(path + 'me/videos/4da6fde3-88f7-4d16-b119-108df5630b06/rating')
+        .set('Authorization', 'Bearer ' + userAccessToken)
+        .set('Accept', 'application/json')
+        .expect(404, done)
+    })
+
+    it('Should success with the correct parameters', function (done) {
+      request(server.url)
+        .get(path + 'me/videos/' + videoId + '/rating')
+        .set('Authorization', 'Bearer ' + userAccessToken)
+        .set('Accept', 'application/json')
+        .expect(200, done)
+    })
+  })
+
+  describe('When removing an user', function () {
+    it('Should fail with an incorrect id', function (done) {
+      request(server.url)
+        .delete(path + 'bla-bla')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400, done)
+    })
+
+    it('Should fail with the root user', function (done) {
+      request(server.url)
+        .delete(path + rootId)
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400, done)
+    })
+
+    it('Should return 404 with a non existing id', function (done) {
+      request(server.url)
+        .delete(path + '45')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(404, done)
+    })
+  })
+
   describe('When removing an user', function () {
     it('Should fail with an incorrect id', function (done) {
       request(server.url)
index f8549a95b3c6e7eb8e97cb073421a6ad7ed6753b..0f5f40b8e4efd6d77132eb1a6eece20534954d71 100644 (file)
@@ -420,6 +420,48 @@ describe('Test videos API validator', function () {
     it('Should succeed with the correct parameters')
   })
 
+  describe('When rating a video', function () {
+    let videoId
+
+    before(function (done) {
+      videosUtils.getVideosList(server.url, function (err, res) {
+        if (err) throw err
+
+        videoId = res.body.data[0].id
+
+        return done()
+      })
+    })
+
+    it('Should fail without a valid uuid', function (done) {
+      const data = {
+        rating: 'like'
+      }
+      requestsUtils.makePutBodyRequest(server.url, path + 'blabla/rate', server.accessToken, data, done)
+    })
+
+    it('Should fail with an unknown id', function (done) {
+      const data = {
+        rating: 'like'
+      }
+      requestsUtils.makePutBodyRequest(server.url, path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', server.accessToken, data, done, 404)
+    })
+
+    it('Should fail with a wrong rating', function (done) {
+      const data = {
+        rating: 'likes'
+      }
+      requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done)
+    })
+
+    it('Should succeed with the correct parameters', function (done) {
+      const data = {
+        rating: 'like'
+      }
+      requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done, 204)
+    })
+  })
+
   describe('When removing a video', function () {
     it('Should have 404 with nothing', function (done) {
       request(server.url)
index e02b6180b48c153fd4013b4a19062d4d6ca3004d..552f10c6fe75356a57be38d8d1e0cb6594e80df5 100644 (file)
@@ -4,6 +4,7 @@
 
 const chai = require('chai')
 const each = require('async/each')
+const eachSeries = require('async/eachSeries')
 const expect = chai.expect
 const parallel = require('async/parallel')
 const series = require('async/series')
@@ -378,7 +379,7 @@ describe('Test multiple pods', function () {
     })
   })
 
-  describe('Should update video views', function () {
+  describe('Should update video views, likes and dislikes', function () {
     let localVideosPod3 = []
     let remoteVideosPod1 = []
     let remoteVideosPod2 = []
@@ -419,7 +420,7 @@ describe('Test multiple pods', function () {
       ], done)
     })
 
-    it('Should views multiple videos on owned servers', function (done) {
+    it('Should view multiple videos on owned servers', function (done) {
       this.timeout(30000)
 
       parallel([
@@ -440,18 +441,18 @@ describe('Test multiple pods', function () {
         },
 
         function (callback) {
-          setTimeout(done, 22000)
+          setTimeout(callback, 22000)
         }
       ], function (err) {
         if (err) throw err
 
-        each(servers, function (server, callback) {
+        eachSeries(servers, function (server, callback) {
           videosUtils.getVideosList(server.url, function (err, res) {
             if (err) throw err
 
             const videos = res.body.data
-            expect(videos.find(video => video.views === 3)).to.be.exist
-            expect(videos.find(video => video.views === 1)).to.be.exist
+            expect(videos.find(video => video.views === 3)).to.exist
+            expect(videos.find(video => video.views === 1)).to.exist
 
             callback()
           })
@@ -459,7 +460,7 @@ describe('Test multiple pods', function () {
       })
     })
 
-    it('Should views multiple videos on each servers', function (done) {
+    it('Should view multiple videos on each servers', function (done) {
       this.timeout(30000)
 
       parallel([
@@ -504,17 +505,17 @@ describe('Test multiple pods', function () {
         },
 
         function (callback) {
-          setTimeout(done, 22000)
+          setTimeout(callback, 22000)
         }
       ], function (err) {
         if (err) throw err
 
         let baseVideos = null
-        each(servers, function (server, callback) {
+        eachSeries(servers, function (server, callback) {
           videosUtils.getVideosList(server.url, function (err, res) {
             if (err) throw err
 
-            const videos = res.body
+            const videos = res.body.data
 
             // Initialize base videos for future comparisons
             if (baseVideos === null) {
@@ -522,10 +523,74 @@ describe('Test multiple pods', function () {
               return callback()
             }
 
-            for (let i = 0; i < baseVideos.length; i++) {
-              expect(baseVideos[i].views).to.equal(videos[i].views)
+            baseVideos.forEach(baseVideo => {
+              const sameVideo = videos.find(video => video.name === baseVideo.name)
+              expect(baseVideo.views).to.equal(sameVideo.views)
+            })
+
+            callback()
+          })
+        }, done)
+      })
+    })
+
+    it('Should like and dislikes videos on different services', function (done) {
+      this.timeout(30000)
+
+      parallel([
+        function (callback) {
+          videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'dislike', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'like', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'dislike', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[1], 'dislike', callback)
+        },
+
+        function (callback) {
+          videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[0], 'like', callback)
+        },
+
+        function (callback) {
+          setTimeout(callback, 22000)
+        }
+      ], function (err) {
+        if (err) throw err
+
+        let baseVideos = null
+        eachSeries(servers, function (server, callback) {
+          videosUtils.getVideosList(server.url, function (err, res) {
+            if (err) throw err
+
+            const videos = res.body.data
+
+            // Initialize base videos for future comparisons
+            if (baseVideos === null) {
+              baseVideos = videos
+              return callback()
             }
 
+            baseVideos.forEach(baseVideo => {
+              const sameVideo = videos.find(video => video.name === baseVideo.name)
+              expect(baseVideo.likes).to.equal(sameVideo.likes)
+              expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
+            })
+
             callback()
           })
         }, done)
index 87d0e9a71e6e9e09307236fe7ce3a5d764736f37..96e4aff9e7191faa4476d4f5daa0244615c5fb71 100644 (file)
@@ -609,6 +609,40 @@ describe('Test a single pod', function () {
     })
   })
 
+  it('Should like a video', function (done) {
+    videosUtils.rateVideo(server.url, server.accessToken, videoId, 'like', function (err) {
+      if (err) throw err
+
+      videosUtils.getVideo(server.url, videoId, function (err, res) {
+        if (err) throw err
+
+        const video = res.body
+
+        expect(video.likes).to.equal(1)
+        expect(video.dislikes).to.equal(0)
+
+        done()
+      })
+    })
+  })
+
+  it('Should dislike the same video', function (done) {
+    videosUtils.rateVideo(server.url, server.accessToken, videoId, 'dislike', function (err) {
+      if (err) throw err
+
+      videosUtils.getVideo(server.url, videoId, function (err, res) {
+        if (err) throw err
+
+        const video = res.body
+
+        expect(video.likes).to.equal(0)
+        expect(video.dislikes).to.equal(1)
+
+        done()
+      })
+    })
+  })
+
   after(function (done) {
     process.kill(-server.app.pid)
 
index bd95e78c22e639188a31ad1fb94b4435136a5d0a..f9568b874b533d1b438dca7caaf9ec040b5d009b 100644 (file)
@@ -10,6 +10,7 @@ const loginUtils = require('../utils/login')
 const podsUtils = require('../utils/pods')
 const serversUtils = require('../utils/servers')
 const usersUtils = require('../utils/users')
+const requestsUtils = require('../utils/requests')
 const videosUtils = require('../utils/videos')
 
 describe('Test users', function () {
@@ -138,6 +139,23 @@ describe('Test users', function () {
     videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done)
   })
 
+  it('Should retrieve a video rating', function (done) {
+    videosUtils.rateVideo(server.url, accessToken, videoId, 'like', function (err) {
+      if (err) throw err
+
+      usersUtils.getUserVideoRating(server.url, accessToken, videoId, function (err, res) {
+        if (err) throw err
+
+        const rating = res.body
+
+        expect(rating.videoId).to.equal(videoId)
+        expect(rating.rating).to.equal('like')
+
+        done()
+      })
+    })
+  })
+
   it('Should not be able to remove the video with an incorrect token', function (done) {
     videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done)
   })
@@ -150,10 +168,21 @@ describe('Test users', function () {
 
   it('Should logout (revoke token)')
 
+  it('Should not be able to get the user informations')
+
   it('Should not be able to upload a video')
 
   it('Should not be able to remove a video')
 
+  it('Should not be able to rate a video', function (done) {
+    const path = '/api/v1/videos/'
+    const data = {
+      rating: 'likes'
+    }
+
+    requestsUtils.makePutBodyRequest(server.url, path + videoId, 'wrong token', data, done, 401)
+  })
+
   it('Should be able to login again')
 
   it('Should have an expired access token')
index a2c010f641d41c18a98e0430941c1c42c5aa0c0b..7817160b94e9b50c39230fc5d70f171aca244f1d 100644 (file)
@@ -5,6 +5,7 @@ const request = require('supertest')
 const usersUtils = {
   createUser,
   getUserInformation,
+  getUserVideoRating,
   getUsersList,
   getUsersListPaginationAndSort,
   removeUser,
@@ -47,6 +48,18 @@ function getUserInformation (url, accessToken, end) {
     .end(end)
 }
 
+function getUserVideoRating (url, accessToken, videoId, end) {
+  const path = '/api/v1/users/me/videos/' + videoId + '/rating'
+
+  request(url)
+    .get(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(200)
+    .expect('Content-Type', /json/)
+    .end(end)
+}
+
 function getUsersList (url, end) {
   const path = '/api/v1/users'
 
index f943684375c4d63f1259e8683e375e3efb78d148..1774260761cfa55549a61497ddba6c9311b261e1 100644 (file)
@@ -16,7 +16,8 @@ const videosUtils = {
   searchVideoWithSort,
   testVideoImage,
   uploadVideo,
-  updateVideo
+  updateVideo,
+  rateVideo
 }
 
 // ---------------------- Export functions --------------------
@@ -236,6 +237,23 @@ function updateVideo (url, accessToken, id, name, description, tags, specialStat
   req.expect(specialStatus).end(end)
 }
 
+function rateVideo (url, accessToken, id, rating, specialStatus, end) {
+  if (!end) {
+    end = specialStatus
+    specialStatus = 204
+  }
+
+  const path = '/api/v1/videos/' + id + '/rate'
+
+  request(url)
+    .put(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .send({ rating })
+    .expect(specialStatus)
+    .end(end)
+}
+
 // ---------------------------------------------------------------------------
 
 module.exports = videosUtils