diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-03-08 21:35:43 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-03-08 21:35:43 +0100 |
commit | d38b82810638b9f664c9016fac2684454c273a77 (patch) | |
tree | 9465c367e5033675309efca4d66790c6fdd5230d | |
parent | 8f9064432122cba0f518a24ac4378357dadec589 (diff) | |
download | PeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.gz PeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.zst PeerTube-d38b82810638b9f664c9016fac2684454c273a77.zip |
Add like/dislike system for videos
31 files changed, 907 insertions, 47 deletions
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index 4cf100f0d..865e04d48 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -8,7 +8,7 @@ import { RestExtractor } from '../rest'; | |||
8 | 8 | ||
9 | @Injectable() | 9 | @Injectable() |
10 | export class UserService { | 10 | export class UserService { |
11 | private static BASE_USERS_URL = '/api/v1/users/'; | 11 | static BASE_USERS_URL = '/api/v1/users/'; |
12 | 12 | ||
13 | constructor( | 13 | constructor( |
14 | private authHttp: AuthHttp, | 14 | private authHttp: AuthHttp, |
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts index 67d16ead1..beaa528c0 100644 --- a/client/src/app/videos/shared/index.ts +++ b/client/src/app/videos/shared/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './loader'; | 1 | export * from './loader'; |
2 | export * from './sort-field.type'; | 2 | export * from './sort-field.type'; |
3 | export * from './rate-type.type'; | ||
3 | export * from './video.model'; | 4 | export * from './video.model'; |
4 | export * from './video.service'; | 5 | 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 index 000000000..88034d1ff --- /dev/null +++ b/client/src/app/videos/shared/rate-type.type.ts | |||
@@ -0,0 +1 @@ | |||
export type RateType = 'like' | 'dislike'; | |||
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/videos/shared/sort-field.type.ts index 74908e344..7bda3112a 100644 --- a/client/src/app/videos/shared/sort-field.type.ts +++ b/client/src/app/videos/shared/sort-field.type.ts | |||
@@ -1,3 +1,3 @@ | |||
1 | export type SortField = "name" | "-name" | 1 | export type SortField = 'name' | '-name' |
2 | | "duration" | "-duration" | 2 | | 'duration' | '-duration' |
3 | | "createdAt" | "-createdAt"; | 3 | | 'createdAt' | '-createdAt'; |
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts index 8e676708b..3eef936eb 100644 --- a/client/src/app/videos/shared/video.model.ts +++ b/client/src/app/videos/shared/video.model.ts | |||
@@ -12,6 +12,8 @@ export class Video { | |||
12 | tags: string[]; | 12 | tags: string[]; |
13 | thumbnailPath: string; | 13 | thumbnailPath: string; |
14 | views: number; | 14 | views: number; |
15 | likes: number; | ||
16 | dislikes: number; | ||
15 | 17 | ||
16 | private static createByString(author: string, podHost: string) { | 18 | private static createByString(author: string, podHost: string) { |
17 | return author + '@' + podHost; | 19 | return author + '@' + podHost; |
@@ -38,7 +40,9 @@ export class Video { | |||
38 | podHost: string, | 40 | podHost: string, |
39 | tags: string[], | 41 | tags: string[], |
40 | thumbnailPath: string, | 42 | thumbnailPath: string, |
41 | views: number | 43 | views: number, |
44 | likes: number, | ||
45 | dislikes: number, | ||
42 | }) { | 46 | }) { |
43 | this.author = hash.author; | 47 | this.author = hash.author; |
44 | this.createdAt = new Date(hash.createdAt); | 48 | this.createdAt = new Date(hash.createdAt); |
@@ -52,6 +56,8 @@ export class Video { | |||
52 | this.tags = hash.tags; | 56 | this.tags = hash.tags; |
53 | this.thumbnailPath = hash.thumbnailPath; | 57 | this.thumbnailPath = hash.thumbnailPath; |
54 | this.views = hash.views; | 58 | this.views = hash.views; |
59 | this.likes = hash.likes; | ||
60 | this.dislikes = hash.dislikes; | ||
55 | 61 | ||
56 | this.by = Video.createByString(hash.author, hash.podHost); | 62 | this.by = Video.createByString(hash.author, hash.podHost); |
57 | } | 63 | } |
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts index 7094d9a34..8bb5a2933 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/videos/shared/video.service.ts | |||
@@ -6,8 +6,16 @@ import 'rxjs/add/operator/map'; | |||
6 | 6 | ||
7 | import { Search } from '../../shared'; | 7 | import { Search } from '../../shared'; |
8 | import { SortField } from './sort-field.type'; | 8 | import { SortField } from './sort-field.type'; |
9 | import { RateType } from './rate-type.type'; | ||
9 | import { AuthService } from '../../core'; | 10 | import { AuthService } from '../../core'; |
10 | import { AuthHttp, RestExtractor, RestPagination, RestService, ResultList } from '../../shared'; | 11 | import { |
12 | AuthHttp, | ||
13 | RestExtractor, | ||
14 | RestPagination, | ||
15 | RestService, | ||
16 | ResultList, | ||
17 | UserService | ||
18 | } from '../../shared'; | ||
11 | import { Video } from './video.model'; | 19 | import { Video } from './video.model'; |
12 | 20 | ||
13 | @Injectable() | 21 | @Injectable() |
@@ -56,14 +64,41 @@ export class VideoService { | |||
56 | } | 64 | } |
57 | 65 | ||
58 | reportVideo(id: string, reason: string) { | 66 | reportVideo(id: string, reason: string) { |
67 | const url = VideoService.BASE_VIDEO_URL + id + '/abuse'; | ||
59 | const body = { | 68 | const body = { |
60 | reason | 69 | reason |
61 | }; | 70 | }; |
62 | const url = VideoService.BASE_VIDEO_URL + id + '/abuse'; | ||
63 | 71 | ||
64 | return this.authHttp.post(url, body) | 72 | return this.authHttp.post(url, body) |
65 | .map(this.restExtractor.extractDataBool) | 73 | .map(this.restExtractor.extractDataBool) |
66 | .catch((res) => this.restExtractor.handleError(res)); | 74 | .catch((res) => this.restExtractor.handleError(res)); |
75 | } | ||
76 | |||
77 | setVideoLike(id: string) { | ||
78 | return this.setVideoRate(id, 'like'); | ||
79 | } | ||
80 | |||
81 | setVideoDislike(id: string) { | ||
82 | return this.setVideoRate(id, 'dislike'); | ||
83 | } | ||
84 | |||
85 | getUserVideoRating(id: string) { | ||
86 | const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating'; | ||
87 | |||
88 | return this.authHttp.get(url) | ||
89 | .map(this.restExtractor.extractDataGet) | ||
90 | .catch((res) => this.restExtractor.handleError(res)); | ||
91 | } | ||
92 | |||
93 | private setVideoRate(id: string, rateType: RateType) { | ||
94 | const url = VideoService.BASE_VIDEO_URL + id + '/rate'; | ||
95 | const body = { | ||
96 | rating: rateType | ||
97 | }; | ||
98 | |||
99 | return this.authHttp.put(url, body) | ||
100 | .map(this.restExtractor.extractDataBool) | ||
101 | .catch((res) => this.restExtractor.handleError(res)); | ||
67 | } | 102 | } |
68 | 103 | ||
69 | private extractVideos(result: ResultList) { | 104 | private extractVideos(result: ResultList) { |
diff --git a/client/src/app/videos/video-watch/video-watch.component.html b/client/src/app/videos/video-watch/video-watch.component.html index 24d741ff9..67094359e 100644 --- a/client/src/app/videos/video-watch/video-watch.component.html +++ b/client/src/app/videos/video-watch/video-watch.component.html | |||
@@ -32,7 +32,7 @@ | |||
32 | 32 | ||
33 | <div *ngIf="video !== null" id="video-info"> | 33 | <div *ngIf="video !== null" id="video-info"> |
34 | <div class="row" id="video-name-actions"> | 34 | <div class="row" id="video-name-actions"> |
35 | <div class="col-md-8"> | 35 | <div class="col-md-6"> |
36 | <div class="row"> | 36 | <div class="row"> |
37 | <div id="video-name" class="col-md-12"> | 37 | <div id="video-name" class="col-md-12"> |
38 | {{ video.name }} | 38 | {{ video.name }} |
@@ -52,7 +52,23 @@ | |||
52 | </div> | 52 | </div> |
53 | </div> | 53 | </div> |
54 | 54 | ||
55 | <div id="video-actions" class="col-md-4 text-right"> | 55 | <div id="video-actions" class="col-md-6 text-right"> |
56 | <div id="rates"> | ||
57 | <button | ||
58 | id="likes" class="btn btn-default" | ||
59 | [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'like' }" (click)="setLike()" | ||
60 | > | ||
61 | <span class="glyphicon glyphicon-thumbs-up"></span> {{ video.likes }} | ||
62 | </button> | ||
63 | |||
64 | <button | ||
65 | id="dislikes" class="btn btn-default" | ||
66 | [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'dislike' }" (click)="setDislike()" | ||
67 | > | ||
68 | <span class=" glyphicon glyphicon-thumbs-down"></span> {{ video.dislikes }} | ||
69 | </button> | ||
70 | </div> | ||
71 | |||
56 | <button id="share" class="btn btn-default" (click)="showShareModal()"> | 72 | <button id="share" class="btn btn-default" (click)="showShareModal()"> |
57 | <span class="glyphicon glyphicon-share"></span> Share | 73 | <span class="glyphicon glyphicon-share"></span> Share |
58 | </button> | 74 | </button> |
diff --git a/client/src/app/videos/video-watch/video-watch.component.scss b/client/src/app/videos/video-watch/video-watch.component.scss index 0b8af52ce..5f322a194 100644 --- a/client/src/app/videos/video-watch/video-watch.component.scss +++ b/client/src/app/videos/video-watch/video-watch.component.scss | |||
@@ -47,6 +47,34 @@ | |||
47 | top: 2px; | 47 | top: 2px; |
48 | } | 48 | } |
49 | 49 | ||
50 | #rates { | ||
51 | display: inline-block; | ||
52 | margin-right: 20px; | ||
53 | |||
54 | // Remove focus style | ||
55 | .btn:focus { | ||
56 | outline: 0; | ||
57 | } | ||
58 | |||
59 | .activated-btn { | ||
60 | color: #333; | ||
61 | background-color: #e6e6e6; | ||
62 | border-color: #8c8c8c; | ||
63 | } | ||
64 | |||
65 | .not-interactive-btn { | ||
66 | cursor: default; | ||
67 | |||
68 | &:hover, &:focus, &:active { | ||
69 | color: #333; | ||
70 | background-color: #fff; | ||
71 | border-color: #ccc; | ||
72 | box-shadow: none; | ||
73 | outline: 0; | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | |||
50 | #share, #more { | 78 | #share, #more { |
51 | font-weight: bold; | 79 | font-weight: bold; |
52 | opacity: 0.85; | 80 | opacity: 0.85; |
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts index d1abc81bc..ed6b30102 100644 --- a/client/src/app/videos/video-watch/video-watch.component.ts +++ b/client/src/app/videos/video-watch/video-watch.component.ts | |||
@@ -10,7 +10,7 @@ import { AuthService } from '../../core'; | |||
10 | import { VideoMagnetComponent } from './video-magnet.component'; | 10 | import { VideoMagnetComponent } from './video-magnet.component'; |
11 | import { VideoShareComponent } from './video-share.component'; | 11 | import { VideoShareComponent } from './video-share.component'; |
12 | import { VideoReportComponent } from './video-report.component'; | 12 | import { VideoReportComponent } from './video-report.component'; |
13 | import { Video, VideoService } from '../shared'; | 13 | import { RateType, Video, VideoService } from '../shared'; |
14 | import { WebTorrentService } from './webtorrent.service'; | 14 | import { WebTorrentService } from './webtorrent.service'; |
15 | 15 | ||
16 | @Component({ | 16 | @Component({ |
@@ -33,6 +33,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
33 | player: VideoJSPlayer; | 33 | player: VideoJSPlayer; |
34 | playerElement: Element; | 34 | playerElement: Element; |
35 | uploadSpeed: number; | 35 | uploadSpeed: number; |
36 | userRating: RateType = null; | ||
36 | video: Video = null; | 37 | video: Video = null; |
37 | videoNotFound = false; | 38 | videoNotFound = false; |
38 | 39 | ||
@@ -61,6 +62,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
61 | this.video = video; | 62 | this.video = video; |
62 | this.setOpenGraphTags(); | 63 | this.setOpenGraphTags(); |
63 | this.loadVideo(); | 64 | this.loadVideo(); |
65 | this.checkUserRating(); | ||
64 | }, | 66 | }, |
65 | error => { | 67 | error => { |
66 | this.videoNotFound = true; | 68 | this.videoNotFound = true; |
@@ -136,6 +138,40 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
136 | }); | 138 | }); |
137 | } | 139 | } |
138 | 140 | ||
141 | setLike() { | ||
142 | if (this.isUserLoggedIn() === false) return; | ||
143 | // Already liked this video | ||
144 | if (this.userRating === 'like') return; | ||
145 | |||
146 | this.videoService.setVideoLike(this.video.id) | ||
147 | .subscribe( | ||
148 | () => { | ||
149 | // Update the video like attribute | ||
150 | this.updateVideoRating(this.userRating, 'like'); | ||
151 | this.userRating = 'like'; | ||
152 | }, | ||
153 | |||
154 | err => this.notificationsService.error('Error', err.text) | ||
155 | ); | ||
156 | } | ||
157 | |||
158 | setDislike() { | ||
159 | if (this.isUserLoggedIn() === false) return; | ||
160 | // Already disliked this video | ||
161 | if (this.userRating === 'dislike') return; | ||
162 | |||
163 | this.videoService.setVideoDislike(this.video.id) | ||
164 | .subscribe( | ||
165 | () => { | ||
166 | // Update the video dislike attribute | ||
167 | this.updateVideoRating(this.userRating, 'dislike'); | ||
168 | this.userRating = 'dislike'; | ||
169 | }, | ||
170 | |||
171 | err => this.notificationsService.error('Error', err.text) | ||
172 | ); | ||
173 | } | ||
174 | |||
139 | showReportModal(event: Event) { | 175 | showReportModal(event: Event) { |
140 | event.preventDefault(); | 176 | event.preventDefault(); |
141 | this.videoReportModal.show(); | 177 | this.videoReportModal.show(); |
@@ -154,6 +190,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
154 | return this.authService.isLoggedIn(); | 190 | return this.authService.isLoggedIn(); |
155 | } | 191 | } |
156 | 192 | ||
193 | private checkUserRating() { | ||
194 | // Unlogged users do not have ratings | ||
195 | if (this.isUserLoggedIn() === false) return; | ||
196 | |||
197 | this.videoService.getUserVideoRating(this.video.id) | ||
198 | .subscribe( | ||
199 | ratingObject => { | ||
200 | if (ratingObject) { | ||
201 | this.userRating = ratingObject.rating; | ||
202 | } | ||
203 | }, | ||
204 | |||
205 | err => this.notificationsService.error('Error', err.text) | ||
206 | ); | ||
207 | } | ||
208 | |||
209 | private updateVideoRating(oldRating: RateType, newRating: RateType) { | ||
210 | let likesToIncrement = 0; | ||
211 | let dislikesToIncrement = 0; | ||
212 | |||
213 | if (oldRating) { | ||
214 | if (oldRating === 'like') likesToIncrement--; | ||
215 | if (oldRating === 'dislike') dislikesToIncrement--; | ||
216 | } | ||
217 | |||
218 | if (newRating === 'like') likesToIncrement++; | ||
219 | if (newRating === 'dislike') dislikesToIncrement++; | ||
220 | |||
221 | this.video.likes += likesToIncrement; | ||
222 | this.video.dislikes += dislikesToIncrement; | ||
223 | } | ||
224 | |||
157 | private loadTooLong() { | 225 | private loadTooLong() { |
158 | this.error = true; | 226 | this.error = true; |
159 | console.error('The video load seems to be abnormally long.'); | 227 | console.error('The video load seems to be abnormally long.'); |
diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js index 39c9579c1..98891c99e 100644 --- a/server/controllers/api/remote/videos.js +++ b/server/controllers/api/remote/videos.js | |||
@@ -11,6 +11,7 @@ const secureMiddleware = middlewares.secure | |||
11 | const videosValidators = middlewares.validators.remote.videos | 11 | const videosValidators = middlewares.validators.remote.videos |
12 | const signatureValidators = middlewares.validators.remote.signature | 12 | const signatureValidators = middlewares.validators.remote.signature |
13 | const logger = require('../../../helpers/logger') | 13 | const logger = require('../../../helpers/logger') |
14 | const friends = require('../../../lib/friends') | ||
14 | const databaseUtils = require('../../../helpers/database-utils') | 15 | const databaseUtils = require('../../../helpers/database-utils') |
15 | 16 | ||
16 | const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS] | 17 | const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS] |
@@ -129,18 +130,22 @@ function processVideosEvents (eventData, fromPod, finalCallback) { | |||
129 | const options = { transaction: t } | 130 | const options = { transaction: t } |
130 | 131 | ||
131 | let columnToUpdate | 132 | let columnToUpdate |
133 | let qaduType | ||
132 | 134 | ||
133 | switch (eventData.eventType) { | 135 | switch (eventData.eventType) { |
134 | case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS: | 136 | case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS: |
135 | columnToUpdate = 'views' | 137 | columnToUpdate = 'views' |
138 | qaduType = constants.REQUEST_VIDEO_QADU_TYPES.VIEWS | ||
136 | break | 139 | break |
137 | 140 | ||
138 | case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES: | 141 | case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES: |
139 | columnToUpdate = 'likes' | 142 | columnToUpdate = 'likes' |
143 | qaduType = constants.REQUEST_VIDEO_QADU_TYPES.LIKES | ||
140 | break | 144 | break |
141 | 145 | ||
142 | case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES: | 146 | case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES: |
143 | columnToUpdate = 'dislikes' | 147 | columnToUpdate = 'dislikes' |
148 | qaduType = constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES | ||
144 | break | 149 | break |
145 | 150 | ||
146 | default: | 151 | default: |
@@ -151,6 +156,19 @@ function processVideosEvents (eventData, fromPod, finalCallback) { | |||
151 | query[columnToUpdate] = eventData.count | 156 | query[columnToUpdate] = eventData.count |
152 | 157 | ||
153 | videoInstance.increment(query, options).asCallback(function (err) { | 158 | videoInstance.increment(query, options).asCallback(function (err) { |
159 | return callback(err, t, videoInstance, qaduType) | ||
160 | }) | ||
161 | }, | ||
162 | |||
163 | function sendQaduToFriends (t, videoInstance, qaduType, callback) { | ||
164 | const qadusParams = [ | ||
165 | { | ||
166 | videoId: videoInstance.id, | ||
167 | type: qaduType | ||
168 | } | ||
169 | ] | ||
170 | |||
171 | friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { | ||
154 | return callback(err, t) | 172 | return callback(err, t) |
155 | }) | 173 | }) |
156 | }, | 174 | }, |
@@ -159,7 +177,6 @@ function processVideosEvents (eventData, fromPod, finalCallback) { | |||
159 | 177 | ||
160 | ], function (err, t) { | 178 | ], function (err, t) { |
161 | if (err) { | 179 | if (err) { |
162 | console.log(err) | ||
163 | logger.debug('Cannot process a video event.', { error: err }) | 180 | logger.debug('Cannot process a video event.', { error: err }) |
164 | return databaseUtils.rollbackTransaction(err, t, finalCallback) | 181 | return databaseUtils.rollbackTransaction(err, t, finalCallback) |
165 | } | 182 | } |
@@ -278,7 +295,10 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { | |||
278 | duration: videoToCreateData.duration, | 295 | duration: videoToCreateData.duration, |
279 | createdAt: videoToCreateData.createdAt, | 296 | createdAt: videoToCreateData.createdAt, |
280 | // FIXME: updatedAt does not seems to be considered by Sequelize | 297 | // FIXME: updatedAt does not seems to be considered by Sequelize |
281 | updatedAt: videoToCreateData.updatedAt | 298 | updatedAt: videoToCreateData.updatedAt, |
299 | views: videoToCreateData.views, | ||
300 | likes: videoToCreateData.likes, | ||
301 | dislikes: videoToCreateData.dislikes | ||
282 | } | 302 | } |
283 | 303 | ||
284 | const video = db.Video.build(videoData) | 304 | const video = db.Video.build(videoData) |
@@ -372,6 +392,9 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { | |||
372 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) | 392 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) |
373 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | 393 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) |
374 | videoInstance.set('extname', videoAttributesToUpdate.extname) | 394 | videoInstance.set('extname', videoAttributesToUpdate.extname) |
395 | videoInstance.set('views', videoAttributesToUpdate.views) | ||
396 | videoInstance.set('likes', videoAttributesToUpdate.likes) | ||
397 | videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) | ||
375 | 398 | ||
376 | videoInstance.save(options).asCallback(function (err) { | 399 | videoInstance.save(options).asCallback(function (err) { |
377 | return callback(err, t, videoInstance, tagInstances) | 400 | return callback(err, t, videoInstance, tagInstances) |
diff --git a/server/controllers/api/users.js b/server/controllers/api/users.js index 324c99b4c..f854b3082 100644 --- a/server/controllers/api/users.js +++ b/server/controllers/api/users.js | |||
@@ -18,7 +18,16 @@ const validatorsUsers = middlewares.validators.users | |||
18 | 18 | ||
19 | const router = express.Router() | 19 | const router = express.Router() |
20 | 20 | ||
21 | router.get('/me', oAuth.authenticate, getUserInformation) | 21 | router.get('/me', |
22 | oAuth.authenticate, | ||
23 | getUserInformation | ||
24 | ) | ||
25 | |||
26 | router.get('/me/videos/:videoId/rating', | ||
27 | oAuth.authenticate, | ||
28 | validatorsUsers.usersVideoRating, | ||
29 | getUserVideoRating | ||
30 | ) | ||
22 | 31 | ||
23 | router.get('/', | 32 | router.get('/', |
24 | validatorsPagination.pagination, | 33 | validatorsPagination.pagination, |
@@ -80,6 +89,22 @@ function getUserInformation (req, res, next) { | |||
80 | }) | 89 | }) |
81 | } | 90 | } |
82 | 91 | ||
92 | function getUserVideoRating (req, res, next) { | ||
93 | const videoId = req.params.videoId | ||
94 | const userId = res.locals.oauth.token.User.id | ||
95 | |||
96 | db.UserVideoRate.load(userId, videoId, function (err, ratingObj) { | ||
97 | if (err) return next(err) | ||
98 | |||
99 | const rating = ratingObj ? ratingObj.type : 'none' | ||
100 | |||
101 | res.json({ | ||
102 | videoId, | ||
103 | rating | ||
104 | }) | ||
105 | }) | ||
106 | } | ||
107 | |||
83 | function listUsers (req, res, next) { | 108 | function listUsers (req, res, next) { |
84 | db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { | 109 | db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { |
85 | if (err) return next(err) | 110 | if (err) return next(err) |
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js index 5a67d1121..9acdb8fd2 100644 --- a/server/controllers/api/videos.js +++ b/server/controllers/api/videos.js | |||
@@ -60,6 +60,12 @@ router.post('/:id/abuse', | |||
60 | reportVideoAbuseRetryWrapper | 60 | reportVideoAbuseRetryWrapper |
61 | ) | 61 | ) |
62 | 62 | ||
63 | router.put('/:id/rate', | ||
64 | oAuth.authenticate, | ||
65 | validatorsVideos.videoRate, | ||
66 | rateVideoRetryWrapper | ||
67 | ) | ||
68 | |||
63 | router.get('/', | 69 | router.get('/', |
64 | validatorsPagination.pagination, | 70 | validatorsPagination.pagination, |
65 | validatorsSort.videosSort, | 71 | validatorsSort.videosSort, |
@@ -104,6 +110,147 @@ module.exports = router | |||
104 | 110 | ||
105 | // --------------------------------------------------------------------------- | 111 | // --------------------------------------------------------------------------- |
106 | 112 | ||
113 | function rateVideoRetryWrapper (req, res, next) { | ||
114 | const options = { | ||
115 | arguments: [ req, res ], | ||
116 | errorMessage: 'Cannot update the user video rate.' | ||
117 | } | ||
118 | |||
119 | databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) { | ||
120 | if (err) return next(err) | ||
121 | |||
122 | return res.type('json').status(204).end() | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | function rateVideo (req, res, finalCallback) { | ||
127 | const rateType = req.body.rating | ||
128 | const videoInstance = res.locals.video | ||
129 | const userInstance = res.locals.oauth.token.User | ||
130 | |||
131 | waterfall([ | ||
132 | databaseUtils.startSerializableTransaction, | ||
133 | |||
134 | function findPreviousRate (t, callback) { | ||
135 | db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) { | ||
136 | return callback(err, t, previousRate) | ||
137 | }) | ||
138 | }, | ||
139 | |||
140 | function insertUserRateIntoDB (t, previousRate, callback) { | ||
141 | const options = { transaction: t } | ||
142 | |||
143 | let likesToIncrement = 0 | ||
144 | let dislikesToIncrement = 0 | ||
145 | |||
146 | if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++ | ||
147 | else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ | ||
148 | |||
149 | // There was a previous rate, update it | ||
150 | if (previousRate) { | ||
151 | // We will remove the previous rate, so we will need to remove it from the video attribute | ||
152 | if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement-- | ||
153 | else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- | ||
154 | |||
155 | previousRate.type = rateType | ||
156 | |||
157 | previousRate.save(options).asCallback(function (err) { | ||
158 | return callback(err, t, likesToIncrement, dislikesToIncrement) | ||
159 | }) | ||
160 | } else { // There was not a previous rate, insert a new one | ||
161 | const query = { | ||
162 | userId: userInstance.id, | ||
163 | videoId: videoInstance.id, | ||
164 | type: rateType | ||
165 | } | ||
166 | |||
167 | db.UserVideoRate.create(query, options).asCallback(function (err) { | ||
168 | return callback(err, t, likesToIncrement, dislikesToIncrement) | ||
169 | }) | ||
170 | } | ||
171 | }, | ||
172 | |||
173 | function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) { | ||
174 | const options = { transaction: t } | ||
175 | const incrementQuery = { | ||
176 | likes: likesToIncrement, | ||
177 | dislikes: dislikesToIncrement | ||
178 | } | ||
179 | |||
180 | // Even if we do not own the video we increment the attributes | ||
181 | // It is usefull for the user to have a feedback | ||
182 | videoInstance.increment(incrementQuery, options).asCallback(function (err) { | ||
183 | return callback(err, t, likesToIncrement, dislikesToIncrement) | ||
184 | }) | ||
185 | }, | ||
186 | |||
187 | function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { | ||
188 | // No need for an event type, we own the video | ||
189 | if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement) | ||
190 | |||
191 | const eventsParams = [] | ||
192 | |||
193 | if (likesToIncrement !== 0) { | ||
194 | eventsParams.push({ | ||
195 | videoId: videoInstance.id, | ||
196 | type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES, | ||
197 | count: likesToIncrement | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | if (dislikesToIncrement !== 0) { | ||
202 | eventsParams.push({ | ||
203 | videoId: videoInstance.id, | ||
204 | type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES, | ||
205 | count: dislikesToIncrement | ||
206 | }) | ||
207 | } | ||
208 | |||
209 | friends.addEventsToRemoteVideo(eventsParams, t, function (err) { | ||
210 | return callback(err, t, likesToIncrement, dislikesToIncrement) | ||
211 | }) | ||
212 | }, | ||
213 | |||
214 | function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { | ||
215 | // We do not own the video, there is no need to send a quick and dirty update to friends | ||
216 | // Our rate was already sent by the addEvent function | ||
217 | if (videoInstance.isOwned() === false) return callback(null, t) | ||
218 | |||
219 | const qadusParams = [] | ||
220 | |||
221 | if (likesToIncrement !== 0) { | ||
222 | qadusParams.push({ | ||
223 | videoId: videoInstance.id, | ||
224 | type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES | ||
225 | }) | ||
226 | } | ||
227 | |||
228 | if (dislikesToIncrement !== 0) { | ||
229 | qadusParams.push({ | ||
230 | videoId: videoInstance.id, | ||
231 | type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES | ||
232 | }) | ||
233 | } | ||
234 | |||
235 | friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { | ||
236 | return callback(err, t) | ||
237 | }) | ||
238 | }, | ||
239 | |||
240 | databaseUtils.commitTransaction | ||
241 | |||
242 | ], function (err, t) { | ||
243 | if (err) { | ||
244 | // This is just a debug because we will retry the insert | ||
245 | logger.debug('Cannot add the user video rate.', { error: err }) | ||
246 | return databaseUtils.rollbackTransaction(err, t, finalCallback) | ||
247 | } | ||
248 | |||
249 | logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) | ||
250 | return finalCallback(null) | ||
251 | }) | ||
252 | } | ||
253 | |||
107 | // Wrapper to video add that retry the function if there is a database error | 254 | // Wrapper to video add that retry the function if there is a database error |
108 | // We need this because we run the transaction in SERIALIZABLE isolation that can fail | 255 | // We need this because we run the transaction in SERIALIZABLE isolation that can fail |
109 | function addVideoRetryWrapper (req, res, next) { | 256 | function addVideoRetryWrapper (req, res, next) { |
@@ -155,8 +302,7 @@ function addVideo (req, res, videoFile, finalCallback) { | |||
155 | extname: path.extname(videoFile.filename), | 302 | extname: path.extname(videoFile.filename), |
156 | description: videoInfos.description, | 303 | description: videoInfos.description, |
157 | duration: videoFile.duration, | 304 | duration: videoFile.duration, |
158 | authorId: author.id, | 305 | authorId: author.id |
159 | views: videoInfos.views | ||
160 | } | 306 | } |
161 | 307 | ||
162 | const video = db.Video.build(videoData) | 308 | const video = db.Video.build(videoData) |
@@ -332,11 +478,19 @@ function getVideo (req, res, next) { | |||
332 | 478 | ||
333 | // FIXME: make a real view system | 479 | // FIXME: make a real view system |
334 | // For example, only add a view when a user watch a video during 30s etc | 480 | // For example, only add a view when a user watch a video during 30s etc |
335 | friends.quickAndDirtyUpdateVideoToFriends(videoInstance.id, constants.REQUEST_VIDEO_QADU_TYPES.VIEWS) | 481 | const qaduParams = { |
482 | videoId: videoInstance.id, | ||
483 | type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS | ||
484 | } | ||
485 | friends.quickAndDirtyUpdateVideoToFriends(qaduParams) | ||
336 | }) | 486 | }) |
337 | } else { | 487 | } else { |
338 | // Just send the event to our friends | 488 | // Just send the event to our friends |
339 | friends.addEventToRemoteVideo(videoInstance.id, constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS) | 489 | const eventParams = { |
490 | videoId: videoInstance.id, | ||
491 | type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS | ||
492 | } | ||
493 | friends.addEventToRemoteVideo(eventParams) | ||
340 | } | 494 | } |
341 | 495 | ||
342 | // Do not wait the view system | 496 | // Do not wait the view system |
diff --git a/server/helpers/custom-validators/remote/videos.js b/server/helpers/custom-validators/remote/videos.js index ba2d0bb93..e1636e0e6 100644 --- a/server/helpers/custom-validators/remote/videos.js +++ b/server/helpers/custom-validators/remote/videos.js | |||
@@ -92,7 +92,9 @@ function isCommonVideoAttributesValid (video) { | |||
92 | videosValidators.isVideoTagsValid(video.tags) && | 92 | videosValidators.isVideoTagsValid(video.tags) && |
93 | videosValidators.isVideoRemoteIdValid(video.remoteId) && | 93 | videosValidators.isVideoRemoteIdValid(video.remoteId) && |
94 | videosValidators.isVideoExtnameValid(video.extname) && | 94 | videosValidators.isVideoExtnameValid(video.extname) && |
95 | videosValidators.isVideoViewsValid(video.views) | 95 | videosValidators.isVideoViewsValid(video.views) && |
96 | videosValidators.isVideoLikesValid(video.likes) && | ||
97 | videosValidators.isVideoDislikesValid(video.dislikes) | ||
96 | } | 98 | } |
97 | 99 | ||
98 | function isRequestTypeAddValid (value) { | 100 | function isRequestTypeAddValid (value) { |
diff --git a/server/helpers/custom-validators/videos.js b/server/helpers/custom-validators/videos.js index c5a1f3cb5..648c7540b 100644 --- a/server/helpers/custom-validators/videos.js +++ b/server/helpers/custom-validators/videos.js | |||
@@ -1,6 +1,7 @@ | |||
1 | 'use strict' | 1 | 'use strict' |
2 | 2 | ||
3 | const validator = require('express-validator').validator | 3 | const validator = require('express-validator').validator |
4 | const values = require('lodash/values') | ||
4 | 5 | ||
5 | const constants = require('../../initializers/constants') | 6 | const constants = require('../../initializers/constants') |
6 | const usersValidators = require('./users') | 7 | const usersValidators = require('./users') |
@@ -26,6 +27,7 @@ const videosValidators = { | |||
26 | isVideoFile, | 27 | isVideoFile, |
27 | isVideoViewsValid, | 28 | isVideoViewsValid, |
28 | isVideoLikesValid, | 29 | isVideoLikesValid, |
30 | isVideoRatingTypeValid, | ||
29 | isVideoDislikesValid, | 31 | isVideoDislikesValid, |
30 | isVideoEventCountValid | 32 | isVideoEventCountValid |
31 | } | 33 | } |
@@ -103,6 +105,10 @@ function isVideoEventCountValid (value) { | |||
103 | return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT) | 105 | return validator.isInt(value + '', VIDEO_EVENTS_CONSTRAINTS_FIELDS.COUNT) |
104 | } | 106 | } |
105 | 107 | ||
108 | function isVideoRatingTypeValid (value) { | ||
109 | return values(constants.VIDEO_RATE_TYPES).indexOf(value) !== -1 | ||
110 | } | ||
111 | |||
106 | function isVideoFile (value, files) { | 112 | function isVideoFile (value, files) { |
107 | // Should have files | 113 | // Should have files |
108 | if (!files) return false | 114 | if (!files) return false |
diff --git a/server/initializers/constants.js b/server/initializers/constants.js index 2d5bb84cc..16a2dd320 100644 --- a/server/initializers/constants.js +++ b/server/initializers/constants.js | |||
@@ -5,7 +5,7 @@ const path = require('path') | |||
5 | 5 | ||
6 | // --------------------------------------------------------------------------- | 6 | // --------------------------------------------------------------------------- |
7 | 7 | ||
8 | const LAST_MIGRATION_VERSION = 15 | 8 | const LAST_MIGRATION_VERSION = 25 |
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
11 | 11 | ||
@@ -95,6 +95,11 @@ const CONSTRAINTS_FIELDS = { | |||
95 | } | 95 | } |
96 | } | 96 | } |
97 | 97 | ||
98 | const VIDEO_RATE_TYPES = { | ||
99 | LIKE: 'like', | ||
100 | DISLIKE: 'dislike' | ||
101 | } | ||
102 | |||
98 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
99 | 104 | ||
100 | // Score a pod has when we create it as a friend | 105 | // Score a pod has when we create it as a friend |
@@ -249,7 +254,8 @@ module.exports = { | |||
249 | STATIC_MAX_AGE, | 254 | STATIC_MAX_AGE, |
250 | STATIC_PATHS, | 255 | STATIC_PATHS, |
251 | THUMBNAILS_SIZE, | 256 | THUMBNAILS_SIZE, |
252 | USER_ROLES | 257 | USER_ROLES, |
258 | VIDEO_RATE_TYPES | ||
253 | } | 259 | } |
254 | 260 | ||
255 | // --------------------------------------------------------------------------- | 261 | // --------------------------------------------------------------------------- |
diff --git a/server/initializers/migrations/0020-video-likes.js b/server/initializers/migrations/0020-video-likes.js new file mode 100644 index 000000000..6db62cb90 --- /dev/null +++ b/server/initializers/migrations/0020-video-likes.js | |||
@@ -0,0 +1,19 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | // utils = { transaction, queryInterface, sequelize, Sequelize } | ||
4 | exports.up = function (utils, finalCallback) { | ||
5 | const q = utils.queryInterface | ||
6 | const Sequelize = utils.Sequelize | ||
7 | |||
8 | const data = { | ||
9 | type: Sequelize.INTEGER, | ||
10 | allowNull: false, | ||
11 | defaultValue: 0 | ||
12 | } | ||
13 | |||
14 | q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback) | ||
15 | } | ||
16 | |||
17 | exports.down = function (options, callback) { | ||
18 | throw new Error('Not implemented.') | ||
19 | } | ||
diff --git a/server/initializers/migrations/0025-video-dislikes.js b/server/initializers/migrations/0025-video-dislikes.js new file mode 100644 index 000000000..40d2e7351 --- /dev/null +++ b/server/initializers/migrations/0025-video-dislikes.js | |||
@@ -0,0 +1,19 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | // utils = { transaction, queryInterface, sequelize, Sequelize } | ||
4 | exports.up = function (utils, finalCallback) { | ||
5 | const q = utils.queryInterface | ||
6 | const Sequelize = utils.Sequelize | ||
7 | |||
8 | const data = { | ||
9 | type: Sequelize.INTEGER, | ||
10 | allowNull: false, | ||
11 | defaultValue: 0 | ||
12 | } | ||
13 | |||
14 | q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback) | ||
15 | } | ||
16 | |||
17 | exports.down = function (options, callback) { | ||
18 | throw new Error('Not implemented.') | ||
19 | } | ||
diff --git a/server/lib/friends.js b/server/lib/friends.js index 7bd087d8c..23accfa45 100644 --- a/server/lib/friends.js +++ b/server/lib/friends.js | |||
@@ -3,6 +3,7 @@ | |||
3 | const each = require('async/each') | 3 | const each = require('async/each') |
4 | const eachLimit = require('async/eachLimit') | 4 | const eachLimit = require('async/eachLimit') |
5 | const eachSeries = require('async/eachSeries') | 5 | const eachSeries = require('async/eachSeries') |
6 | const series = require('async/series') | ||
6 | const request = require('request') | 7 | const request = require('request') |
7 | const waterfall = require('async/waterfall') | 8 | const waterfall = require('async/waterfall') |
8 | 9 | ||
@@ -28,7 +29,9 @@ const friends = { | |||
28 | updateVideoToFriends, | 29 | updateVideoToFriends, |
29 | reportAbuseVideoToFriend, | 30 | reportAbuseVideoToFriend, |
30 | quickAndDirtyUpdateVideoToFriends, | 31 | quickAndDirtyUpdateVideoToFriends, |
32 | quickAndDirtyUpdatesVideoToFriends, | ||
31 | addEventToRemoteVideo, | 33 | addEventToRemoteVideo, |
34 | addEventsToRemoteVideo, | ||
32 | hasFriends, | 35 | hasFriends, |
33 | makeFriends, | 36 | makeFriends, |
34 | quitFriends, | 37 | quitFriends, |
@@ -84,24 +87,52 @@ function reportAbuseVideoToFriend (reportData, video) { | |||
84 | createRequest(options) | 87 | createRequest(options) |
85 | } | 88 | } |
86 | 89 | ||
87 | function quickAndDirtyUpdateVideoToFriends (videoId, type, transaction, callback) { | 90 | function quickAndDirtyUpdateVideoToFriends (qaduParams, transaction, callback) { |
88 | const options = { | 91 | const options = { |
89 | videoId, | 92 | videoId: qaduParams.videoId, |
90 | type, | 93 | type: qaduParams.type, |
91 | transaction | 94 | transaction |
92 | } | 95 | } |
93 | return createVideoQaduRequest(options, callback) | 96 | return createVideoQaduRequest(options, callback) |
94 | } | 97 | } |
95 | 98 | ||
96 | function addEventToRemoteVideo (videoId, type, transaction, callback) { | 99 | function quickAndDirtyUpdatesVideoToFriends (qadusParams, transaction, finalCallback) { |
100 | const tasks = [] | ||
101 | |||
102 | qadusParams.forEach(function (qaduParams) { | ||
103 | const fun = function (callback) { | ||
104 | quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback) | ||
105 | } | ||
106 | |||
107 | tasks.push(fun) | ||
108 | }) | ||
109 | |||
110 | series(tasks, finalCallback) | ||
111 | } | ||
112 | |||
113 | function addEventToRemoteVideo (eventParams, transaction, callback) { | ||
97 | const options = { | 114 | const options = { |
98 | videoId, | 115 | videoId: eventParams.videoId, |
99 | type, | 116 | type: eventParams.type, |
100 | transaction | 117 | transaction |
101 | } | 118 | } |
102 | createVideoEventRequest(options, callback) | 119 | createVideoEventRequest(options, callback) |
103 | } | 120 | } |
104 | 121 | ||
122 | function addEventsToRemoteVideo (eventsParams, transaction, finalCallback) { | ||
123 | const tasks = [] | ||
124 | |||
125 | eventsParams.forEach(function (eventParams) { | ||
126 | const fun = function (callback) { | ||
127 | addEventToRemoteVideo(eventParams, transaction, callback) | ||
128 | } | ||
129 | |||
130 | tasks.push(fun) | ||
131 | }) | ||
132 | |||
133 | series(tasks, finalCallback) | ||
134 | } | ||
135 | |||
105 | function hasFriends (callback) { | 136 | function hasFriends (callback) { |
106 | db.Pod.countAll(function (err, count) { | 137 | db.Pod.countAll(function (err, count) { |
107 | if (err) return callback(err) | 138 | if (err) return callback(err) |
diff --git a/server/lib/request-video-qadu-scheduler.js b/server/lib/request-video-qadu-scheduler.js index ac50cfc11..a85d35160 100644 --- a/server/lib/request-video-qadu-scheduler.js +++ b/server/lib/request-video-qadu-scheduler.js | |||
@@ -44,14 +44,17 @@ module.exports = class RequestVideoQaduScheduler extends BaseRequestScheduler { | |||
44 | } | 44 | } |
45 | } | 45 | } |
46 | 46 | ||
47 | const videoData = {} | 47 | // Maybe another attribute was filled for this video |
48 | let videoData = requestsToMakeGrouped[hashKey].videos[video.id] | ||
49 | if (!videoData) videoData = {} | ||
50 | |||
48 | switch (request.type) { | 51 | switch (request.type) { |
49 | case constants.REQUEST_VIDEO_QADU_TYPES.LIKES: | 52 | case constants.REQUEST_VIDEO_QADU_TYPES.LIKES: |
50 | videoData.likes = video.likes | 53 | videoData.likes = video.likes |
51 | break | 54 | break |
52 | 55 | ||
53 | case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES: | 56 | case constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES: |
54 | videoData.likes = video.dislikes | 57 | videoData.dislikes = video.dislikes |
55 | break | 58 | break |
56 | 59 | ||
57 | case constants.REQUEST_VIDEO_QADU_TYPES.VIEWS: | 60 | case constants.REQUEST_VIDEO_QADU_TYPES.VIEWS: |
diff --git a/server/middlewares/validators/users.js b/server/middlewares/validators/users.js index 3089370ff..ce83fc074 100644 --- a/server/middlewares/validators/users.js +++ b/server/middlewares/validators/users.js | |||
@@ -7,7 +7,8 @@ const logger = require('../../helpers/logger') | |||
7 | const validatorsUsers = { | 7 | const validatorsUsers = { |
8 | usersAdd, | 8 | usersAdd, |
9 | usersRemove, | 9 | usersRemove, |
10 | usersUpdate | 10 | usersUpdate, |
11 | usersVideoRating | ||
11 | } | 12 | } |
12 | 13 | ||
13 | function usersAdd (req, res, next) { | 14 | function usersAdd (req, res, next) { |
@@ -62,6 +63,25 @@ function usersUpdate (req, res, next) { | |||
62 | checkErrors(req, res, next) | 63 | checkErrors(req, res, next) |
63 | } | 64 | } |
64 | 65 | ||
66 | function usersVideoRating (req, res, next) { | ||
67 | req.checkParams('videoId', 'Should have a valid video id').notEmpty().isUUID(4) | ||
68 | |||
69 | logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) | ||
70 | |||
71 | checkErrors(req, res, function () { | ||
72 | db.Video.load(req.params.videoId, function (err, video) { | ||
73 | if (err) { | ||
74 | logger.error('Error in user request validator.', { error: err }) | ||
75 | return res.sendStatus(500) | ||
76 | } | ||
77 | |||
78 | if (!video) return res.status(404).send('Video not found') | ||
79 | |||
80 | next() | ||
81 | }) | ||
82 | }) | ||
83 | } | ||
84 | |||
65 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
66 | 86 | ||
67 | module.exports = validatorsUsers | 87 | module.exports = validatorsUsers |
diff --git a/server/middlewares/validators/videos.js b/server/middlewares/validators/videos.js index 5c3f3ecf3..7dc79c56f 100644 --- a/server/middlewares/validators/videos.js +++ b/server/middlewares/validators/videos.js | |||
@@ -13,7 +13,9 @@ const validatorsVideos = { | |||
13 | videosRemove, | 13 | videosRemove, |
14 | videosSearch, | 14 | videosSearch, |
15 | 15 | ||
16 | videoAbuseReport | 16 | videoAbuseReport, |
17 | |||
18 | videoRate | ||
17 | } | 19 | } |
18 | 20 | ||
19 | function videosAdd (req, res, next) { | 21 | function videosAdd (req, res, next) { |
@@ -119,6 +121,17 @@ function videoAbuseReport (req, res, next) { | |||
119 | }) | 121 | }) |
120 | } | 122 | } |
121 | 123 | ||
124 | function videoRate (req, res, next) { | ||
125 | req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4) | ||
126 | req.checkBody('rating', 'Should have a valid rate type').isVideoRatingTypeValid() | ||
127 | |||
128 | logger.debug('Checking videoRate parameters', { parameters: req.body }) | ||
129 | |||
130 | checkErrors(req, res, function () { | ||
131 | checkVideoExists(req.params.id, res, next) | ||
132 | }) | ||
133 | } | ||
134 | |||
122 | // --------------------------------------------------------------------------- | 135 | // --------------------------------------------------------------------------- |
123 | 136 | ||
124 | module.exports = validatorsVideos | 137 | module.exports = validatorsVideos |
diff --git a/server/models/request-video-event.js b/server/models/request-video-event.js index ef3ebcb3a..9ebeaec90 100644 --- a/server/models/request-video-event.js +++ b/server/models/request-video-event.js | |||
@@ -83,6 +83,9 @@ function listWithLimitAndRandom (limitPods, limitRequestsPerPod, callback) { | |||
83 | if (podIds.length === 0) return callback(null, []) | 83 | if (podIds.length === 0) return callback(null, []) |
84 | 84 | ||
85 | const query = { | 85 | const query = { |
86 | order: [ | ||
87 | [ 'id', 'ASC' ] | ||
88 | ], | ||
86 | include: [ | 89 | include: [ |
87 | { | 90 | { |
88 | model: self.sequelize.models.Video, | 91 | 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 index 000000000..84007d70c --- /dev/null +++ b/server/models/user-video-rate.js | |||
@@ -0,0 +1,77 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | /* | ||
4 | User rates per video. | ||
5 | |||
6 | */ | ||
7 | |||
8 | const values = require('lodash/values') | ||
9 | |||
10 | const constants = require('../initializers/constants') | ||
11 | |||
12 | // --------------------------------------------------------------------------- | ||
13 | |||
14 | module.exports = function (sequelize, DataTypes) { | ||
15 | const UserVideoRate = sequelize.define('UserVideoRate', | ||
16 | { | ||
17 | type: { | ||
18 | type: DataTypes.ENUM(values(constants.VIDEO_RATE_TYPES)), | ||
19 | allowNull: false | ||
20 | } | ||
21 | }, | ||
22 | { | ||
23 | indexes: [ | ||
24 | { | ||
25 | fields: [ 'videoId', 'userId', 'type' ], | ||
26 | unique: true | ||
27 | } | ||
28 | ], | ||
29 | classMethods: { | ||
30 | associate, | ||
31 | |||
32 | load | ||
33 | } | ||
34 | } | ||
35 | ) | ||
36 | |||
37 | return UserVideoRate | ||
38 | } | ||
39 | |||
40 | // ------------------------------ STATICS ------------------------------ | ||
41 | |||
42 | function associate (models) { | ||
43 | this.belongsTo(models.Video, { | ||
44 | foreignKey: { | ||
45 | name: 'videoId', | ||
46 | allowNull: false | ||
47 | }, | ||
48 | onDelete: 'CASCADE' | ||
49 | }) | ||
50 | |||
51 | this.belongsTo(models.User, { | ||
52 | foreignKey: { | ||
53 | name: 'userId', | ||
54 | allowNull: false | ||
55 | }, | ||
56 | onDelete: 'CASCADE' | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | function load (userId, videoId, transaction, callback) { | ||
61 | if (!callback) { | ||
62 | callback = transaction | ||
63 | transaction = null | ||
64 | } | ||
65 | |||
66 | const query = { | ||
67 | where: { | ||
68 | userId, | ||
69 | videoId | ||
70 | } | ||
71 | } | ||
72 | |||
73 | const options = {} | ||
74 | if (transaction) options.transaction = transaction | ||
75 | |||
76 | return this.findOne(query, options).asCallback(callback) | ||
77 | } | ||
diff --git a/server/models/video.js b/server/models/video.js index fb46aca86..182555c85 100644 --- a/server/models/video.js +++ b/server/models/video.js | |||
@@ -89,6 +89,24 @@ module.exports = function (sequelize, DataTypes) { | |||
89 | min: 0, | 89 | min: 0, |
90 | isInt: true | 90 | isInt: true |
91 | } | 91 | } |
92 | }, | ||
93 | likes: { | ||
94 | type: DataTypes.INTEGER, | ||
95 | allowNull: false, | ||
96 | defaultValue: 0, | ||
97 | validate: { | ||
98 | min: 0, | ||
99 | isInt: true | ||
100 | } | ||
101 | }, | ||
102 | dislikes: { | ||
103 | type: DataTypes.INTEGER, | ||
104 | allowNull: false, | ||
105 | defaultValue: 0, | ||
106 | validate: { | ||
107 | min: 0, | ||
108 | isInt: true | ||
109 | } | ||
92 | } | 110 | } |
93 | }, | 111 | }, |
94 | { | 112 | { |
@@ -113,6 +131,9 @@ module.exports = function (sequelize, DataTypes) { | |||
113 | }, | 131 | }, |
114 | { | 132 | { |
115 | fields: [ 'views' ] | 133 | fields: [ 'views' ] |
134 | }, | ||
135 | { | ||
136 | fields: [ 'likes' ] | ||
116 | } | 137 | } |
117 | ], | 138 | ], |
118 | classMethods: { | 139 | classMethods: { |
@@ -349,6 +370,8 @@ function toFormatedJSON () { | |||
349 | author: this.Author.name, | 370 | author: this.Author.name, |
350 | duration: this.duration, | 371 | duration: this.duration, |
351 | views: this.views, | 372 | views: this.views, |
373 | likes: this.likes, | ||
374 | dislikes: this.dislikes, | ||
352 | tags: map(this.Tags, 'name'), | 375 | tags: map(this.Tags, 'name'), |
353 | thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), | 376 | thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), |
354 | createdAt: this.createdAt, | 377 | createdAt: this.createdAt, |
@@ -381,7 +404,9 @@ function toAddRemoteJSON (callback) { | |||
381 | createdAt: self.createdAt, | 404 | createdAt: self.createdAt, |
382 | updatedAt: self.updatedAt, | 405 | updatedAt: self.updatedAt, |
383 | extname: self.extname, | 406 | extname: self.extname, |
384 | views: self.views | 407 | views: self.views, |
408 | likes: self.likes, | ||
409 | dislikes: self.dislikes | ||
385 | } | 410 | } |
386 | 411 | ||
387 | return callback(null, remoteVideo) | 412 | return callback(null, remoteVideo) |
@@ -400,7 +425,9 @@ function toUpdateRemoteJSON (callback) { | |||
400 | createdAt: this.createdAt, | 425 | createdAt: this.createdAt, |
401 | updatedAt: this.updatedAt, | 426 | updatedAt: this.updatedAt, |
402 | extname: this.extname, | 427 | extname: this.extname, |
403 | views: this.views | 428 | views: this.views, |
429 | likes: this.likes, | ||
430 | dislikes: this.dislikes | ||
404 | } | 431 | } |
405 | 432 | ||
406 | return json | 433 | return json |
diff --git a/server/tests/api/check-params/users.js b/server/tests/api/check-params/users.js index 6edb54660..11e2bada4 100644 --- a/server/tests/api/check-params/users.js +++ b/server/tests/api/check-params/users.js | |||
@@ -9,11 +9,13 @@ const loginUtils = require('../../utils/login') | |||
9 | const requestsUtils = require('../../utils/requests') | 9 | const requestsUtils = require('../../utils/requests') |
10 | const serversUtils = require('../../utils/servers') | 10 | const serversUtils = require('../../utils/servers') |
11 | const usersUtils = require('../../utils/users') | 11 | const usersUtils = require('../../utils/users') |
12 | const videosUtils = require('../../utils/videos') | ||
12 | 13 | ||
13 | describe('Test users API validators', function () { | 14 | describe('Test users API validators', function () { |
14 | const path = '/api/v1/users/' | 15 | const path = '/api/v1/users/' |
15 | let userId = null | 16 | let userId = null |
16 | let rootId = null | 17 | let rootId = null |
18 | let videoId = null | ||
17 | let server = null | 19 | let server = null |
18 | let userAccessToken = null | 20 | let userAccessToken = null |
19 | 21 | ||
@@ -48,6 +50,23 @@ describe('Test users API validators', function () { | |||
48 | usersUtils.createUser(server.url, server.accessToken, username, password, next) | 50 | usersUtils.createUser(server.url, server.accessToken, username, password, next) |
49 | }, | 51 | }, |
50 | function (next) { | 52 | function (next) { |
53 | const name = 'my super name for pod' | ||
54 | const description = 'my super description for pod' | ||
55 | const tags = [ 'tag' ] | ||
56 | const file = 'video_short2.webm' | ||
57 | videosUtils.uploadVideo(server.url, server.accessToken, name, description, tags, file, next) | ||
58 | }, | ||
59 | function (next) { | ||
60 | videosUtils.getVideosList(server.url, function (err, res) { | ||
61 | if (err) throw err | ||
62 | |||
63 | const videos = res.body.data | ||
64 | videoId = videos[0].id | ||
65 | |||
66 | next() | ||
67 | }) | ||
68 | }, | ||
69 | function (next) { | ||
51 | const user = { | 70 | const user = { |
52 | username: 'user1', | 71 | username: 'user1', |
53 | password: 'my super password' | 72 | password: 'my super password' |
@@ -289,6 +308,63 @@ describe('Test users API validators', function () { | |||
289 | }) | 308 | }) |
290 | }) | 309 | }) |
291 | 310 | ||
311 | describe('When getting my video rating', function () { | ||
312 | it('Should fail with a non authenticated user', function (done) { | ||
313 | request(server.url) | ||
314 | .get(path + 'me/videos/' + videoId + '/rating') | ||
315 | .set('Authorization', 'Bearer faketoken') | ||
316 | .set('Accept', 'application/json') | ||
317 | .expect(401, done) | ||
318 | }) | ||
319 | |||
320 | it('Should fail with an incorrect video uuid', function (done) { | ||
321 | request(server.url) | ||
322 | .get(path + 'me/videos/blabla/rating') | ||
323 | .set('Authorization', 'Bearer ' + userAccessToken) | ||
324 | .set('Accept', 'application/json') | ||
325 | .expect(400, done) | ||
326 | }) | ||
327 | |||
328 | it('Should fail with an unknown video', function (done) { | ||
329 | request(server.url) | ||
330 | .get(path + 'me/videos/4da6fde3-88f7-4d16-b119-108df5630b06/rating') | ||
331 | .set('Authorization', 'Bearer ' + userAccessToken) | ||
332 | .set('Accept', 'application/json') | ||
333 | .expect(404, done) | ||
334 | }) | ||
335 | |||
336 | it('Should success with the correct parameters', function (done) { | ||
337 | request(server.url) | ||
338 | .get(path + 'me/videos/' + videoId + '/rating') | ||
339 | .set('Authorization', 'Bearer ' + userAccessToken) | ||
340 | .set('Accept', 'application/json') | ||
341 | .expect(200, done) | ||
342 | }) | ||
343 | }) | ||
344 | |||
345 | describe('When removing an user', function () { | ||
346 | it('Should fail with an incorrect id', function (done) { | ||
347 | request(server.url) | ||
348 | .delete(path + 'bla-bla') | ||
349 | .set('Authorization', 'Bearer ' + server.accessToken) | ||
350 | .expect(400, done) | ||
351 | }) | ||
352 | |||
353 | it('Should fail with the root user', function (done) { | ||
354 | request(server.url) | ||
355 | .delete(path + rootId) | ||
356 | .set('Authorization', 'Bearer ' + server.accessToken) | ||
357 | .expect(400, done) | ||
358 | }) | ||
359 | |||
360 | it('Should return 404 with a non existing id', function (done) { | ||
361 | request(server.url) | ||
362 | .delete(path + '45') | ||
363 | .set('Authorization', 'Bearer ' + server.accessToken) | ||
364 | .expect(404, done) | ||
365 | }) | ||
366 | }) | ||
367 | |||
292 | describe('When removing an user', function () { | 368 | describe('When removing an user', function () { |
293 | it('Should fail with an incorrect id', function (done) { | 369 | it('Should fail with an incorrect id', function (done) { |
294 | request(server.url) | 370 | request(server.url) |
diff --git a/server/tests/api/check-params/videos.js b/server/tests/api/check-params/videos.js index f8549a95b..0f5f40b8e 100644 --- a/server/tests/api/check-params/videos.js +++ b/server/tests/api/check-params/videos.js | |||
@@ -420,6 +420,48 @@ describe('Test videos API validator', function () { | |||
420 | it('Should succeed with the correct parameters') | 420 | it('Should succeed with the correct parameters') |
421 | }) | 421 | }) |
422 | 422 | ||
423 | describe('When rating a video', function () { | ||
424 | let videoId | ||
425 | |||
426 | before(function (done) { | ||
427 | videosUtils.getVideosList(server.url, function (err, res) { | ||
428 | if (err) throw err | ||
429 | |||
430 | videoId = res.body.data[0].id | ||
431 | |||
432 | return done() | ||
433 | }) | ||
434 | }) | ||
435 | |||
436 | it('Should fail without a valid uuid', function (done) { | ||
437 | const data = { | ||
438 | rating: 'like' | ||
439 | } | ||
440 | requestsUtils.makePutBodyRequest(server.url, path + 'blabla/rate', server.accessToken, data, done) | ||
441 | }) | ||
442 | |||
443 | it('Should fail with an unknown id', function (done) { | ||
444 | const data = { | ||
445 | rating: 'like' | ||
446 | } | ||
447 | requestsUtils.makePutBodyRequest(server.url, path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', server.accessToken, data, done, 404) | ||
448 | }) | ||
449 | |||
450 | it('Should fail with a wrong rating', function (done) { | ||
451 | const data = { | ||
452 | rating: 'likes' | ||
453 | } | ||
454 | requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done) | ||
455 | }) | ||
456 | |||
457 | it('Should succeed with the correct parameters', function (done) { | ||
458 | const data = { | ||
459 | rating: 'like' | ||
460 | } | ||
461 | requestsUtils.makePutBodyRequest(server.url, path + videoId + '/rate', server.accessToken, data, done, 204) | ||
462 | }) | ||
463 | }) | ||
464 | |||
423 | describe('When removing a video', function () { | 465 | describe('When removing a video', function () { |
424 | it('Should have 404 with nothing', function (done) { | 466 | it('Should have 404 with nothing', function (done) { |
425 | request(server.url) | 467 | request(server.url) |
diff --git a/server/tests/api/multiple-pods.js b/server/tests/api/multiple-pods.js index e02b6180b..552f10c6f 100644 --- a/server/tests/api/multiple-pods.js +++ b/server/tests/api/multiple-pods.js | |||
@@ -4,6 +4,7 @@ | |||
4 | 4 | ||
5 | const chai = require('chai') | 5 | const chai = require('chai') |
6 | const each = require('async/each') | 6 | const each = require('async/each') |
7 | const eachSeries = require('async/eachSeries') | ||
7 | const expect = chai.expect | 8 | const expect = chai.expect |
8 | const parallel = require('async/parallel') | 9 | const parallel = require('async/parallel') |
9 | const series = require('async/series') | 10 | const series = require('async/series') |
@@ -378,7 +379,7 @@ describe('Test multiple pods', function () { | |||
378 | }) | 379 | }) |
379 | }) | 380 | }) |
380 | 381 | ||
381 | describe('Should update video views', function () { | 382 | describe('Should update video views, likes and dislikes', function () { |
382 | let localVideosPod3 = [] | 383 | let localVideosPod3 = [] |
383 | let remoteVideosPod1 = [] | 384 | let remoteVideosPod1 = [] |
384 | let remoteVideosPod2 = [] | 385 | let remoteVideosPod2 = [] |
@@ -419,7 +420,7 @@ describe('Test multiple pods', function () { | |||
419 | ], done) | 420 | ], done) |
420 | }) | 421 | }) |
421 | 422 | ||
422 | it('Should views multiple videos on owned servers', function (done) { | 423 | it('Should view multiple videos on owned servers', function (done) { |
423 | this.timeout(30000) | 424 | this.timeout(30000) |
424 | 425 | ||
425 | parallel([ | 426 | parallel([ |
@@ -440,18 +441,18 @@ describe('Test multiple pods', function () { | |||
440 | }, | 441 | }, |
441 | 442 | ||
442 | function (callback) { | 443 | function (callback) { |
443 | setTimeout(done, 22000) | 444 | setTimeout(callback, 22000) |
444 | } | 445 | } |
445 | ], function (err) { | 446 | ], function (err) { |
446 | if (err) throw err | 447 | if (err) throw err |
447 | 448 | ||
448 | each(servers, function (server, callback) { | 449 | eachSeries(servers, function (server, callback) { |
449 | videosUtils.getVideosList(server.url, function (err, res) { | 450 | videosUtils.getVideosList(server.url, function (err, res) { |
450 | if (err) throw err | 451 | if (err) throw err |
451 | 452 | ||
452 | const videos = res.body.data | 453 | const videos = res.body.data |
453 | expect(videos.find(video => video.views === 3)).to.be.exist | 454 | expect(videos.find(video => video.views === 3)).to.exist |
454 | expect(videos.find(video => video.views === 1)).to.be.exist | 455 | expect(videos.find(video => video.views === 1)).to.exist |
455 | 456 | ||
456 | callback() | 457 | callback() |
457 | }) | 458 | }) |
@@ -459,7 +460,7 @@ describe('Test multiple pods', function () { | |||
459 | }) | 460 | }) |
460 | }) | 461 | }) |
461 | 462 | ||
462 | it('Should views multiple videos on each servers', function (done) { | 463 | it('Should view multiple videos on each servers', function (done) { |
463 | this.timeout(30000) | 464 | this.timeout(30000) |
464 | 465 | ||
465 | parallel([ | 466 | parallel([ |
@@ -504,17 +505,17 @@ describe('Test multiple pods', function () { | |||
504 | }, | 505 | }, |
505 | 506 | ||
506 | function (callback) { | 507 | function (callback) { |
507 | setTimeout(done, 22000) | 508 | setTimeout(callback, 22000) |
508 | } | 509 | } |
509 | ], function (err) { | 510 | ], function (err) { |
510 | if (err) throw err | 511 | if (err) throw err |
511 | 512 | ||
512 | let baseVideos = null | 513 | let baseVideos = null |
513 | each(servers, function (server, callback) { | 514 | eachSeries(servers, function (server, callback) { |
514 | videosUtils.getVideosList(server.url, function (err, res) { | 515 | videosUtils.getVideosList(server.url, function (err, res) { |
515 | if (err) throw err | 516 | if (err) throw err |
516 | 517 | ||
517 | const videos = res.body | 518 | const videos = res.body.data |
518 | 519 | ||
519 | // Initialize base videos for future comparisons | 520 | // Initialize base videos for future comparisons |
520 | if (baseVideos === null) { | 521 | if (baseVideos === null) { |
@@ -522,10 +523,74 @@ describe('Test multiple pods', function () { | |||
522 | return callback() | 523 | return callback() |
523 | } | 524 | } |
524 | 525 | ||
525 | for (let i = 0; i < baseVideos.length; i++) { | 526 | baseVideos.forEach(baseVideo => { |
526 | expect(baseVideos[i].views).to.equal(videos[i].views) | 527 | const sameVideo = videos.find(video => video.name === baseVideo.name) |
528 | expect(baseVideo.views).to.equal(sameVideo.views) | ||
529 | }) | ||
530 | |||
531 | callback() | ||
532 | }) | ||
533 | }, done) | ||
534 | }) | ||
535 | }) | ||
536 | |||
537 | it('Should like and dislikes videos on different services', function (done) { | ||
538 | this.timeout(30000) | ||
539 | |||
540 | parallel([ | ||
541 | function (callback) { | ||
542 | videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback) | ||
543 | }, | ||
544 | |||
545 | function (callback) { | ||
546 | videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'dislike', callback) | ||
547 | }, | ||
548 | |||
549 | function (callback) { | ||
550 | videosUtils.rateVideo(servers[0].url, servers[0].accessToken, remoteVideosPod1[0], 'like', callback) | ||
551 | }, | ||
552 | |||
553 | function (callback) { | ||
554 | videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'like', callback) | ||
555 | }, | ||
556 | |||
557 | function (callback) { | ||
558 | videosUtils.rateVideo(servers[2].url, servers[2].accessToken, localVideosPod3[1], 'dislike', callback) | ||
559 | }, | ||
560 | |||
561 | function (callback) { | ||
562 | videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[1], 'dislike', callback) | ||
563 | }, | ||
564 | |||
565 | function (callback) { | ||
566 | videosUtils.rateVideo(servers[2].url, servers[2].accessToken, remoteVideosPod3[0], 'like', callback) | ||
567 | }, | ||
568 | |||
569 | function (callback) { | ||
570 | setTimeout(callback, 22000) | ||
571 | } | ||
572 | ], function (err) { | ||
573 | if (err) throw err | ||
574 | |||
575 | let baseVideos = null | ||
576 | eachSeries(servers, function (server, callback) { | ||
577 | videosUtils.getVideosList(server.url, function (err, res) { | ||
578 | if (err) throw err | ||
579 | |||
580 | const videos = res.body.data | ||
581 | |||
582 | // Initialize base videos for future comparisons | ||
583 | if (baseVideos === null) { | ||
584 | baseVideos = videos | ||
585 | return callback() | ||
527 | } | 586 | } |
528 | 587 | ||
588 | baseVideos.forEach(baseVideo => { | ||
589 | const sameVideo = videos.find(video => video.name === baseVideo.name) | ||
590 | expect(baseVideo.likes).to.equal(sameVideo.likes) | ||
591 | expect(baseVideo.dislikes).to.equal(sameVideo.dislikes) | ||
592 | }) | ||
593 | |||
529 | callback() | 594 | callback() |
530 | }) | 595 | }) |
531 | }, done) | 596 | }, done) |
diff --git a/server/tests/api/single-pod.js b/server/tests/api/single-pod.js index 87d0e9a71..96e4aff9e 100644 --- a/server/tests/api/single-pod.js +++ b/server/tests/api/single-pod.js | |||
@@ -609,6 +609,40 @@ describe('Test a single pod', function () { | |||
609 | }) | 609 | }) |
610 | }) | 610 | }) |
611 | 611 | ||
612 | it('Should like a video', function (done) { | ||
613 | videosUtils.rateVideo(server.url, server.accessToken, videoId, 'like', function (err) { | ||
614 | if (err) throw err | ||
615 | |||
616 | videosUtils.getVideo(server.url, videoId, function (err, res) { | ||
617 | if (err) throw err | ||
618 | |||
619 | const video = res.body | ||
620 | |||
621 | expect(video.likes).to.equal(1) | ||
622 | expect(video.dislikes).to.equal(0) | ||
623 | |||
624 | done() | ||
625 | }) | ||
626 | }) | ||
627 | }) | ||
628 | |||
629 | it('Should dislike the same video', function (done) { | ||
630 | videosUtils.rateVideo(server.url, server.accessToken, videoId, 'dislike', function (err) { | ||
631 | if (err) throw err | ||
632 | |||
633 | videosUtils.getVideo(server.url, videoId, function (err, res) { | ||
634 | if (err) throw err | ||
635 | |||
636 | const video = res.body | ||
637 | |||
638 | expect(video.likes).to.equal(0) | ||
639 | expect(video.dislikes).to.equal(1) | ||
640 | |||
641 | done() | ||
642 | }) | ||
643 | }) | ||
644 | }) | ||
645 | |||
612 | after(function (done) { | 646 | after(function (done) { |
613 | process.kill(-server.app.pid) | 647 | process.kill(-server.app.pid) |
614 | 648 | ||
diff --git a/server/tests/api/users.js b/server/tests/api/users.js index bd95e78c2..f9568b874 100644 --- a/server/tests/api/users.js +++ b/server/tests/api/users.js | |||
@@ -10,6 +10,7 @@ const loginUtils = require('../utils/login') | |||
10 | const podsUtils = require('../utils/pods') | 10 | const podsUtils = require('../utils/pods') |
11 | const serversUtils = require('../utils/servers') | 11 | const serversUtils = require('../utils/servers') |
12 | const usersUtils = require('../utils/users') | 12 | const usersUtils = require('../utils/users') |
13 | const requestsUtils = require('../utils/requests') | ||
13 | const videosUtils = require('../utils/videos') | 14 | const videosUtils = require('../utils/videos') |
14 | 15 | ||
15 | describe('Test users', function () { | 16 | describe('Test users', function () { |
@@ -138,6 +139,23 @@ describe('Test users', function () { | |||
138 | videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) | 139 | videosUtils.uploadVideo(server.url, accessToken, name, description, tags, video, 204, done) |
139 | }) | 140 | }) |
140 | 141 | ||
142 | it('Should retrieve a video rating', function (done) { | ||
143 | videosUtils.rateVideo(server.url, accessToken, videoId, 'like', function (err) { | ||
144 | if (err) throw err | ||
145 | |||
146 | usersUtils.getUserVideoRating(server.url, accessToken, videoId, function (err, res) { | ||
147 | if (err) throw err | ||
148 | |||
149 | const rating = res.body | ||
150 | |||
151 | expect(rating.videoId).to.equal(videoId) | ||
152 | expect(rating.rating).to.equal('like') | ||
153 | |||
154 | done() | ||
155 | }) | ||
156 | }) | ||
157 | }) | ||
158 | |||
141 | it('Should not be able to remove the video with an incorrect token', function (done) { | 159 | it('Should not be able to remove the video with an incorrect token', function (done) { |
142 | videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done) | 160 | videosUtils.removeVideo(server.url, 'bad_token', videoId, 401, done) |
143 | }) | 161 | }) |
@@ -150,10 +168,21 @@ describe('Test users', function () { | |||
150 | 168 | ||
151 | it('Should logout (revoke token)') | 169 | it('Should logout (revoke token)') |
152 | 170 | ||
171 | it('Should not be able to get the user informations') | ||
172 | |||
153 | it('Should not be able to upload a video') | 173 | it('Should not be able to upload a video') |
154 | 174 | ||
155 | it('Should not be able to remove a video') | 175 | it('Should not be able to remove a video') |
156 | 176 | ||
177 | it('Should not be able to rate a video', function (done) { | ||
178 | const path = '/api/v1/videos/' | ||
179 | const data = { | ||
180 | rating: 'likes' | ||
181 | } | ||
182 | |||
183 | requestsUtils.makePutBodyRequest(server.url, path + videoId, 'wrong token', data, done, 401) | ||
184 | }) | ||
185 | |||
157 | it('Should be able to login again') | 186 | it('Should be able to login again') |
158 | 187 | ||
159 | it('Should have an expired access token') | 188 | it('Should have an expired access token') |
diff --git a/server/tests/utils/users.js b/server/tests/utils/users.js index a2c010f64..7817160b9 100644 --- a/server/tests/utils/users.js +++ b/server/tests/utils/users.js | |||
@@ -5,6 +5,7 @@ const request = require('supertest') | |||
5 | const usersUtils = { | 5 | const usersUtils = { |
6 | createUser, | 6 | createUser, |
7 | getUserInformation, | 7 | getUserInformation, |
8 | getUserVideoRating, | ||
8 | getUsersList, | 9 | getUsersList, |
9 | getUsersListPaginationAndSort, | 10 | getUsersListPaginationAndSort, |
10 | removeUser, | 11 | removeUser, |
@@ -47,6 +48,18 @@ function getUserInformation (url, accessToken, end) { | |||
47 | .end(end) | 48 | .end(end) |
48 | } | 49 | } |
49 | 50 | ||
51 | function getUserVideoRating (url, accessToken, videoId, end) { | ||
52 | const path = '/api/v1/users/me/videos/' + videoId + '/rating' | ||
53 | |||
54 | request(url) | ||
55 | .get(path) | ||
56 | .set('Accept', 'application/json') | ||
57 | .set('Authorization', 'Bearer ' + accessToken) | ||
58 | .expect(200) | ||
59 | .expect('Content-Type', /json/) | ||
60 | .end(end) | ||
61 | } | ||
62 | |||
50 | function getUsersList (url, end) { | 63 | function getUsersList (url, end) { |
51 | const path = '/api/v1/users' | 64 | const path = '/api/v1/users' |
52 | 65 | ||
diff --git a/server/tests/utils/videos.js b/server/tests/utils/videos.js index f94368437..177426076 100644 --- a/server/tests/utils/videos.js +++ b/server/tests/utils/videos.js | |||
@@ -16,7 +16,8 @@ const videosUtils = { | |||
16 | searchVideoWithSort, | 16 | searchVideoWithSort, |
17 | testVideoImage, | 17 | testVideoImage, |
18 | uploadVideo, | 18 | uploadVideo, |
19 | updateVideo | 19 | updateVideo, |
20 | rateVideo | ||
20 | } | 21 | } |
21 | 22 | ||
22 | // ---------------------- Export functions -------------------- | 23 | // ---------------------- Export functions -------------------- |
@@ -236,6 +237,23 @@ function updateVideo (url, accessToken, id, name, description, tags, specialStat | |||
236 | req.expect(specialStatus).end(end) | 237 | req.expect(specialStatus).end(end) |
237 | } | 238 | } |
238 | 239 | ||
240 | function rateVideo (url, accessToken, id, rating, specialStatus, end) { | ||
241 | if (!end) { | ||
242 | end = specialStatus | ||
243 | specialStatus = 204 | ||
244 | } | ||
245 | |||
246 | const path = '/api/v1/videos/' + id + '/rate' | ||
247 | |||
248 | request(url) | ||
249 | .put(path) | ||
250 | .set('Accept', 'application/json') | ||
251 | .set('Authorization', 'Bearer ' + accessToken) | ||
252 | .send({ rating }) | ||
253 | .expect(specialStatus) | ||
254 | .end(end) | ||
255 | } | ||
256 | |||
239 | // --------------------------------------------------------------------------- | 257 | // --------------------------------------------------------------------------- |
240 | 258 | ||
241 | module.exports = videosUtils | 259 | module.exports = videosUtils |