71712: Export metadata CSV; WIP

This commit is contained in:
Marie Verdonck
2020-07-06 19:18:40 +02:00
parent eb9809801f
commit 902d4c2330
10 changed files with 205 additions and 66 deletions

View File

@@ -2,8 +2,13 @@ import { Component, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { combineLatest as combineLatestObservable } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { first, map } from 'rxjs/operators';
import { of } from 'rxjs/internal/observable/of';
import { first, map, take } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { ProcessDataService } from '../../core/data/processes/process-data.service';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { Script } from '../../process-page/scripts/script.model';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
@@ -11,6 +16,10 @@ import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/mod
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import {
ExportMetadataSelectorComponent,
METADATA_EXPORT_SCRIPT_NAME
} from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
@@ -64,7 +73,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
private variableService: CSSVariableService,
private authService: AuthService,
private modalService: NgbModal,
private authorizationService: AuthorizationDataService
private authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService,
) {
super(menuService, injector);
}
@@ -75,6 +85,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
ngOnInit(): void {
this.createMenu();
this.createSiteAdministratorMenuSections();
this.createExportMenuSections();
super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.authService.isAuthenticated()
@@ -259,61 +270,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
link: ''
} as LinkMenuItemModel,
},
/* Export */
{
id: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.export'
} as TextMenuItemModel,
icon: 'sign-out-alt',
index: 3
},
{
id: 'export_community',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_community',
link: ''
} as LinkMenuItemModel,
},
{
id: 'export_collection',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_collection',
link: ''
} as LinkMenuItemModel,
},
{
id: 'export_item',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_item',
link: ''
} as LinkMenuItemModel,
}, {
id: 'export_metadata',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_metadata',
link: ''
} as LinkMenuItemModel,
},
/* Statistics */
{
@@ -362,6 +318,85 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
})));
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
* the export scripts exist and the current user is allowed to execute them
*/
createExportMenuSections() {
const isAuthorized$: Observable<boolean> = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
isAuthorized$.subscribe((authorized: boolean) => {
if (authorized) {
const metadataExportScriptExists$ = this.scriptDataService.scripWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME);
metadataExportScriptExists$.subscribe((metadataExportScriptExists: boolean) => {
const menuList = [
/* Export */
{
id: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.export'
} as TextMenuItemModel,
icon: 'sign-out-alt',
index: 3
},
{
id: 'export_community',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_community',
link: ''
} as LinkMenuItemModel,
},
{
id: 'export_collection',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_collection',
link: ''
} as LinkMenuItemModel,
},
{
id: 'export_item',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.export_item',
link: ''
} as LinkMenuItemModel,
},
{
id: 'export_metadata',
parentID: 'export',
active: true,
visible: authorized && metadataExportScriptExists,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.export_metadata',
function: () => {
this.modalService.open(ExportMetadataSelectorComponent);
}
} as OnClickMenuItemModel,
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
}
});
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator
*/

View File

@@ -21,7 +21,6 @@ import { HostWindowState } from './shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
import { isAuthenticated } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service';
import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/initial-menus-state';

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../shared/operators';
import { DataService } from '../data.service';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
@@ -12,6 +13,7 @@ import { Script } from '../../../process-page/scripts/script.model';
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
import { find, map, switchMap } from 'rxjs/operators';
import { URLCombiner } from '../../url-combiner/url-combiner';
import { PaginatedList } from '../paginated-list';
import { MultipartPostRequest, RestRequest } from '../request.models';
import { RequestService } from '../request.service';
import { Observable } from 'rxjs';
@@ -58,4 +60,23 @@ export class ScriptDataService extends DataService<Script> {
});
return form;
}
/**
* Check whether a script with given name exist; user needs to be allowed to execute script for this to to not throw a 401 Unauthorized
* @param scriptName script we want to check exists (and we can execute)
*/
public scripWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
return this.findAll().pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((scriptsRD: PaginatedList<Script>) => {
let found = false;
scriptsRD.page.forEach((script: Script) => {
if (script.id === scriptName) {
found = true;
}
});
return found;
}));
}
}

View File

