Merge branch 'master' into resolvers-branch-angular6

Conflicts:
	package.json
	src/app/+search-page/search-filters/search-filters.component.ts
	src/app/core/auth/auth.effects.ts
	src/app/core/auth/auth.service.ts
	src/app/core/auth/server-auth.service.ts
	src/app/core/data/comcol-data.service.ts
	src/app/core/data/community-data.service.ts
	src/app/core/data/data.service.ts
	src/app/core/data/item-data.service.ts
	src/app/core/shared/dspace-object.model.ts
	src/app/header/header.component.spec.ts
	src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
	src/app/shared/testing/auth-service-stub.ts
	yarn.lock
This commit is contained in:
lotte
2018-10-08 12:28:08 +02:00
78 changed files with 2011 additions and 265 deletions

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-author w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '&quot;' + value + '&quot;': ''} }}"
[objects$]="(items$ !== undefined)? items$ : authors$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../../core/shared/metadatum.model';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-author-page',
styleUrls: ['./browse-by-author-page.component.scss'],
templateUrl: './browse-by-author-page.component.html'
})
/**
* Component for browsing (items) by author (dc.contributor.author)
*/
export class BrowseByAuthorPageComponent implements OnInit {
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-author-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
value = '';
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
this.value = +params.value || params.value || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
const searchOptions = {
pagination: pagination,
sort: sort
};
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
this.items$ = undefined;
}
/**
* Updates the current page with searchOptions and display items linked to author
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @param author The author's name for displaying items
*/
updatePageWithItems(searchOptions, author: string) {
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-title w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
[objects$]="items$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,92 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
@Component({
selector: 'ds-browse-by-title-page',
styleUrls: ['./browse-by-title-page.component.scss'],
templateUrl: './browse-by-title-page.component.html'
})
/**
* Component for browsing items by title (dc.title)
*/
export class BrowseByTitlePageComponent implements OnInit {
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
this.updatePage({
pagination: pagination,
sort: sort
});
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
sort: searchOptions.sort
});
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,16 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'title', component: BrowseByTitlePageComponent },
{ path: 'author', component: BrowseByAuthorPageComponent }
])
]
})
export class BrowseByRoutingModule {
}

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
@NgModule({
imports: [
BrowseByRoutingModule,
CommonModule,
SharedModule
],
declarations: [
BrowseByTitlePageComponent,
BrowseByAuthorPageComponent
],
providers: [
ItemDataService,
BrowseService
]
})
export class BrowseByModule {
}

View File

@@ -35,7 +35,7 @@
</ds-comcol-page-content>
</div>
</div>
<ds-error *ngIf="collectionRD?.hasFailed"0 message="{{'error.collection' | translate}}"></ds-error>
<ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
<br>
<ng-container *ngVar="(itemRD$ | async) as itemRD">

View File

@@ -6,13 +6,21 @@ import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data';
import { getSucceededRemoteData } from '../core/shared/operators';
/**
* This class represents a resolver that requests a specific collection before the route is activated
*/
@Injectable()
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
constructor(private collectionService: CollectionDataService) {
}
/**
* Method for resolving a collection based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id).pipe(
getSucceededRemoteData()
);

View File

@@ -6,13 +6,21 @@ import { getSucceededRemoteData } from '../core/shared/operators';
import { Community } from '../core/shared/community.model';
import { CommunityDataService } from '../core/data/community-data.service';
/**
* This class represents a resolver that requests a specific community before the route is activated
*/
@Injectable()
export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
constructor(private communityService: CommunityDataService) {
}
/**
* Method for resolving a community based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById(route.params.id).pipe(
getSucceededRemoteData()
);

View File

@@ -6,11 +6,20 @@ import { getSucceededRemoteData } from '../core/shared/operators';
import { ItemDataService } from '../core/data/item-data.service';
import { Item } from '../core/shared/item.model';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable()
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
constructor(private itemService: ItemDataService) {
}
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id).pipe(
getSucceededRemoteData()

View File

@@ -12,6 +12,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },

View File

@@ -1,14 +1,15 @@
import { AuthType } from './auth-type';
import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { EPerson } from '../eperson/models/eperson.model';
export class AuthObjectFactory {
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
public static getConstructor(type): GenericConstructor<NormalizedObject> {
switch (type) {
case AuthType.Eperson: {
return NormalizedEpersonModel
case AuthType.EPerson: {
return NormalizedEPerson
}
case AuthType.Status: {

View File

@@ -8,12 +8,13 @@ import { CoreState } from '../core.reducers';
import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { getMockStore } from '../../shared/mocks/mock-store';
describe('ConfigResponseParsingService', () => {
describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService;
const EnvConfig = {} as GlobalConfig;
const store = {} as Store<CoreState>;
const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig;
const store = getMockStore() as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store);
beforeEach(() => {
@@ -86,13 +87,19 @@ describe('ConfigResponseParsingService', () => {
type: 'eperson',
uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
_links: {
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
}
}
}
},
_links: {
eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b',
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
eperson: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
},
self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
}
}
},
statusCode: '200'

View File

@@ -12,22 +12,23 @@ import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models';
import { AuthType } from './auth-type';
import { AuthStatus } from './models/auth-status.model';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
@Injectable()
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = AuthObjectFactory;
protected toCache = false;
protected toCache = true;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,) {
protected objectCache: ObjectCacheService) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
const response = this.process<AuthStatus, AuthType>(data.payload, request.href);
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode);
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
return new AuthStatusResponse(response, data.statusCode);
} else {
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
}

View File

@@ -1,4 +1,4 @@
export enum AuthType {
Eperson = 'eperson',
EPerson = 'eperson',
Status = 'status'
}

View File

@@ -5,7 +5,7 @@ import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
// import models
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
export const AuthActionTypes = {
@@ -76,10 +76,10 @@ export class AuthenticatedSuccessAction implements Action {
payload: {
authenticated: boolean;
authToken: AuthTokenInfo;
user: Eperson
user: EPerson
};
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) {
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) {
this.payload = { authenticated, authToken, user };
}
}
@@ -250,9 +250,9 @@ export class RefreshTokenErrorAction implements Action {
*/
export class RegistrationAction implements Action {
public type: string = AuthActionTypes.REGISTRATION;
payload: Eperson;
payload: EPerson;
constructor(user: Eperson) {
constructor(user: EPerson) {
this.payload = user;
}
}
@@ -278,9 +278,9 @@ export class RegistrationErrorAction implements Action {
*/
export class RegistrationSuccessAction implements Action {
public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
payload: Eperson;
payload: EPerson;
constructor(user: Eperson) {
constructor(user: EPerson) {
this.payload = user;
}
}

View File

@@ -25,12 +25,11 @@ import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
import { AuthService } from './auth.service';
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('AuthEffects', () => {
let authEffects: AuthEffects;
let actions: Observable<any>;
const authServiceStub = new AuthServiceStub();
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
@@ -105,7 +104,7 @@ describe('AuthEffects', () => {
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)});
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)});
expect(authEffects.authenticated$).toBeObservable(expected);
});

