Add video recomandation by tags (#1001)

* Recommendation by tags (thx bradsk88)

Thx bradsk88 for the help.

* Prefer jest-preset-angular to skip need for babel config

* Fix jest
This commit is contained in:
Jorropo 2018-09-04 11:01:54 +02:00 committed by Chocobozzz
parent 5cf84858d4
commit b0c36821d1
12 changed files with 1068 additions and 72 deletions

View File

@ -21,7 +21,7 @@
"webpack-bundle-analyzer": "webpack-bundle-analyzer",
"webdriver-manager": "webdriver-manager",
"ngx-extractor": "ngx-extractor",
"test": "jest"
"test": "jest --no-cache"
},
"license": "GPLv3",
"typings": "*.d.ts",
@ -31,13 +31,23 @@
"simple-get": "^2.8.1"
},
"jest": {
"globals": {
"ts-jest": {
"tsConfigFile": "<rootDir>/src/tsconfig.spec.json"
},
"__TRANSFORM_HTML__": true
},
"transform": {
"^.+\\.tsx?$": "ts-jest"
"^.+\\.(ts|js|html)$": "<rootDir>/node_modules/jest-preset-angular/preprocessor.js"
},
"moduleNameMapper": {
"^@app/(.*)": "<rootDir>/src/app/$1"
"^@app/(.*)": "<rootDir>/src/app/$1",
"environments/(.*)": "<rootDir>/src/environments/$1"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testMatch": [
"**/__tests__/**/*.+(ts|js)?(x)",
"**/+(*.)+(spec|test).+(ts|js)?(x)"
],
"moduleFileExtensions": [
"ts",
"tsx",
@ -45,7 +55,12 @@
"jsx",
"json",
"node"
]
],
"transformIgnorePatterns": [
"<rootDir>/node_modules/(?!lodash-es/)"
],
"preset": "jest-preset-angular",
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.7.5",
@ -99,6 +114,7 @@
"jasmine-core": "^3.1.0",
"jasmine-spec-reporter": "^4.2.1",
"jest": "^23.5.0",
"jest-preset-angular": "^6.0.0",
"jschannel": "^1.0.2",
"karma": "^3.0.0",
"karma-chrome-launcher": "^2.2.0",

View File

@ -0,0 +1,4 @@
export interface RecommendationInfo {
uuid: string
tags?: string[]
}

View File

@ -14,7 +14,8 @@
</div>
<div class="form-group">
<label i18n class="label-tags">Tags</label> <span i18n>(press Enter to add)</span>
<label i18n class="label-tags">Tags</label>
<my-help i18n-preHtml preHtml="Tags could be used to suggest relevant recommendations.</br>Press Enter to add a new tag."></my-help>
<tag-input
[validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
formControlName="tags" maxItems="5" modelAsStrings="true"

View File

@ -200,8 +200,7 @@
<my-video-comments [video]="video" [user]="user"></my-video-comments>
</div>
<my-recommended-videos class="col-12 col-lg-3"
[inputVideo]="video" [user]="user"></my-recommended-videos>
</div>
[inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" [user]="user"></my-recommended-videos>
</div>
<div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">

View File

@ -21,7 +21,7 @@ describe('"Recent Videos" Recommender', () => {
{ uuid: 'uuid2' }
]
getVideosMock.mockReturnValueOnce(of({ videos: vids }))
const result = await service.getRecommendations('uuid1').toPromise()
const result = await service.getRecommendations({ uuid: 'uuid1' }).toPromise()
const uuids = result.map(v => v.uuid)
expect(uuids).toEqual(['uuid2'])
done()
@ -36,7 +36,7 @@ describe('"Recent Videos" Recommender', () => {
{ uuid: 'uuid7' }
]
getVideosMock.mockReturnValueOnce(of({ videos: vids }))
const result = await service.getRecommendations('uuid1').toPromise()
const result = await service.getRecommendations({ uuid: 'uuid1' }).toPromise()
expect(result.length).toEqual(5)
done()
})
@ -51,12 +51,12 @@ describe('"Recent Videos" Recommender', () => {
]
getVideosMock
.mockReturnValueOnce(of({ videos: vids }))
const result = await service.getRecommendations('uuid1').toPromise()
const result = await service.getRecommendations({ uuid: 'uuid1' }).toPromise()
expect(result.length).toEqual(5)
done()
})
it('should fetch an extra result in case the given UUID is in the list', async (done) => {
await service.getRecommendations('uuid1').toPromise()
await service.getRecommendations({ uuid: 'uuid1' }).toPromise()
let expectedSize = service.pageSize + 1
let params = { currentPage: jasmine.anything(), itemsPerPage: expectedSize }
expect(getVideosMock).toHaveBeenCalledWith(params, jasmine.anything())

View File

@ -1,9 +1,12 @@
import { Inject, Injectable } from '@angular/core'
import { RecommendationService } from '@app/videos/recommendations/recommendations.service'
import { Video } from '@app/shared/video/video.model'
import { VideoService, VideosProvider } from '@app/shared/video/video.service'
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
import { VideoService } from '@app/shared/video/video.service'
import { map } from 'rxjs/operators'
import { Observable } from 'rxjs'
import { SearchService } from '@app/search/search.service'
import { AdvancedSearch } from '@app/search/advanced-search.model'
/**
* Provides "recommendations" by providing the most recently uploaded videos.
@ -14,26 +17,40 @@ export class RecentVideosRecommendationService implements RecommendationService
readonly pageSize = 5
constructor (
@Inject(VideoService) private videos: VideosProvider
private videos: VideoService,
private searchService: SearchService
) {
}
getRecommendations (uuid: string): Observable<Video[]> {
return this.fetchPage(1)
getRecommendations (recommendation: RecommendationInfo): Observable<Video[]> {
return this.fetchPage(1, recommendation)
.pipe(
map(vids => {
const otherVideos = vids.filter(v => v.uuid !== uuid)
const otherVideos = vids.filter(v => v.uuid !== recommendation.uuid)
return otherVideos.slice(0, this.pageSize)
})
)
}
private fetchPage (page: number): Observable<Video[]> {
private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
let pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
if (!recommendation.tags) {
return this.videos.getVideos(pagination, '-createdAt')
.pipe(
map(v => v.videos)
)
}
if (recommendation.tags.length === 0) {
return this.videos.getVideos(pagination, '-createdAt')
.pipe(
map(v => v.videos)
)
}
return this.searchService.searchVideos('',
pagination,
new AdvancedSearch({ tagsOneOf: recommendation.tags.join(','), sort: '-createdAt' })
).pipe(
map(v => v.videos)
)
}
}

View File

@ -1,8 +1,9 @@
import { Video } from '@app/shared/video/video.model'
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
import { Observable } from 'rxjs'
export type UUID = string
export interface RecommendationService {
getRecommendations (uuid: UUID): Observable<Video[]>
getRecommendations (recommendation: RecommendationInfo): Observable<Video[]>
}

View File

@ -1,6 +1,7 @@
import { Component, Input, OnChanges } from '@angular/core'
import { Observable } from 'rxjs'
import { Video } from '@app/shared/video/video.model'
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-videos.store'
import { User } from '@app/shared'
@ -9,7 +10,7 @@ import { User } from '@app/shared'
templateUrl: './recommended-videos.component.html'
})
export class RecommendedVideosComponent implements OnChanges {
@Input() inputVideo: Video
@Input() inputRecommendation: RecommendationInfo
@Input() user: User
readonly hasVideos$: Observable<boolean>
@ -23,8 +24,8 @@ export class RecommendedVideosComponent implements OnChanges {
}
public ngOnChanges (): void {
if (this.inputVideo) {
this.store.requestNewRecommendations(this.inputVideo.uuid)
if (this.inputRecommendation) {
this.store.requestNewRecommendations(this.inputRecommendation)
}
}

View File

@ -1,6 +1,7 @@
import { Inject, Injectable } from '@angular/core'
import { Observable, ReplaySubject } from 'rxjs'
import { Video } from '@app/shared/video/video.model'
import { RecommendationInfo } from '@app/shared/video/recommendation-info.model'
import { RecentVideosRecommendationService } from '@app/videos/recommendations/recent-videos-recommendation.service'
import { RecommendationService, UUID } from '@app/videos/recommendations/recommendations.service'
import { map, switchMap, take } from 'rxjs/operators'
@ -12,13 +13,13 @@ import { map, switchMap, take } from 'rxjs/operators'
export class RecommendedVideosStore {
public readonly recommendations$: Observable<Video[]>
public readonly hasRecommendations$: Observable<boolean>
private readonly requestsForLoad$$ = new ReplaySubject<UUID>(1)
private readonly requestsForLoad$$ = new ReplaySubject<RecommendationInfo>(1)
constructor (
@Inject(RecentVideosRecommendationService) private recommendations: RecommendationService
) {
this.recommendations$ = this.requestsForLoad$$.pipe(
switchMap(requestedUUID => recommendations.getRecommendations(requestedUUID)
switchMap(requestedRecommendation => recommendations.getRecommendations(requestedRecommendation)
.pipe(take(1))
))
this.hasRecommendations$ = this.recommendations$.pipe(
@ -26,7 +27,7 @@ export class RecommendedVideosStore {
)
}
requestNewRecommendations (videoUUID: string) {
this.requestsForLoad$$.next(videoUUID)
requestNewRecommendations (recommend: RecommendationInfo) {
this.requestsForLoad$$.next(recommend)
}
}

1
client/src/setupJest.ts Normal file
View File

@ -0,0 +1 @@
import 'jest-preset-angular';

View File

@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"module": "commonjs",
"target": "es5",
"baseUrl": "",
"allowJs": true
},
"files": [
"test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

File diff suppressed because it is too large Load Diff