@@ -4,4 +4,5 @@ export enum DSpaceObjectType {
ITEM = 'ITEM',
COLLECTION = 'COLLECTION',
COMMUNITY = 'COMMUNITY',
DSPACEOBJECT = 'DSPACEOBJECT',
}

View File

@@ -93,7 +93,7 @@ export class DSOSelectorComponent implements OnInit {
return this.searchService.search(
new PaginatedSearchOptions({
query: query,
dsoType: this.type,
dsoType: this.type != DSpaceObjectType.DSPACEOBJECT ? this.type : null,
pagination: this.defaultPagination
})
)

View File

@@ -1,16 +1,15 @@
import { Injectable, Input, OnInit } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { RemoteData } from '../../../core/data/remote-data';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { hasValue, isNotEmpty } from '../../empty.util';
export enum SelectorActionType {
CREATE = 'create',
EDIT = 'edit'
EDIT = 'edit',
EXPORT_METADATA = 'export-metadata'
}
/**
@@ -25,7 +24,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
@Input() dsoRD: RemoteData<DSpaceObject>;
/**
* The type of the DSO that's being edited or created
* The type of the DSO that's being edited, created or exported
*/
objectType: DSpaceObjectType;

View File

@@ -1,12 +1,10 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { EditItemSelectorComponent } from './edit-item-selector.component';
import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterStub } from '../../../testing/router.stub';
import * as itemRouter from '../../../../+item-page/item-page-routing.module';
import { MetadataValue } from '../../../../core/shared/metadata.models';

View File

@@ -0,0 +1,79 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { RequestEntry } from '../../../../core/data/request.reducer';
import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service';
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
export const METADATA_EXPORT_SCRIPT_NAME: string = 'metadata-export';
/**
* Component to wrap a list of existing dso's inside a modal
* Used to choose a dso from to export metadata of
*/
@Component({
selector: 'ds-edit-item-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.DSPACEOBJECT;
selectorType = DSpaceObjectType.DSPACEOBJECT;
action = SelectorActionType.EXPORT_METADATA;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService) {
super(activeModal, route);
}
/**
* If the dso is a collection or community: start export-metadata script & navigate to process if successful
* Otherwise show error message
*/
navigate(dso: DSpaceObject) {
if (dso instanceof Collection || dso instanceof Community) {
this.startScriptNotifyAndRedirect(dso, dso.handle);
} else {
this.notificationsService.error(this.translationService.get('dso-selector.export-metadata.notValidDSO'));
}
}
/**
* Start export-metadata script of dso & navigate to process if successful
* Otherwise show error message
* @param dso Dso to export
*/
private startScriptNotifyAndRedirect(dso: DSpaceObject, handle: string) {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: handle }),
Object.assign(new ProcessParameter(), { name: '-f', value: dso.uuid + '.csv' }),
];
this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, [])
.pipe(take(1))
.subscribe((requestEntry: RequestEntry) => {
if (requestEntry.response.isSuccessful) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content);
const response: any = requestEntry.response;
if (isNotEmpty(response.resourceSelfLinks)) {
const processNumber = response.resourceSelfLinks[0].split('/').pop();
this.router.navigateByUrl('/processes/' + processNumber);
}
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content);
}
});
}
}

View File

@@ -10,6 +10,7 @@ import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'
import { NgxPaginationModule } from 'ngx-pagination';
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
import { FileUploadModule } from 'ng2-file-upload';
@@ -397,7 +398,8 @@ const COMPONENTS = [
EpersonSearchBoxComponent,
GroupSearchBoxComponent,
FileDownloadLinkComponent,
CollectionDropdownComponent
CollectionDropdownComponent,
ExportMetadataSelectorComponent
];
const ENTRY_COMPONENTS = [
@@ -474,6 +476,7 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsEditMetadataComponent,
FileDownloadLinkComponent,
CurationFormComponent,
ExportMetadataSelectorComponent
];
const SHARED_ITEM_PAGE_COMPONENTS = [

View File

@@ -965,6 +965,10 @@
"dso-selector.edit.item.head": "Edit item",
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
"dso-selector.export-metadata.notValidDSO": "You can only export metadata for collections and communities",
"dso-selector.no-results": "No {{ type }} found",
"dso-selector.placeholder": "Search for a {{ type }}",