View File

@@ -28,7 +28,7 @@ import {
RegistrationErrorAction,
RegistrationSuccessAction
} from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { AppState } from '../../app.reducer';
@@ -66,7 +66,7 @@ export class AuthEffects {
ofType(AuthActionTypes.AUTHENTICATED),
switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser(action.payload).pipe(
map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
})
);
@@ -94,7 +94,7 @@ export class AuthEffects {
debounceTime(500), // to remove when functionality is implemented
switchMap((action: RegistrationAction) => {
return this.authService.create(action.payload).pipe(
map((user: Eperson) => new RegistrationSuccessAction(user)),
map((user: EPerson) => new RegistrationSuccessAction(user)),
catchError((error) => observableOf(new RegistrationErrorAction(error)))
);
})

View File

@@ -42,7 +42,7 @@ export class AuthInterceptor implements HttpInterceptor {
}
private isSuccess(response: HttpResponseBase): boolean {
return response.status === 200;
return (response.status === 200 || response.status === 204);
}
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {

View File

@@ -21,7 +21,7 @@ import {
SetRedirectUrlAction
} from './auth.actions';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('authReducer', () => {
@@ -107,7 +107,7 @@ describe('authReducer', () => {
loading: true,
info: undefined
};
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock);
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock);
const newState = authReducer(initialState, action);
state = {
authenticated: true,
@@ -116,7 +116,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
expect(newState).toEqual(state);
});
@@ -182,7 +182,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutAction();
@@ -199,7 +199,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutSuccessAction();
@@ -225,7 +225,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutErrorAction(mockError);
@@ -237,7 +237,7 @@ describe('authReducer', () => {
error: 'Test error message',
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
expect(newState).toEqual(state);
});
@@ -250,7 +250,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
const action = new RefreshTokenAction(newTokenInfo);
@@ -262,7 +262,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
expect(newState).toEqual(state);
@@ -276,7 +276,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
@@ -289,7 +289,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: false
};
expect(newState).toEqual(state);
@@ -303,7 +303,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
const action = new RefreshTokenErrorAction();
@@ -329,7 +329,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
state = {

View File

@@ -12,7 +12,7 @@ import {
SetRedirectUrlAction
} from './auth.actions';
// import models
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
/**
@@ -46,7 +46,7 @@ export interface AuthState {
refreshing?: boolean;
// the authenticated user
user?: Eperson;
user?: EPerson;
}
/**

View File

@@ -17,10 +17,12 @@ import { AuthRequestServiceStub } from '../../shared/testing/auth-request-servic
import { AuthRequestService } from './auth-request.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { Eperson } from '../eperson/models/eperson.model';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPerson } from '../eperson/models/eperson.model';
import { EPersonMock } from '../../shared/testing/eperson-mock';
import { AppState } from '../../app.reducer';
import { ClientCookieService } from '../../shared/services/client-cookie.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
describe('AuthService test', () => {
@@ -41,9 +43,9 @@ describe('AuthService test', () => {
loaded: true,
loading: false,
authToken: token,
user: EpersonMock
user: EPersonMock
};
const rdbService = getMockRemoteDataBuildService();
describe('', () => {
beforeEach(() => {
@@ -60,6 +62,7 @@ describe('AuthService test', () => {
{provide: Router, useValue: routerStub},
{provide: ActivatedRoute, useValue: routeStub},
{provide: Store, useValue: mockStore},
{provide: RemoteDataBuildService, useValue: rdbService},
CookieService,
AuthService
],
@@ -78,7 +81,7 @@ describe('AuthService test', () => {
});
it('should return the authenticated user object when user token is valid', () => {
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => {
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => {
expect(user).toBeDefined();
});
});
@@ -120,6 +123,7 @@ describe('AuthService test', () => {
{provide: AuthRequestService, useValue: authRequest},
{provide: REQUEST, useValue: {}},
{provide: Router, useValue: routerStub},
{provide: RemoteDataBuildService, useValue: rdbService},
CookieService
]
}).compileComponents();
@@ -131,7 +135,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, authReqService, router, cookieService, store);
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
}));
it('should return true when user is logged in', () => {
@@ -183,14 +187,14 @@ describe('AuthService test', () => {
loaded: true,
loading: false,
authToken: expiredToken,
user: EpersonMock
user: EPersonMock
};
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, authReqService, router, cookieService, store);
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
storage = (authService as any).storage;
spyOn(storage, 'get');
spyOn(storage, 'remove');

View File

@@ -16,8 +16,10 @@ import { REQUEST } from '@nguniversal/express-engine/tokens';
import { RouterReducerState } from '@ngrx/router-store';
import { select, Store } from '@ngrx/store';
import { CookieAttributes } from 'js-cookie';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
@@ -35,6 +37,8 @@ import { AppState, routerStateSelector } from '../../app.reducer';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
@@ -58,7 +62,9 @@ export class AuthService {
protected authRequestService: AuthRequestService,
protected router: Router,
protected storage: CookieService,
protected store: Store<AppState>) {
protected store: Store<AppState>,
protected rdbService: RemoteDataBuildService
) {
this.store.pipe(
select(isAuthenticated),
startWith(false)
@@ -132,7 +138,7 @@ export class AuthService {
* Returns the authenticated user
* @returns {User}
*/
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
@@ -140,13 +146,18 @@ export class AuthService {
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe(
map((status: AuthStatus) => {
switchMap((status: AuthStatus) => {
if (status.authenticated) {
return status.eperson[0];
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
// Review when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
return person$.pipe(map((eperson) => eperson.payload));
} else {
throw(new Error('Not authenticated'));
}
}));
}))
}
/**
@@ -206,7 +217,7 @@ export class AuthService {
* Create a new user
* @returns {User}
*/
public create(user: Eperson): Observable<Eperson> {
public create(user: EPerson): Observable<EPerson> {
// Normally you would do an HTTP request to POST the user
// details and then return the new user object
// but, let's just return the new user for this example.
@@ -357,8 +368,12 @@ export class AuthService {
this.router.navigated = false;
const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = url;
} else {
this.router.navigate(['/']);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = '/';
}
})

View File

@@ -1,7 +1,8 @@
import { AuthError } from './auth-error.model';
import { AuthTokenInfo } from './auth-token-info.model';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { EPerson } from '../../eperson/models/eperson.model';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/Observable';
export class AuthStatus {
@@ -13,7 +14,7 @@ export class AuthStatus {
error?: AuthError;
eperson: Eperson[];
eperson: Observable<RemoteData<EPerson>>;
token?: AuthTokenInfo;

View File

@@ -1,12 +1,18 @@
import { AuthStatus } from './auth-status.model';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from '../../cache/models/normalized-object.model';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
@mapsTo(AuthStatus)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
@inheritSerialization(NormalizedObject)
export class NormalizedAuthStatus extends NormalizedObject {
@autoserialize
id: string;
@autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
uuid: string;
/**
* True if REST API is up and running, should never return false
@@ -20,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject {
@autoserialize
authenticated: boolean;
@autoserializeAs(Eperson)
eperson: Eperson[];
@relationship(ResourceType.EPerson, false)
@autoserialize
eperson: string;
}

View File

@@ -1,5 +1,4 @@
import {first, map} from 'rxjs/operators';
import { first, map, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@@ -10,7 +9,8 @@ import { isNotEmpty } from '../../shared/empty.util';
import { AuthService } from './auth.service';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { CheckAuthenticationTokenAction } from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
/**
* The auth service.
@@ -22,7 +22,7 @@ export class ServerAuthService extends AuthService {
* Returns the authenticated user
* @returns {User}
*/
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
@@ -35,13 +35,18 @@ export class ServerAuthService extends AuthService {
options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe(
map((status: AuthStatus) => {
switchMap((status: AuthStatus) => {
if (status.authenticated) {
return status.eperson[0];
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
// person$.subscribe(() => console.log('test'));
return person$.pipe(map((eperson) => eperson.payload));
} else {
throw(new Error('Not authenticated'));
}
}));
}))
}
/**

View File

@@ -6,7 +6,7 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models';
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service';
@@ -143,7 +143,9 @@ describe('BrowseService', () => {
});
describe('getBrowseEntriesFor', () => {
describe('getBrowseEntriesFor and getBrowseItemsFor', () => {
const mockAuthorName = 'Donald Smith';
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
@@ -156,7 +158,7 @@ describe('BrowseService', () => {
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
});
describe('when called with a valid browse definition id', () => {
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should configure a new BrowseEntriesRequest', () => {
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
@@ -175,7 +177,26 @@ describe('BrowseService', () => {
});
describe('when called with an invalid browse definition id', () => {
describe('when getBrowseItemsFor is called with a valid browse definition id', () => {
it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
});
});
describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
@@ -184,6 +205,16 @@ describe('BrowseService', () => {
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
});
});
describe('when getBrowseItemsFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
});
});
});
describe('getBrowseURLFor', () => {

View File

@@ -16,19 +16,27 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models';
import {
BrowseEndpointRequest,
BrowseEntriesRequest,
BrowseItemsRequest,
GetRequest,
RestRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
configureRequest,
filterSuccessfulResponses,
filterSuccessfulResponses, getBrowseDefinitionLinks,
getRemoteDataPayload,
getRequestFromSelflink,
getResponseFromSelflink
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable()
export class BrowseService {
@@ -71,6 +79,8 @@ export class BrowseService {
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))),
distinctUntilChanged()
);
@@ -82,17 +92,7 @@ export class BrowseService {
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
const request$ = this.getBrowseDefinitions().pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
}),
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.entries),
hasValueOperator(),
@@ -124,6 +124,66 @@ export class BrowseService {
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
})),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
}
/**
* Get all items linked to a certain metadata value
* @param {string} definitionID definition ID to define the metadata-field (e.g. author)
* @param {string} filterValue metadata value to filter by (e.g. author's name)
* @param options Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(options.pagination)) {
args.push(`page=${options.pagination.currentPage - 1}`);
args.push(`size=${options.pagination.pageSize}`);
}
if (isNotEmpty(filterValue)) {
args.push(`filterValue=${filterValue}`);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
}
return href;
}),
map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService)
);
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<Item>) => Object.assign(list, {
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
})),
distinctUntilChanged()
);

View File

@@ -8,6 +8,8 @@ import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model';
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
@@ -33,6 +35,12 @@ export class NormalizedObjectFactory {
case ResourceType.ResourcePolicy: {
return NormalizedResourcePolicy
}
case ResourceType.EPerson: {
return NormalizedEPerson
}
case ResourceType.Group: {
return NormalizedGroup
}
default: {
return undefined;
}

View File

@@ -62,6 +62,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
const IMPORTS = [
@@ -115,6 +116,7 @@ const PROVIDERS = [
ServerResponseService,
BrowseResponseParsingService,
BrowseEntriesResponseParsingService,
BrowseItemsResponseParsingService,
BrowseService,
ConfigResponseParsingService,
RouteService,

View File

@@ -7,6 +7,8 @@ import { GlobalConfig } from '../../../config/global-config.interface';
import { GenericConstructor } from '../shared/generic-constructor';
import { PaginatedList } from './paginated-list';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResourceType } from '../shared/resource-type';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
@@ -34,6 +36,7 @@ export abstract class BaseResponseParsingService {
} else if (Array.isArray(data)) {
return this.processArray(data, requestHref);
} else if (isObjectLevel(data)) {
data = this.fixBadEPersonRestResponse(data);
const object = this.deserialize(data);
if (isNotEmpty(data._embedded)) {
Object
@@ -53,6 +56,7 @@ export abstract class BaseResponseParsingService {
}
});
}
this.cache(object, requestHref);
return object;
}
@@ -145,4 +149,23 @@ export abstract class BaseResponseParsingService {
}
return obj[keys[0]];
}
// TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
private fixBadEPersonRestResponse(obj: any): any {
if (obj.type === ResourceType.EPerson) {
const groups = obj.groups;
const normGroups = [];
if (isNotEmpty(groups)) {
groups.forEach((group) => {
const parts = ['eperson', 'groups', group.uuid];
const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString();
normGroups.push(href);
}
)
}
return Object.assign({}, obj, { groups: normGroups });
}
return obj;
}
}

View File

@@ -0,0 +1,168 @@
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
describe('BrowseItemsResponseParsingService', () => {
let service: BrowseItemsResponseParsingService;
beforeEach(() => {
service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService());
});
describe('parse', () => {
const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items');
const validResponse = {
payload: {
_embedded: {
items: [
{
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
{
id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
handle: '10986/17475',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:33:42.526+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b'
}
}
}
]
},
_links: {
first: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items'
},
next: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2'
},
last: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2'
}
},
page: {
size: 2,
totalElements: 16,
totalPages: 8,
number: 0
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
payload: {
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseStatusCode = {
payload: {}, statusCode: '500'
} as DSpaceRESTV2Response;
it('should return a GenericSuccessResponse if data contains a valid browse items response', () => {
const response = service.parse(request, validResponse);
expect(response.constructor).toBe(GenericSuccessResponse);
});
it('should return an ErrorResponse if data contains an invalid browse entries response', () => {
const response = service.parse(request, invalidResponseNotAList);
expect(response.constructor).toBe(ErrorResponse);
});
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
const response = service.parse(request, invalidResponseStatusCode);
expect(response.constructor).toBe(ErrorResponse);
});
});
});

View File

@@ -0,0 +1,58 @@
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
import {
ErrorResponse,
GenericSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
/**
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
*/
@Injectable()
export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = {
getConstructor: () => DSpaceObject
};
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
/**
* Parses data from the browse endpoint to a list of DSpaceObjects
* @param {RestRequest} request
* @param {DSpaceRESTV2Response} data
* @returns {RestResponse}
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(DSpaceObject);
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
{ statusText: data.statusCode }
)
);
}
}
}

View File

@@ -9,7 +9,7 @@ import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service';
import { FindByIDRequest } from './request.models';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -52,6 +52,10 @@ describe('ComColDataService', () => {
const EnvConfig = {} as GlobalConfig;
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID
});
const communitiesEndpoint = 'https://rest.api/core/communities';
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
@@ -98,7 +102,7 @@ describe('ComColDataService', () => {
);
}
describe('getScopedEndpoint', () => {
describe('getBrowseEndpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
});
@@ -112,7 +116,7 @@ describe('ComColDataService', () => {
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
@@ -128,13 +132,13 @@ describe('ComColDataService', () => {
});
it('should fetch the scope Community from the cache', () => {
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush();
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
});
it('should return the endpoint to fetch resources within the given scope', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--e-', { e: scopedEndpoint });
expect(result).toBeObservable(expected);
@@ -151,7 +155,7 @@ describe('ComColDataService', () => {
});
it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
expect(result).toBeObservable(expected);

View File

@@ -7,7 +7,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service';
import { FindByIDRequest } from './request.models';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DSOSuccessResponse } from '../cache/response-cache.models';
@@ -27,17 +27,18 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
* @return { Observable<string> }
* an Observable<string> containing the scoped URL
*/
public getScopedEndpoint(scopeID: string): Observable<string> {
if (isEmpty(scopeID)) {
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(this.linkPath);
} else {
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)),
first((href: string) => isNotEmpty(href)),
tap((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID);
const scopeCommunityHrefObs = this.cds.getEndpoint()
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID))
.filter((href: string) => isNotEmpty(href))
.take(1)
.do((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request);
}),);
});
// return scopeCommunityHrefObs.pipe(
// mergeMap((href: string) => this.responseCache.get(href)),
@@ -61,16 +62,15 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
map((entry: ResponseCacheEntry) => entry.response));
const errorResponses = responses.pipe(
filter((response) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`The Community with scope ${scopeID} couldn't be retrieved`)))
mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
);
const successResponses = responses.pipe(
filter((response) => response.isSuccessful),
mergeMap(() => this.objectCache.getByUUID(scopeID)),
mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
map((nc: NormalizedCommunity) => nc._links[this.linkPath]),
filter((href) => isNotEmpty(href))
);
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged());
}
}

