Merge remote-tracking branch 'origin/main' into CST-4633-search-refactoring

This commit is contained in:
Giuseppe Digilio
2022-01-13 09:42:44 +01:00
19 changed files with 229 additions and 71 deletions

View File

@@ -359,7 +359,7 @@ dspace-angular
│ ├── plugins * Folder for Cypress plugins (if any) │ ├── plugins * Folder for Cypress plugins (if any)
│ ├── support * Folder for global e2e test actions/commands (run for all tests) │ ├── support * Folder for global e2e test actions/commands (run for all tests)
│ └── tsconfig.json * TypeScript configuration file for e2e tests │ └── tsconfig.json * TypeScript configuration file for e2e tests
├── docker * ├── docker * See docker/README.md for details
│ ├── cli.assetstore.yml * │ ├── cli.assetstore.yml *
│ ├── cli.ingest.yml * │ ├── cli.ingest.yml *
│ ├── cli.yml * │ ├── cli.yml *
@@ -367,8 +367,6 @@ dspace-angular
│ ├── docker-compose-ci.yml * │ ├── docker-compose-ci.yml *
│ ├── docker-compose-rest.yml * │ ├── docker-compose-rest.yml *
│ ├── docker-compose.yml * │ ├── docker-compose.yml *
│ ├── environment.dev.ts *
│ ├── local.cfg *
│ └── README.md * │ └── README.md *
├── docs * Folder for documentation ├── docs * Folder for documentation
│ └── Configuration.md * Configuration documentation │ └── Configuration.md * Configuration documentation

View File

@@ -29,10 +29,6 @@ docker push dspace/dspace-angular:dspace-7_x
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
- cli.assetstore.yml - cli.assetstore.yml
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing. - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
- environment.dev.ts
- Environment file for running DSpace Angular in Docker
- local.cfg
- Environment file for running the DSpace 7 REST API in Docker.
## To refresh / pull DSpace images from Dockerhub ## To refresh / pull DSpace images from Dockerhub

View File

@@ -18,10 +18,19 @@ services:
dspace-cli: dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
container_name: dspace-cli container_name: dspace-cli
#environment: environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir
dspace__P__dir: /dspace
# db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
solr__P__server: http://dspacesolr:8983/solr
volumes: volumes:
- "assetstore:/dspace/assetstore" - "assetstore:/dspace/assetstore"
- "./local.cfg:/dspace/config/local.cfg"
entrypoint: /dspace/bin/dspace entrypoint: /dspace/bin/dspace
command: help command: help
networks: networks:

View File

@@ -17,6 +17,19 @@ services:
# DSpace (backend) webapp container # DSpace (backend) webapp container
dspace: dspace:
container_name: dspace container_name: dspace
environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url and dspace.ui.url
dspace__P__dir: /dspace
dspace__P__server__P__url: http://localhost:8080/server
dspace__P__ui__P__url: http://localhost:4000
# db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
solr__P__server: http://dspacesolr:8983/solr
depends_on: depends_on:
- dspacedb - dspacedb
image: dspace/dspace:dspace-7_x-test image: dspace/dspace:dspace-7_x-test
@@ -29,7 +42,6 @@ services:
tty: true tty: true
volumes: volumes:
- assetstore:/dspace/assetstore - assetstore:/dspace/assetstore
- "./local.cfg:/dspace/config/local.cfg"
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
- solr_configs:/dspace/solr - solr_configs:/dspace/solr
# Ensure that the database is ready BEFORE starting tomcat # Ensure that the database is ready BEFORE starting tomcat

View File

@@ -13,10 +13,32 @@
version: '3.7' version: '3.7'
networks: networks:
dspacenet: dspacenet:
ipam:
config:
# Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container.
# If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below.
- subnet: 172.23.0.0/16
services: services:
# DSpace (backend) webapp container # DSpace (backend) webapp container
dspace: dspace:
container_name: dspace container_name: dspace
environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
dspace__P__dir: /dspace
dspace__P__server__P__url: http://localhost:8080/server
dspace__P__ui__P__url: http://localhost:4000
dspace__P__name: 'DSpace Started with Docker Compose'
# db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
solr__P__server: http://dspacesolr:8983/solr
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
proxies__P__trusted__P__ipranges: '172.23.0'
image: dspace/dspace:dspace-7_x-test image: dspace/dspace:dspace-7_x-test
depends_on: depends_on:
- dspacedb - dspacedb
@@ -29,7 +51,6 @@ services:
tty: true tty: true
volumes: volumes:
- assetstore:/dspace/assetstore - assetstore:/dspace/assetstore
- "./local.cfg:/dspace/config/local.cfg"
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
- solr_configs:/dspace/solr - solr_configs:/dspace/solr
# Ensure that the database is ready BEFORE starting tomcat # Ensure that the database is ready BEFORE starting tomcat

