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:
parent
5cf84858d4
commit
b0c36821d1
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export interface RecommendationInfo {
|
||||
uuid: string
|
||||
tags?: string[]
|
||||
}
|
|
@ -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"
|
||||
|
@ -223,4 +224,4 @@
|
|||
|
||||
<my-video-caption-add-modal
|
||||
#videoCaptionAddModal [existingCaptions]="existingCaptions" (captionAdded)="onCaptionAdded($event)"
|
||||
></my-video-caption-add-modal>
|
||||
></my-video-caption-add-modal>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 }
|
||||
return this.videos.getVideos(pagination, '-createdAt')
|
||||
.pipe(
|
||||
map(v => v.videos)
|
||||
)
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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[]>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import 'jest-preset-angular';
|
|
@ -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"
|
||||
]
|
||||
}
|
1018
client/yarn.lock
1018
client/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue