Merge remote-tracking branch 'remotes/origin/main' into authorities_and_controlled_vocabularies

This commit is contained in:
Giuseppe Digilio
2020-07-21 18:55:48 +02:00
10 changed files with 221 additions and 115 deletions

View File

@@ -1,4 +1,6 @@
# This workflow runs whenever a new pull request is created # This workflow runs whenever a new pull request is created
# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs).
# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818
name: Pull Request opened name: Pull Request opened
# Only run for newly opened PRs against the "main" branch # Only run for newly opened PRs against the "main" branch

View File

@@ -1,24 +1,26 @@
import {Component, Input, OnInit} from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import {filter, first, map, switchMap, take} from 'rxjs/operators'; import { defaultIfEmpty, filter, first, map, switchMap, take } from 'rxjs/operators';
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
import {getItemEditPath} from '../../item-page-routing.module'; import { getItemEditPath } from '../../item-page-routing.module';
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import {combineLatest as observableCombineLatest, combineLatest, Observable} from 'rxjs'; import { combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf } from 'rxjs';
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
import {VirtualMetadata} from '../virtual-metadata/virtual-metadata.component'; import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
import {Relationship} from '../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
import {hasValue, isNotEmpty} from '../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import {Item} from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import {MetadataValue} from '../../../core/shared/metadata.models'; import { MetadataValue } from '../../../core/shared/metadata.models';
import {ViewMode} from '../../../core/shared/view-mode.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
import {ActivatedRoute, Router} from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import {NotificationsService} from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import {ItemDataService} from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import {TranslateService} from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import {RelationshipService} from '../../../core/data/relationship.service'; import { RelationshipService } from '../../../core/data/relationship.service';
import {EntityTypeService} from '../../../core/data/entity-type.service'; import { EntityTypeService } from '../../../core/data/entity-type.service';
import { LinkService } from '../../../core/cache/builders/link.service';
import { followLink } from '../../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-item-delete', selector: 'ds-item-delete',
@@ -80,6 +82,7 @@ export class ItemDeleteComponent
protected objectUpdatesService: ObjectUpdatesService, protected objectUpdatesService: ObjectUpdatesService,
protected relationshipService: RelationshipService, protected relationshipService: RelationshipService,
protected entityTypeService: EntityTypeService, protected entityTypeService: EntityTypeService,
protected linkService: LinkService,
) { ) {
super( super(
route, route,
@@ -98,30 +101,33 @@ export class ItemDeleteComponent
super.ngOnInit(); super.ngOnInit();
this.url = this.router.url; this.url = this.router.url;
this.types$ = this.entityTypeService.getEntityTypeByLabel( const label = this.item.firstMetadataValue('relationship.type');
this.item.firstMetadataValue('relationship.type') if (label !== undefined) {
).pipe( this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)), switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((relationshipTypes) => relationshipTypes.page), map((relationshipTypes) => relationshipTypes.page),
switchMap((types) => switchMap((types) =>
combineLatest(types.map((type) => this.getRelationships(type))).pipe( combineLatest(types.map((type) => this.getRelationships(type))).pipe(
map((relationships) => map((relationships) =>
types.reduce<RelationshipType[]>((includedTypes, type, index) => { types.reduce<RelationshipType[]>((includedTypes, type, index) => {
if (!includedTypes.some((includedType) => includedType.id === type.id) if (!includedTypes.some((includedType) => includedType.id === type.id)
&& !(relationships[index].length === 0)) { && !(relationships[index].length === 0)) {
return [...includedTypes, type]; return [...includedTypes, type];
} else { } else {
return includedTypes; return includedTypes;
} }
}, []) }, [])
), ),
) )
), ),
); );
} else {
this.types$ = observableOf([]);
}
this.types$.pipe( this.types$.pipe(
take(1), take(1),
@@ -187,6 +193,7 @@ export class ItemDeleteComponent
observableCombineLatest( observableCombineLatest(
relationships.map((relationship) => this.getRelationshipType(relationship)) relationships.map((relationship) => this.getRelationshipType(relationship))
).pipe( ).pipe(
defaultIfEmpty([]),
map((types) => relationships.filter( map((types) => relationships.filter(
(relationship, index) => relationshipType.id === types[index].id (relationship, index) => relationshipType.id === types[index].id
)), )),
@@ -205,6 +212,12 @@ export class ItemDeleteComponent
*/ */
private getRelationshipType(relationship: Relationship): Observable<RelationshipType> { private getRelationshipType(relationship: Relationship): Observable<RelationshipType> {
this.linkService.resolveLinks(
relationship,
followLink('relationshipType'),
followLink('leftItem'),
followLink('rightItem'),
);
return relationship.relationshipType.pipe( return relationship.relationshipType.pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
@@ -305,6 +318,7 @@ export class ItemDeleteComponent
combineLatest( combineLatest(
types.map((type) => this.isSelected(type)) types.map((type) => this.isSelected(type))
).pipe( ).pipe(
defaultIfEmpty([]),
map((selection) => types.filter( map((selection) => types.filter(
(type, index) => selection[index] (type, index) => selection[index]
)), )),

View File

@@ -1,48 +1,56 @@
<div class="item-relationships"> <div class="item-relationships">
<div class="button-row top d-flex"> <ng-container *ngVar="entityType$ | async as entityType">
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)" <ng-container *ngIf="entityType">
[disabled]="!(hasChanges() | async)" <div class="button-row top d-flex">
(click)="discard()"><i <button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)"
class="fas fa-times"></i> [disabled]="!(hasChanges() | async)"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> (click)="discard()"><i
</button> class="fas fa-times"></i>
<button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async" <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
(click)="reinstate()"><i </button>
class="fas fa-undo-alt"></i> <button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> (click)="reinstate()"><i
</button> class="fas fa-undo-alt"></i>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)" <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
(click)="submit()"><i </button>
class="fas fa-save"></i> <button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> (click)="submit()"><i
</button> class="fas fa-save"></i>
</div> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
<div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4"> </button>
<ds-edit-relationship-list </div>
[url]="url" <div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
[item]="item" <ds-edit-relationship-list
[itemType]="entityType$ | async" [url]="url"
[relationshipType]="relationshipType" [item]="item"
></ds-edit-relationship-list> [itemType]="entityType"
</div> [relationshipType]="relationshipType"
<div class="button-row bottom"> ></ds-edit-relationship-list>
<div class="float-right"> </div>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <div class="button-row bottom">
[disabled]="!(hasChanges() | async)" <div class="float-right">
(click)="discard()"><i <button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
class="fas fa-times"></i> [disabled]="!(hasChanges() | async)"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span> (click)="discard()"><i
</button> class="fas fa-times"></i>
<button class="btn btn-warning" *ngIf="isReinstatable() | async" <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
(click)="reinstate()"><i </button>
class="fas fa-undo-alt"></i> <button class="btn btn-warning" *ngIf="isReinstatable() | async"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span> (click)="reinstate()"><i
</button> class="fas fa-undo-alt"></i>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)" <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
(click)="submit()"><i </button>
class="fas fa-save"></i> <button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> (click)="submit()"><i
</button> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
</div>
</div>
</ng-container>
<div *ngIf="!entityType"
class="alert alert-info mt-2" role="alert">
{{ 'item.edit.relationships.no-entity-type' | translate }}
</div> </div>
</div> </ng-container>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { Item } from '../../../core/shared/item.model';
import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { zip as observableZip } from 'rxjs'; import { of as observableOf, zip as observableZip} from 'rxjs';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
@@ -87,26 +87,30 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
*/ */
public initializeUpdates(): void { public initializeUpdates(): void {
this.entityType$ = this.entityTypeService.getEntityTypeByLabel( const label = this.item.firstMetadataValue('relationship.type');
this.item.firstMetadataValue('relationship.type') if (label !== undefined) {
).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
);
this.relationshipTypes$ = this.entityType$.pipe( this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
switchMap((entityType) => getSucceededRemoteData(),
this.entityTypeService.getEntityTypeRelationships( getRemoteDataPayload(),
entityType.id, );
followLink('leftType'),
followLink('rightType')) this.relationshipTypes$ = this.entityType$.pipe(
.pipe( switchMap((entityType) =>
getSucceededRemoteData(), this.entityTypeService.getEntityTypeRelationships(
getRemoteDataPayload(), entityType.id,
map((relationshipTypes) => relationshipTypes.page), followLink('leftType'),
) followLink('rightType'))
), .pipe(
); getSucceededRemoteData(),
getRemoteDataPayload(),
map((relationshipTypes) => relationshipTypes.page),
)
),
);
} else {
this.entityType$ = observableOf(undefined);
}
} }
/** /**

View File

@@ -27,9 +27,6 @@ export class ServerAuthService extends AuthService {
headers = headers.append('Accept', 'application/json'); headers = headers.append('Accept', 'application/json');
headers = headers.append('Authorization', `Bearer ${token.accessToken}`); headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
// NB this is used to pass server client IP check.
const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress;
headers = headers.append('X-Forwarded-For', clientIp);
options.headers = headers; options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe( return this.authRequestService.getRequest('status', options).pipe(

View File

@@ -0,0 +1,44 @@
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpRequest } from '@angular/common/http';
import { REQUEST } from '@nguniversal/express-engine/tokens';
describe('ForwardClientIpInterceptor', () => {
let service: DSpaceRESTv2Service;
let httpMock: HttpTestingController;
let requestUrl;
let clientIp;
beforeEach(() => {
requestUrl = 'test-url';
clientIp = '1.2.3.4';
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
DSpaceRESTv2Service,
{
provide: HTTP_INTERCEPTORS,
useClass: ForwardClientIpInterceptor,
multi: true,
},
{ provide: REQUEST, useValue: { get: () => undefined, connection: { remoteAddress: clientIp } }}
],
});
service = TestBed.get(DSpaceRESTv2Service);
httpMock = TestBed.get(HttpTestingController);
});
it('should add an X-Forwarded-For header matching the client\'s IP', () => {
service.get(requestUrl).subscribe((response) => {
expect(response).toBeTruthy();
});
const httpRequest = httpMock.expectOne(requestUrl);
expect(httpRequest.request.headers.get('X-Forwarded-For')).toEqual(clientIp);
});
});

View File

@@ -0,0 +1,23 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { REQUEST } from '@nguniversal/express-engine/tokens';
@Injectable()
/**
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
*/
export class ForwardClientIpInterceptor implements HttpInterceptor {
constructor(@Inject(REQUEST) protected req: any) {
}
/**
* Intercept http requests and add the client's IP to the X-Forwarded-For header
* @param httpRequest
* @param next
*/
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress;
return next.handle(httpRequest.clone({ setHeaders: { 'X-Forwarded-For': clientIp } }));
}
}

View File

@@ -19,6 +19,7 @@ describe('ProcessFormComponent', () => {
let component: ProcessFormComponent; let component: ProcessFormComponent;
let fixture: ComponentFixture<ProcessFormComponent>; let fixture: ComponentFixture<ProcessFormComponent>;
let scriptService; let scriptService;
let router;
let parameterValues; let parameterValues;
let script; let script;
@@ -41,7 +42,10 @@ describe('ProcessFormComponent', () => {
} }
}) })
} }
) );
router = {
navigateByUrl: () => undefined,
};
} }
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -1378,6 +1378,8 @@
"item.edit.relationships.save-button": "Save", "item.edit.relationships.save-button": "Save",
"item.edit.relationships.no-entity-type": "Add 'relationship.type' metadata to enable relationships for this item",
"item.edit.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.head": "Bitstreams",

View File

@@ -27,6 +27,8 @@ import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { ServerLocaleService } from 'src/app/core/locale/server-locale.service'; import { ServerLocaleService } from 'src/app/core/locale/server-locale.service';
import { LocaleService } from 'src/app/core/locale/locale.service'; import { LocaleService } from 'src/app/core/locale/locale.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ForwardClientIpInterceptor } from '../../app/core/forward-client-ip/forward-client-ip.interceptor';
export function createTranslateLoader() { export function createTranslateLoader() {
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5'); return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
@@ -79,7 +81,13 @@ export function createTranslateLoader() {
{ {
provide: LocaleService, provide: LocaleService,
useClass: ServerLocaleService useClass: ServerLocaleService
} },
// register ForwardClientIpInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,
useClass: ForwardClientIpInterceptor,
multi: true
},
] ]
}) })
export class ServerAppModule { export class ServerAppModule {