View File

@@ -16,10 +16,14 @@ services:
dspace-angular: dspace-angular:
container_name: dspace-angular container_name: dspace-angular
environment: environment:
DSPACE_HOST: dspace-angular DSPACE_UI_SSL: false
DSPACE_NAMESPACE: / DSPACE_UI_HOST: dspace-angular
DSPACE_PORT: '4000' DSPACE_UI_PORT: '4000'
DSPACE_SSL: "false" DSPACE_UI_NAMESPACE: /
DSPACE_REST_SSL: false
DSPACE_REST_HOST: localhost
DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: /server
image: dspace/dspace-angular:dspace-7_x image: dspace/dspace-angular:dspace-7_x
build: build:
context: .. context: ..
@@ -33,5 +37,3 @@ services:
target: 9876 target: 9876
stdin_open: true stdin_open: true
tty: true tty: true
volumes:
- ./environment.dev.ts:/app/src/environments/environment.dev.ts

View File

@@ -1,18 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
// This file is based on environment.template.ts provided by Angular UI
export const environment = {
// Default to using the local REST API (running in Docker)
rest: {
ssl: false,
host: 'localhost',
port: 8080,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: '/server'
}
};

View File

@@ -1,6 +0,0 @@
dspace.dir=/dspace
db.url=jdbc:postgresql://dspacedb:5432/dspace
dspace.server.url=http://localhost:8080/server
dspace.ui.url=http://localhost:4000
dspace.name=DSpace Started with Docker Compose
solr.server=http://dspacesolr:8983/solr

View File

@@ -19,7 +19,7 @@
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button> class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div> </div>
<div between class="btn-group"> <div between class="btn-group">
<button class="btn btn-primary" [disabled]="!(canReset$ | async)"> <button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}} <i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button> </button>
</div> </div>
@@ -36,9 +36,13 @@
</button> </button>
</ds-form> </ds-form>
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
<div *ngIf="epersonService.getActiveEPerson() | async"> <div *ngIf="epersonService.getActiveEPerson() | async">
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5> <h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
<ds-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-loading>
<ds-pagination <ds-pagination
*ngIf="(groups | async)?.payload?.totalElements > 0" *ngIf="(groups | async)?.payload?.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="config"

View File

@@ -2,7 +2,7 @@ import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -14,6 +14,7 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPeopleRegistryComponent } from '../epeople-registry.component';
import { EPersonFormComponent } from './eperson-form.component'; import { EPersonFormComponent } from './eperson-form.component';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
@@ -28,9 +29,8 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
describe('EPersonFormComponent', () => { describe('EPersonFormComponent', () => {
let component: EPersonFormComponent; let component: EPersonFormComponent;
@@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => {
let authService: AuthServiceStub; let authService: AuthServiceStub;
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let groupsDataService: GroupDataService; let groupsDataService: GroupDataService;
let epersonRegistrationService: EpersonRegistrationService;
let paginationService; let paginationService;
@@ -199,12 +200,18 @@ describe('EPersonFormComponent', () => {
{ provide: AuthService, useValue: authService }, { provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) } { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
EPeopleRegistryComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
registerEmail: createSuccessfulRemoteDataObject$(null)
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(EPersonFormComponent); fixture = TestBed.createComponent(EPersonFormComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -514,4 +521,23 @@ describe('EPersonFormComponent', () => {
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson); expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
}); });
}); });
describe('Reset Password', () => {
let ePersonId;
let ePersonEmail;
beforeEach(() => {
ePersonId = 'testEPersonId';
ePersonEmail = 'person.email@4science.it';
component.epersonInitial = Object.assign(new EPerson(), {
id: ePersonId,
email: ePersonEmail
});
component.resetPassword();
});
it('should call epersonRegistrationService.registerEmail', () => {
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
});
});
}); });

View File

@@ -34,6 +34,8 @@ import { NoContent } from '../../../core/shared/NoContent.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { Registration } from '../../../core/shared/registration.model';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
@Component({ @Component({
selector: 'ds-eperson-form', selector: 'ds-eperson-form',
@@ -121,7 +123,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* Observable whether or not the admin is allowed to reset the EPerson's password * Observable whether or not the admin is allowed to reset the EPerson's password
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false) * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
*/ */
canReset$: Observable<boolean> = observableOf(false); canReset$: Observable<boolean>;
/** /**
* Observable whether or not the admin is allowed to delete the EPerson * Observable whether or not the admin is allowed to delete the EPerson
@@ -167,17 +169,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/ */
emailValueChangeSubscribe: Subscription; emailValueChangeSubscribe: Subscription;
constructor(protected changeDetectorRef: ChangeDetectorRef, constructor(
public epersonService: EPersonDataService, protected changeDetectorRef: ChangeDetectorRef,
public groupsDataService: GroupDataService, public epersonService: EPersonDataService,
private formBuilderService: FormBuilderService, public groupsDataService: GroupDataService,
private translateService: TranslateService, private formBuilderService: FormBuilderService,
private notificationsService: NotificationsService, private translateService: TranslateService,
private authService: AuthService, private notificationsService: NotificationsService,
private authorizationService: AuthorizationDataService, private authService: AuthService,
private modalService: NgbModal, private authorizationService: AuthorizationDataService,
private paginationService: PaginationService, private modalService: NgbModal,
public requestService: RequestService) { private paginationService: PaginationService,
public requestService: RequestService,
private epersonRegistrationService: EpersonRegistrationService,
) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.epersonInitial = eperson; this.epersonInitial = eperson;
if (hasValue(eperson)) { if (hasValue(eperson)) {
@@ -310,6 +315,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.canDelete$ = activeEPerson$.pipe( this.canDelete$ = activeEPerson$.pipe(
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
); );
this.canReset$ = observableOf(true);
}); });
} }
@@ -479,6 +485,26 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.isImpersonated = false; this.isImpersonated = false;
} }
/**
* Sends an email to current eperson address with the information
* to reset password
*/
resetPassword() {
if (hasValue(this.epersonInitial.email)) {
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
.subscribe((response: RemoteData<Registration>) => {
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email}));
} else {
this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'),
this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email}));
}
}
);
}
}
/** /**
* Cancel the current edit when component is destroyed & unsub all subscriptions * Cancel the current edit when component is destroyed & unsub all subscriptions
*/ */

View File