View File

@@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
import { FindAllOptions } from './request.models';
import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
const LINK_NAME = 'test'
const endpoint = 'https://rest.api/core';
// tslint:disable:max-classes-per-file
class NormalizedTestObject extends NormalizedObject {
@@ -28,10 +28,9 @@ class TestService extends DataService<NormalizedTestObject, any> {
super();
}
public getScopedEndpoint(scope: string): Observable<string> {
throw new Error('getScopedEndpoint is abstract in DataService');
public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
return Observable.of(endpoint);
}
}
describe('DataService', () => {
@@ -42,7 +41,6 @@ describe('DataService', () => {
const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService;
const store = {} as Store<CoreState>;
const endpoint = 'https://rest.api/core';
function initTestService(): TestService {
return new TestService(
@@ -50,7 +48,7 @@ describe('DataService', () => {
requestService,
rdbService,
store,
LINK_NAME,
endpoint,
halService
);
}
@@ -62,25 +60,17 @@ describe('DataService', () => {
it('should return an observable with the endpoint', () => {
options = {};
(service as any).getFindAllHref(endpoint).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(endpoint);
}
);
});
// getScopedEndpoint is not implemented in abstract DataService
it('should throw error if scopeID provided in options', () => {
options = { scopeID: 'somevalue' };
expect(() => { (service as any).getFindAllHref(endpoint, options) })
.toThrowError('getScopedEndpoint is abstract in DataService');
});
it('should include page in href if currentPage provided in options', () => {
options = { currentPage: 2 };
const expected = `${endpoint}?page=${options.currentPage - 1}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -89,7 +79,7 @@ describe('DataService', () => {
options = { elementsPerPage: 5 };
const expected = `${endpoint}?size=${options.elementsPerPage}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -99,7 +89,7 @@ describe('DataService', () => {
options = { sort: sortOptions};
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -108,7 +98,7 @@ describe('DataService', () => {
options = { startsWith: 'ab' };
const expected = `${endpoint}?startsWith=${options.startsWith}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -124,7 +114,7 @@ describe('DataService', () => {
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => {
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
})

View File

@@ -1,7 +1,5 @@
import { filter, take, first } from 'rxjs/operators';
import {of as observableOf, Observable } from 'rxjs';
import {mergeMap, first, take, distinctUntilChanged, map, filter} from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -14,6 +12,8 @@ import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { promise } from 'selenium-webdriver';
import map = promise.map;
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
protected abstract responseCache: ResponseCacheService;
@@ -23,17 +23,13 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract linkPath: string;
protected abstract halService: HALEndpointService;
public abstract getScopedEndpoint(scope: string): Observable<string>
public abstract getBrowseEndpoint(options: FindAllOptions): Observable<string>
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
protected getFindAllHref(options: FindAllOptions = {}): Observable<string> {
let result: Observable<string>;
const args = [];
if (hasValue(options.scopeID)) {
result = this.getScopedEndpoint(options.scopeID).pipe(distinctUntilChanged());
} else {
result = observableOf(endpoint);
}
result = this.getBrowseEndpoint(options).distinctUntilChanged();
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
@@ -60,12 +56,11 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
}
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(filter((href: string) => isNotEmpty(href)),
mergeMap((endpoint: string) => this.getFindAllHref(endpoint, options)),);
const hrefObs = this.getFindAllHref(options);
hrefObs.pipe(
filter((href: string) => hasValue(href)),
take(1),)
take(1))
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);

View File

@@ -10,6 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DataService } from './data.service';
import { RemoteData } from './remote-data';
import { RequestService } from './request.service';
import { FindAllOptions } from './request.models';
/* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
@@ -24,8 +25,8 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
super();
}
getScopedEndpoint(scope: string): Observable<string> {
return undefined;
getBrowseEndpoint(options: FindAllOptions): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
getFindByIDHref(endpoint, resourceID): string {

View File

@@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
describe('ItemDataService', () => {
let scheduler: TestScheduler;
@@ -20,6 +21,14 @@ describe('ItemDataService', () => {
const halEndpointService = {} as HALEndpointService;
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID,
sort: {
field: '',
direction: undefined
}
});
const browsesEndpoint = 'https://rest.api/discover/browses';
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
@@ -46,16 +55,16 @@ describe('ItemDataService', () => {
);
}
describe('getScopedEndpoint', () => {
describe('getBrowseEndpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
});
it('should return the endpoint to fetch Items within the given scope', () => {
it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
bs = initMockBrowseService(true);
service = initTestService();
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--b-', { b: scopedEndpoint });
expect(result).toBeObservable(expected);
@@ -67,7 +76,7 @@ describe('ItemDataService', () => {
service = initTestService();
});
it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID);
const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, browseError);
expect(result).toBeObservable(expected);

View File

@@ -1,11 +1,7 @@
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedItem } from '../cache/models/normalized-item.model';
@@ -17,6 +13,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -32,15 +29,21 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
super();
}
public getScopedEndpoint(scopeID: string): Observable<string> {
if (isEmpty(scopeID)) {
return this.halService.getEndpoint(this.linkPath);
} else {
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath).pipe(
filter((href: string) => isNotEmpty(href)),
map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()),
distinctUntilChanged(),);
/**
* Get the endpoint for browsing items
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
* @param {FindAllOptions} options
* @returns {Observable<string>}
*/
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
let field = 'dc.date.issued';
if (options.sort && options.sort.field) {
field = options.sort.field;
}
return this.bs.getBrowseURLFor(field, this.linkPath)
.filter((href: string) => isNotEmpty(href))
.map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString())
.distinctUntilChanged();
}
}

View File

@@ -12,6 +12,7 @@ import { AuthResponseParsingService } from '../auth/auth-response-parsing.servic
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
/* tslint:disable:max-classes-per-file */
@@ -184,6 +185,12 @@ export class BrowseEntriesRequest extends GetRequest {
}
}
export class BrowseItemsRequest extends GetRequest {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return BrowseItemsResponseParsingService;
}
}
export class ConfigRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);

View File

@@ -1,7 +1,7 @@
import { DSpaceObject } from '../../shared/dspace-object.model';
import { Group } from './group.model';
export class Eperson extends DSpaceObject {
export class EPerson extends DSpaceObject {
public handle: string;

View File

@@ -2,13 +2,13 @@ import { autoserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from './eperson.model';
import { EPerson } from './eperson.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
@mapsTo(Eperson)
@mapsTo(EPerson)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEpersonModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
@autoserialize
public handle: string;

View File

@@ -2,13 +2,12 @@ import { autoserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from './eperson.model';
import { mapsTo } from '../../cache/builders/build-decorators';
import { Group } from './group.model';
@mapsTo(Group)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedGroupModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
@autoserialize
public handle: string;

View File

@@ -1,6 +1,7 @@
import { autoserialize, autoserializeAs } from 'cerialize';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
export class BrowseEntry {
export class BrowseEntry implements ListableObject {
@autoserialize
type: string;

View File

@@ -5,6 +5,7 @@ import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { Observable } from 'rxjs';
import { autoserialize } from 'cerialize';
/**
* An abstract model class for a DSpaceObject.
@@ -16,11 +17,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* The human-readable identifier of this DSpaceObject
*/
@autoserialize
id: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserialize
uuid: string;
/**
@@ -31,11 +34,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/**
* The name for this DSpaceObject
*/
@autoserialize
name: string;
/**
* An array containing all metadata of this DSpaceObject
*/
@autoserialize
metadata: Metadatum[];
/**

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs';
import { filter, first, flatMap, map, tap } from 'rxjs/operators';
import { hasValueOperator } from '../../shared/empty.util';
import { hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { DSOSuccessResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
@@ -8,6 +8,7 @@ import { RemoteData } from '../data/remote-data';
import { RestRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from './browse-definition.model';
import { DSpaceObject } from './dspace-object.model';
import { PaginatedList } from '../data/paginated-list';
import { SearchResult } from '../../+search-page/search-result.model';
@@ -62,3 +63,24 @@ export const toDSpaceObjectListRD = () =>
return Object.assign(rd, {payload: payload});
})
);
/**
* Get the browse links from a definition by ID given an array of all definitions
* @param {string} definitionID
* @returns {(source: Observable<RemoteData<BrowseDefinition[]>>) => Observable<any>}
*/
export const getBrowseDefinitionLinks = (definitionID: string) =>
(source: Observable<RemoteData<BrowseDefinition[]>>): Observable<any> =>
source.pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
})
);

View File

@@ -6,7 +6,7 @@ export enum ResourceType {
Item = 'item',
Collection = 'collection',
Community = 'community',
Eperson = 'eperson',
EPerson = 'eperson',
Group = 'group',
ResourcePolicy = 'resourcePolicy'
}

View File

@@ -20,6 +20,8 @@ import { RouterStub } from '../shared/testing/router-stub';
import { Router } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import * as ngrx from '@ngrx/store';
import { NO_ERRORS_SCHEMA } from '@angular/core';
let comp: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let store: Store<HeaderState>;
@@ -35,11 +37,12 @@ describe('HeaderComponent', () => {
NgbCollapseModule.forRoot(),
NoopAnimationsModule,
ReactiveFormsModule],
declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent],
declarations: [HeaderComponent],
providers: [
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: Router, useClass: RouterStub },
]
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents(); // compile template and css
}));

View File

@@ -5,7 +5,7 @@ import { By } from '@angular/platform-browser';
import { Store, StoreModule } from '@ngrx/store';
import { authReducer, AuthState } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock';
import { EPersonMock } from '../testing/eperson-mock';
import { TranslateModule } from '@ngx-translate/core';
import { AppState } from '../../app.reducer';
import { AuthNavMenuComponent } from './auth-nav-menu.component';
@@ -13,6 +13,7 @@ import { HostWindowServiceStub } from '../testing/host-window-service-stub';
import { HostWindowService } from '../host-window.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { AuthService } from '../../core/auth/auth.service';
describe('AuthNavMenuComponent', () => {
@@ -31,7 +32,7 @@ describe('AuthNavMenuComponent', () => {
loaded: true,
loading: false,
authToken: new AuthTokenInfo('test_token'),
user: EpersonMock
user: EPersonMock
};
let routerState = {
url: '/home'
@@ -53,6 +54,7 @@ describe('AuthNavMenuComponent', () => {
],
providers: [
{provide: HostWindowService, useValue: window},
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
@@ -222,6 +224,7 @@ describe('AuthNavMenuComponent', () => {
],
providers: [
{provide: HostWindowService, useValue: window},
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA

View File

@@ -14,8 +14,9 @@ import {
isAuthenticated,
isAuthenticationLoading
} from '../../core/auth/selectors';
import { Eperson } from '../../core/eperson/models/eperson.model';
import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'ds-auth-nav-menu',
@@ -40,10 +41,14 @@ export class AuthNavMenuComponent implements OnInit {
public showAuth = observableOf(false);
public user: Observable<Eperson>;
public user: Observable<EPerson>;
public sub: Subscription;
constructor(private store: Store<AppState>,
private windowService: HostWindowService) {
private windowService: HostWindowService,
private authService: AuthService
) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
@@ -56,12 +61,15 @@ export class AuthNavMenuComponent implements OnInit {
this.user = this.store.pipe(select(getAuthenticatedUser));
this.showAuth = this.store.pipe(
select(routerStateSelector),
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)),
map((router: RouterReducerState) => {
return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
})
);
this.showAuth = this.store.select(routerStateSelector)
.filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state))
.map((router: RouterReducerState) => {
const url = router.state.url;
const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
if (show) {
this.authService.setRedirectUrl(url);
}
return show;
});
}
}

View File

@@ -0,0 +1,12 @@
<ng-container *ngVar="(objects$ | async) as objects">
<h2 class="w-100">{{title}}</h2>
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
<ds-viewable-collection
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="objects">
</ds-viewable-collection>
</div>
<ds-loading *ngIf="!objects || objects?.payload?.page.length <= 0" message="{{'loading.browse-by' | translate}}"></ds-loading>
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
</ng-container>

View File

@@ -0,0 +1,44 @@
import { BrowseByComponent } from './browse-by.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { SharedModule } from '../shared.module';
describe('BrowseByComponent', () => {
let comp: BrowseByComponent;
let fixture: ComponentFixture<BrowseByComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule],
declarations: [],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByComponent);
comp = fixture.componentInstance;
});
it('should display a loading message when objects is empty',() => {
(comp as any).objects = undefined;
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-loading'))).toBeDefined();
});
it('should display results when objects is not empty', () => {
(comp as any).objects = Observable.of({
payload: {
page: {
length: 1
}
}
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined();
});
});

View File

@@ -0,0 +1,30 @@
import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { Observable } from 'rxjs/Observable';
import { Item } from '../../core/shared/item.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
@Component({
selector: 'ds-browse-by',
styleUrls: ['./browse-by.component.scss'],
templateUrl: './browse-by.component.html',
animations: [
fadeIn,
fadeInOut
]
})
/**
* Component to display a browse-by page for any ListableObject
*/
export class BrowseByComponent {
@Input() title: string;
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
@Input() paginationConfig: PaginationComponentOptions;
@Input() sortConfig: SortOptions;
@Input() currentUrl: string;
query: string;
}

View File

@@ -7,8 +7,8 @@ import { Store, StoreModule } from '@ngrx/store';
import { LogInComponent } from './log-in.component';
import { authReducer } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model';
import { EPersonMock } from '../testing/eperson-mock';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from '../../core/auth/auth.service';
import { AuthServiceStub } from '../testing/auth-service-stub';
@@ -19,7 +19,7 @@ describe('LogInComponent', () => {
let component: LogInComponent;
let fixture: ComponentFixture<LogInComponent>;
let page: Page;
let user: Eperson;
let user: EPerson;
const authState = {
authenticated: false,
@@ -28,7 +28,7 @@ describe('LogInComponent', () => {
};
beforeEach(() => {
user = EpersonMock;
user = EPersonMock;
});
beforeEach(async(() => {

View File

@@ -5,8 +5,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Store, StoreModule } from '@ngrx/store';
import { authReducer } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model';
import { EPersonMock } from '../testing/eperson-mock';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router';
import { AppState } from '../../app.reducer';
@@ -18,7 +18,7 @@ describe('LogOutComponent', () => {
let component: LogOutComponent;
let fixture: ComponentFixture<LogOutComponent>;
let page: Page;
let user: Eperson;
let user: EPerson;
const authState = {
authenticated: false,
@@ -28,7 +28,7 @@ describe('LogOutComponent', () => {
const routerStub = new RouterStub();
beforeEach(() => {
user = EpersonMock;
user = EPersonMock;
});
beforeEach(async(() => {

View File

@@ -5,6 +5,7 @@ import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { RemoteData } from '../../core/data/remote-data';
import { RequestEntry } from '../../core/data/request.reducer';
import { hasValue } from '../empty.util';
import { NormalizedObject } from '../../core/cache/models/normalized-object.model';
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
return {
@@ -17,7 +18,8 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab
payload
} as RemoteData<any>)))
}
}
},
buildSingle: (href$: string | Observable<string>) => Observable.of(new RemoteData(false, false, true, undefined, {}))
} as RemoteDataBuildService;
}

View File

@@ -8,7 +8,7 @@
<ds-object-grid [config]="config"
[sortConfig]="sortConfig"
[objects]="objects"
[hideGear]="true"
[hideGear]="hideGear"
*ngIf="getViewMode()===viewModeEnum.Grid">
</ds-object-grid>

View File

@@ -1,22 +1,28 @@
<div class="card">
<ds-truncatable [id]="object.id">
<div class="card">
<a [routerLink]="['/items/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
</ds-grid-thumbnail>
</a>
<div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
<a [routerLink]="['/items/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
</ds-grid-thumbnail>
</a>
<div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
</p>
<ds-truncatable-part [id]="object.id" [minLines]="2">
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
</p>
</ds-truncatable-part>
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
<ds-truncatable-part [id]="object.id" [minLines]="5">
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
</ds-truncatable-part>
<div class="text-center">
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
<div class="text-center pt-2">
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
</div>
</div>
</div>
</div>
</ds-truncatable>

View File

@@ -0,0 +1,7 @@
<div class="d-flex flex-row">
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
{{object.value}}
</a>
<span class="pr-2">&nbsp;</span>
<span class="badge badge-pill badge-secondary align-self-center">{{object.count}}</span>
</div>

View File

@@ -0,0 +1 @@
@import '../../../../styles/variables';

View File

@@ -0,0 +1,47 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
let browseEntryListElementComponent: BrowseEntryListElementComponent;
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
type: 'browseEntry',
value: 'De Langhe Kristof'
});
describe('MetadataListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BrowseEntryListElementComponent , TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: {mockValue}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(BrowseEntryListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(BrowseEntryListElementComponent);
browseEntryListElementComponent = fixture.componentInstance;
}));
describe('When the metadatum is loaded', () => {
beforeEach(() => {
browseEntryListElementComponent.object = mockValue;
fixture.detectChanges();
});
it('should show the value as a link', () => {
const browseEntryLink = fixture.debugElement.query(By.css('a.lead'));
expect(browseEntryLink.nativeElement.textContent.trim()).toBe(mockValue.value);
});
});
});

View File

@@ -0,0 +1,18 @@
import { Component, Input, Inject } from '@angular/core';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({
selector: 'ds-browse-entry-list-element',
styleUrls: ['./browse-entry-list-element.component.scss'],
templateUrl: './browse-entry-list-element.component.html'
})
/**
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
*/
@renderElementsFor(BrowseEntry, ViewMode.List)
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {}

View File

@@ -1,18 +1,24 @@
<a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}}
</a>
<div>
<span class="text-muted">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
<ds-truncatable [id]="object.id">
<a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}}
</a>
<div>
<ds-truncatable-part [id]="object.id" [minLines]="1">
<span class="text-muted">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
</span>
</span>
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
</span>
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
</div>
</div>
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
</span>
</ds-truncatable-part>
<ds-truncatable-part [id]="object.id" [minLines]="3">
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract")}}
</div>
</ds-truncatable-part>
</div>
</ds-truncatable>

View File

@@ -76,6 +76,9 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { BrowseByModule } from '../+browse-by/browse-by.module';
import { BrowseByComponent } from './browse-by/browse-by.component';
import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component';
import { DebounceDirective } from './utils/debounce.directive';
import { ClickOutsideDirective } from './utils/click-outside.directive';
import { EmphasizePipe } from './utils/emphasize.pipe';
@@ -155,6 +158,7 @@ const COMPONENTS = [
ViewModeSwitchComponent,
TruncatableComponent,
TruncatablePartComponent,
BrowseByComponent,
InputSuggestionsComponent
];
@@ -168,6 +172,7 @@ const ENTRY_COMPONENTS = [
CollectionGridElementComponent,
CommunityGridElementComponent,
SearchResultGridElementComponent,
BrowseEntryListElementComponent
];
const PROVIDERS = [

View File

@@ -2,12 +2,13 @@ import {of as observableOf, Observable } from 'rxjs';
import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from '../../core/auth/models/auth-status.model';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { Eperson } from '../../core/eperson/models/eperson.model';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { isNotEmpty } from '../empty.util';
import { EpersonMock } from './eperson-mock';
import { EPersonMock } from './eperson-mock';
import { RemoteData } from '../../core/data/remote-data';
export class AuthRequestServiceStub {
protected mockUser: Eperson = EpersonMock;
protected mockUser: EPerson = EPersonMock;
protected mockTokenInfo = new AuthTokenInfo('test_token');
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
@@ -26,7 +27,7 @@ export class AuthRequestServiceStub {
if (this.validateToken(token)) {
authStatusStub.authenticated = true;
authStatusStub.token = this.mockTokenInfo;
authStatusStub.eperson = [this.mockUser];
authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
} else {
authStatusStub.authenticated = false;
}
@@ -45,7 +46,7 @@ export class AuthRequestServiceStub {
if (this.validateToken(token)) {
authStatusStub.authenticated = true;
authStatusStub.token = this.mockTokenInfo;
authStatusStub.eperson = [this.mockUser];
authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
} else {
authStatusStub.authenticated = false;
}

View File

@@ -2,8 +2,9 @@
import {of as observableOf, Observable } from 'rxjs';
import { AuthStatus } from '../../core/auth/models/auth-status.model';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { EpersonMock } from './eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model';
import { EPersonMock } from './eperson-mock';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { RemoteData } from '../../core/data/remote-data';
export class AuthServiceStub {
@@ -20,7 +21,7 @@ export class AuthServiceStub {
authStatus.okay = true;
authStatus.authenticated = true;
authStatus.token = this.token;
authStatus.eperson = [EpersonMock];
authStatus.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, EPersonMock));
return observableOf(authStatus);
} else {
console.log('error');
@@ -28,9 +29,9 @@ export class AuthServiceStub {
}
}
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
if (token.accessToken === 'token_test') {
return observableOf(EpersonMock);
return Observable.of(EPersonMock);
} else {
throw(new Error('Message Error test'));
}

View File

@@ -1,6 +1,6 @@
import { Eperson } from '../../core/eperson/models/eperson.model';
import { EPerson } from '../../core/eperson/models/eperson.model';
export const EpersonMock: Eperson = Object.assign(new Eperson(),{
export const EPersonMock: EPerson = Object.assign(new EPerson(),{
handle: null,
groups: [],
netid: 'test@test.com',