@@ -13,6 +13,7 @@ export enum FeatureID {
CanManageGroup = 'canManageGroup', CanManageGroup = 'canManageGroup',
IsCollectionAdmin = 'isCollectionAdmin', IsCollectionAdmin = 'isCollectionAdmin',
IsCommunityAdmin = 'isCommunityAdmin', IsCommunityAdmin = 'isCommunityAdmin',
CanChangePassword = 'canChangePassword',
CanDownload = 'canDownload', CanDownload = 'canDownload',
CanRequestACopy = 'canRequestACopy', CanRequestACopy = 'canRequestACopy',
CanManageVersions = 'canManageVersions', CanManageVersions = 'canManageVersions',

View File

@@ -47,6 +47,12 @@ export class Version extends DSpaceObject {
@autoserialize @autoserialize
summary: string; summary: string;
/**
* The name of the submitter of this version
*/
@autoserialize
submitterName: string;
/** /**
* The Date this version was created * The Date this version was created
*/ */

View File

@@ -7,7 +7,7 @@
<ds-profile-page-metadata-form [user]="user"></ds-profile-page-metadata-form> <ds-profile-page-metadata-form [user]="user"></ds-profile-page-metadata-form>
</div> </div>
</div> </div>
<div class="card mb-4"> <div *ngIf="canChangePassword$ | async" class="card mb-4 security-section">
<div class="card-header">{{'profile.card.security' | translate}}</div> <div class="card-header">{{'profile.card.security' | translate}}</div>
<div class="card-body"> <div class="card-body">
<ds-profile-page-security-form <ds-profile-page-security-form

View File

@@ -13,10 +13,13 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { authReducer } from '../core/auth/auth.reducer'; import { authReducer } from '../core/auth/auth.reducer';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { createPaginatedList } from '../shared/testing/utils.test'; import { createPaginatedList } from '../shared/testing/utils.test';
import { of as observableOf } from 'rxjs'; import { BehaviorSubject, of as observableOf } from 'rxjs';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { RestResponse } from '../core/cache/response.models'; import { RestResponse } from '../core/cache/response.models';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { getTestScheduler } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
describe('ProfilePageComponent', () => { describe('ProfilePageComponent', () => {
let component: ProfilePageComponent; let component: ProfilePageComponent;
@@ -28,10 +31,13 @@ describe('ProfilePageComponent', () => {
let epersonService; let epersonService;
let notificationsService; let notificationsService;
const canChangePassword = new BehaviorSubject(true);
function init() { function init() {
user = Object.assign(new EPerson(), { user = Object.assign(new EPerson(), {
id: 'userId', id: 'userId',
groups: createSuccessfulRemoteDataObject$(createPaginatedList([])) groups: createSuccessfulRemoteDataObject$(createPaginatedList([])),
_links: {self: {href: 'test.com/uuid/1234567654321'}}
}); });
initialState = { initialState = {
core: { core: {
@@ -74,6 +80,7 @@ describe('ProfilePageComponent', () => {
{ provide: EPersonDataService, useValue: epersonService }, { provide: EPersonDataService, useValue: epersonService },
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: authService }, { provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) },
provideMockStore({ initialState }), provideMockStore({ initialState }),
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -183,7 +190,7 @@ describe('ProfilePageComponent', () => {
component.setPasswordValue('testest'); component.setPasswordValue('testest');
component.setInvalid(false); component.setInvalid(false);
operations = [{op: 'add', path: '/password', value: 'testest'}]; operations = [{ op: 'add', path: '/password', value: 'testest' }];
result = component.updateSecurity(); result = component.updateSecurity();
}); });
@@ -196,4 +203,36 @@ describe('ProfilePageComponent', () => {
}); });
}); });
}); });
describe('canChangePassword$', () => {
describe('when the user is allowed to change their password', () => {
beforeEach(() => {
canChangePassword.next(true);
});
it('should contain true', () => {
getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true });
});
it('should show the security section on the page', () => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull();
});
});
describe('when the user is not allowed to change their password', () => {
beforeEach(() => {
canChangePassword.next(false);
});
it('should contain false', () => {
getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false });
});
it('should not show the security section on the page', () => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull();
});
});
});
}); });

View File

@@ -18,6 +18,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model'; import { followLink } from '../shared/utils/follow-link-config.model';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
@Component({ @Component({
selector: 'ds-profile-page', selector: 'ds-profile-page',
@@ -67,11 +69,13 @@ export class ProfilePageComponent implements OnInit {
* The authenticated user * The authenticated user
*/ */
private currentUser: EPerson; private currentUser: EPerson;
canChangePassword$: Observable<boolean>;
constructor(private authService: AuthService, constructor(private authService: AuthService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translate: TranslateService, private translate: TranslateService,
private epersonService: EPersonDataService) { private epersonService: EPersonDataService,
private authorizationService: AuthorizationDataService) {
} }
ngOnInit(): void { ngOnInit(): void {
@@ -83,6 +87,7 @@ export class ProfilePageComponent implements OnInit {
tap((user: EPerson) => this.currentUser = user) tap((user: EPerson) => this.currentUser = user)
); );
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups)); this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href)));
} }
/** /**

View File

@@ -17,7 +17,7 @@
<thead> <thead>
<tr> <tr>
<th scope="col">{{"item.version.history.table.version" | translate}}</th> <th scope="col">{{"item.version.history.table.version" | translate}}</th>
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th> <th scope="col" *ngIf="(showSubmitter() | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th> <th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th> <th scope="col">{{"item.version.history.table.summary" | translate}}</th>
</tr> </tr>
@@ -87,10 +87,8 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
</td> </td>
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor"> <td class="version-row-element-editor" *ngIf="(showSubmitter() | async)">
<span *ngVar="(version?.eperson | async)?.payload as eperson"> {{version?.submitterName}}
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
</span>
</td> </td>
<td class="version-row-element-date"> <td class="version-row-element-date">
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}} {{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}

View File

@@ -24,6 +24,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
describe('ItemVersionsComponent', () => { describe('ItemVersionsComponent', () => {
let component: ItemVersionsComponent; let component: ItemVersionsComponent;
@@ -34,6 +35,7 @@ describe('ItemVersionsComponent', () => {
let workspaceItemDataService: WorkspaceitemDataService; let workspaceItemDataService: WorkspaceitemDataService;
let workflowItemDataService: WorkflowItemDataService; let workflowItemDataService: WorkflowItemDataService;
let versionService: VersionDataService; let versionService: VersionDataService;
let configurationService: ConfigurationDataService;
const versionHistory = Object.assign(new VersionHistory(), { const versionHistory = Object.assign(new VersionHistory(), {
id: '1', id: '1',
@@ -109,6 +111,10 @@ describe('ItemVersionsComponent', () => {
findById: EMPTY, findById: EMPTY,
}); });
const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
findByPropertyName: of(true),
});
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -125,6 +131,7 @@ describe('ItemVersionsComponent', () => {
{provide: VersionDataService, useValue: versionServiceSpy}, {provide: VersionDataService, useValue: versionServiceSpy},
{provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy}, {provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy},
{provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy}, {provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy},
{provide: ConfigurationDataService, useValue: configurationServiceSpy},
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -135,6 +142,7 @@ describe('ItemVersionsComponent', () => {
workspaceItemDataService = TestBed.inject(WorkspaceitemDataService); workspaceItemDataService = TestBed.inject(WorkspaceitemDataService);
workflowItemDataService = TestBed.inject(WorkflowItemDataService); workflowItemDataService = TestBed.inject(WorkflowItemDataService);
versionService = TestBed.inject(VersionDataService); versionService = TestBed.inject(VersionDataService);
configurationService = TestBed.inject(ConfigurationDataService);
})); }));

View File

@@ -5,7 +5,6 @@ import { RemoteData } from '../../../core/data/remote-data';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
combineLatest as observableCombineLatest,
Observable, Observable,
of, of,
Subscription, Subscription,
@@ -48,6 +47,7 @@ import { ItemVersionsSharedService } from './item-versions-shared.service';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
@Component({ @Component({
selector: 'ds-item-versions', selector: 'ds-item-versions',
@@ -180,6 +180,7 @@ export class ItemVersionsComponent implements OnInit {
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private workspaceItemDataService: WorkspaceitemDataService, private workspaceItemDataService: WorkspaceitemDataService,
private workflowItemDataService: WorkflowItemDataService, private workflowItemDataService: WorkflowItemDataService,
private configurationService: ConfigurationDataService,
) { ) {
} }
@@ -375,6 +376,36 @@ export class ItemVersionsComponent implements OnInit {
return this.authorizationService.isAuthorized(FeatureID.CanEditVersion, version.self); return this.authorizationService.isAuthorized(FeatureID.CanEditVersion, version.self);
} }
/**
* Show submitter in version history table
*/
showSubmitter() {
const includeSubmitter$ = this.configurationService.findByPropertyName('versioning.item.history.include.submitter').pipe(
getFirstSucceededRemoteDataPayload(),
map((configurationProperty) => configurationProperty.values[0]),
startWith(false),
);
const isAdmin$ = combineLatest([
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
]).pipe(
map(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
return isCollectionAdmin || isCommunityAdmin || isSiteAdmin;
}),
take(1),
);
return combineLatest([includeSubmitter$, isAdmin$]).pipe(
map(([includeSubmitter, isAdmin]) => {
return includeSubmitter && isAdmin;
})
);
}
/** /**
* Check if the current user can delete the version * Check if the current user can delete the version
* @param version * @param version
@@ -389,7 +420,7 @@ export class ItemVersionsComponent implements OnInit {
*/ */
getAllVersions(versionHistory$: Observable<VersionHistory>): void { getAllVersions(versionHistory$: Observable<VersionHistory>): void {
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options); const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
observableCombineLatest([versionHistory$, currentPagination]).pipe( combineLatest([versionHistory$, currentPagination]).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => { switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
return this.versionHistoryService.getVersions(versionHistory.id, return this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}), new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
@@ -486,7 +517,7 @@ export class ItemVersionsComponent implements OnInit {
); );
this.itemPageRoutes$ = this.versionsRD$.pipe( this.itemPageRoutes$ = this.versionsRD$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
switchMap((versions) => observableCombineLatest(...versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))), switchMap((versions) => combineLatest(versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))),
map((versions) => { map((versions) => {
const itemPageRoutes = {}; const itemPageRoutes = {};
versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item)); versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item));