1
0

71713: Import metadata CSV

This commit is contained in:
Marie Verdonck
2020-07-07 18:46:50 +02:00
parent d295fa422f
commit 2898cbac6d
10 changed files with 322 additions and 47 deletions

View File

@@ -0,0 +1,34 @@
<div class="container">
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
<p>{{'admin.metadata-import.page.help' | translate}}</p>
<div ng2FileDrop
class="ds-document-drop-zone position-fixed h-100 w-100"
[class.ds-document-drop-zone-active]="(isOverDocumentDropZone | async)"
[uploader]="uploader"
(onFileDrop)="setFile($event)"
(fileOver)="fileOverDocument($event)">
</div>
<div *ngIf="(isOverDocumentDropZone | async)"
class="ds-document-drop-zone-inner position-fixed h-100 w-100 p-2">
<div
class="ds-document-drop-zone-inner-content position-relative d-flex flex-column justify-content-center text-center h-100 w-100">
<p class="text-primary">{{'admin.metadata-import.page.dropMsg' | translate}}</p>
</div>
</div>
<div class="well ds-base-drop-zone mt-1 mb-3 text-muted">
<label for="file-upload" class="d-flex align-items-center m-0">
<span class="btn btn-light">
{{'process.new.parameter.file.upload-button' | translate}}
</span>
<span class="file-name ml-1">{{fileObject?.name}}</span>
</label>
<input requireFile #file="ngModel" type="file" name="file-upload" id="file-upload" class="form-control-file d-none"
[ngModel]="fileObject" (ngModelChange)="setFile($event)"/>
</div>
<button class="btn btn-secondary"
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
<button class="btn btn-primary"
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
</div>

View File

@@ -0,0 +1,29 @@
.ds-base-drop-zone {
border: 2px dashed $gray-600;
}
.ds-document-drop-zone {
top: 0;
left: 0;
z-index: -1;
}
.ds-document-drop-zone-active {
z-index: $drop-zone-area-z-index !important;
}
.ds-document-drop-zone-inner {
background-color: rgba($white, 0.7);
z-index: $drop-zone-area-inner-z-index;
top: 0;
left: 0;
}
.ds-document-drop-zone-inner-content {
border: 4px dashed map-get($theme-colors, primary);
z-index: $drop-zone-area-inner-z-index;
}
.ds-document-drop-zone-inner-content p {
font-size: ($font-size-lg * 2.5);
}

View File

@@ -0,0 +1,149 @@
import { Location } from '@angular/common';
import { Component, HostListener, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { uniqueId } from 'lodash';
import { FileUploader } from 'ng2-file-upload';
import { Observable } from 'rxjs/internal/Observable';
import { filter, map, take, tap } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import { RequestEntry } from '../../core/data/request.reducer';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { of as observableOf } from 'rxjs';
import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
@Component({
selector: 'ds-metadata-import-page',
templateUrl: './metadata-import-page.component.html',
styleUrls: ['./metadata-import-page.component.scss']
})
/**
* Component that represents a metadata import page for administrators
*/
export class MetadataImportPageComponent implements OnInit {
public isOverDocumentDropZone: Observable<boolean>;
public uploader: FileUploader;
public uploaderId: string;
/**
* The uploader configuration options
* @type {UploaderOptions}
*/
uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
// URL needs to contain something to not produce any errors. We are using onFileDrop; not the uploader
url: 'placeholder',
});
/**
* The current value of the file
*/
fileObject: File;
/**
* The authenticated user's email
*/
private currentUserEmail$: Observable<string>;
public constructor(protected authService: AuthService,
private location: Location,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
private scriptDataService: ScriptDataService,
private router: Router) {
}
/**
* Method provided by Angular. Invoked after the constructor.
*/
ngOnInit() {
this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
map((user: EPerson) => user.email)
);
this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId();
this.isOverDocumentDropZone = observableOf(false);
window.addEventListener('drop', e => {
e && e.preventDefault();
}, false);
this.uploader = new FileUploader({
// required, but using onFileDrop, not uploader
url: 'placeholder',
});
}
@HostListener('window:dragover', ['$event'])
onDragOver(event: any) {
// Show drop area on the page
event.preventDefault();
if ((event.target as any).tagName !== 'HTML') {
this.isOverDocumentDropZone = observableOf(true);
}
}
/**
* Called when files are dragged on the window document drop area.
*/
public fileOverDocument(isOver: boolean) {
if (!isOver) {
this.isOverDocumentDropZone = observableOf(isOver);
}
}
/**
* Set (CSV) file
* @param files
*/
setFile(files) {
console.log('setfiles', files)
this.fileObject = files.length > 0 ? files[0] : undefined;
}
/**
* When return button is pressed go to previous location
*/
public onReturn() {
this.location.back();
}
/**
* Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
*/
public importMetadata() {
if (this.fileObject == null) {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
} else {
this.currentUserEmail$.pipe(
filter((email: string) => hasValue(email)),
take(1)
).subscribe((email: string) => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-e', value: email }),
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
];
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject])
.pipe(take(1))
.subscribe((requestEntry: RequestEntry) => {
if (requestEntry.response.isSuccessful) {
const title = this.translate.get('process.new.notification.success.title');
const content = this.translate.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.translate.get('process.new.notification.error.title');
const content = this.translate.get('process.new.notification.error.content');
this.notificationsService.error(title, content);
}
});
});
}
}
}

View File

@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { getAdminModulePath } from '../app-routing.module';
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
@@ -48,6 +49,12 @@ export function getAccessControlModulePath() {
component: AdminCurationTasksComponent,
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
},
{
path: 'metadata-import',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: MetadataImportPageComponent,
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
},
])
],
providers: [

View File

@@ -2,13 +2,13 @@ import { Component, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { of } from 'rxjs/internal/observable/of';
import { first, map, take, tap, filter } from 'rxjs/operators';
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 {
METADATA_EXPORT_SCRIPT_NAME,
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService
} from '../../core/data/processes/script-data.service';
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';
@@ -17,8 +17,7 @@ import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal
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
ExportMetadataSelectorComponent
} 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';
@@ -86,6 +85,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
this.createMenu();
this.createSiteAdministratorMenuSections();
this.createExportMenuSections();
this.createImportMenuSections();
super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.authService.isAuthenticated()
@@ -236,39 +236,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
} as OnClickMenuItemModel,
},
/* Import */
/* Curation tasks */
{
id: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.import'
} as TextMenuItemModel,
icon: 'sign-in-alt',
index: 2
},
{
id: 'import_metadata',
parentID: 'import',
id: 'curation_tasks',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_metadata',
link: ''
} as LinkMenuItemModel,
},
{
id: 'import_batch',
parentID: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_batch',
text: 'menu.section.curation_task',
link: ''
} as LinkMenuItemModel,
icon: 'filter',
index: 7
},
/* Statistics */
@@ -401,6 +380,63 @@ 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 import scripts exist and the current user is allowed to execute them
*/
createImportMenuSections() {
const menuList = [
/* Import */
{
id: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.import'
} as TextMenuItemModel,
icon: 'sign-in-alt',
index: 2
},
{
id: 'import_batch',
parentID: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_batch',
link: ''
} as LinkMenuItemModel,
}
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
take(1)
).subscribe(() => {
this.menuService.addSection(this.menuID, {
id: 'import_metadata',
parentID: 'import',
active: true,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_metadata',
link: '/admin/metadata-import'
} as LinkMenuItemModel,
shouldPersistOnRouteChange: true
});
});
}
/**
* Create menu sections dependent on whether or not the current user is a site administrator
*/

View File

@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
@@ -40,7 +41,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
WorkflowItemSearchResultAdminWorkflowListElementComponent,
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
WorkflowItemAdminWorkflowActionsComponent
WorkflowItemAdminWorkflowActionsComponent,
MetadataImportPageComponent
],
entryComponents: [
@@ -54,7 +57,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
WorkflowItemSearchResultAdminWorkflowListElementComponent,
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
WorkflowItemAdminWorkflowActionsComponent
WorkflowItemAdminWorkflowActionsComponent,
MetadataImportPageComponent
]
})
export class AdminModule {

View File

@@ -1,8 +1,4 @@
import { Injectable } from '@angular/core';
import {
getSucceededRemoteData,
getFirstSucceededRemoteDataPayload
} from '../../shared/operators';
import { DataService } from '../data.service';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
@@ -14,7 +10,7 @@ import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
import { Script } from '../../../process-page/scripts/script.model';
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
import { find, map, switchMap, filter } from 'rxjs/operators';
import { find, map, switchMap } from 'rxjs/operators';
import { URLCombiner } from '../../url-combiner/url-combiner';
import { RemoteData } from '../remote-data';
import { MultipartPostRequest, RestRequest } from '../request.models';
@@ -25,6 +21,9 @@ import { dataService } from '../../cache/builders/build-decorators';
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
import { hasValue } from '../../../shared/empty.util';
export const METADATA_IMPORT_SCRIPT_NAME: string = 'metadata-import';
export const METADATA_EXPORT_SCRIPT_NAME: string = 'metadata-export';
@Injectable()
@dataService(SCRIPT)
export class ScriptDataService extends DataService<Script> {

View File

@@ -5,7 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model';
import { Item } from '../../../../core/shared/item.model';
@@ -13,7 +13,7 @@ import { ProcessParameter } from '../../../../process-page/processes/process-par
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { ExportMetadataSelectorComponent, METADATA_EXPORT_SCRIPT_NAME } from './export-metadata-selector.component';
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
describe('ExportMetadataSelectorComponent', () => {
let component: ExportMetadataSelectorComponent;

View File

@@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { take, map } from 'rxjs/operators';
import { of as observableOf } from 'rxjs';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { METADATA_EXPORT_SCRIPT_NAME, 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';
@@ -16,8 +16,6 @@ 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

View File

@@ -442,6 +442,24 @@
"admin.metadata-import.breadcrumbs": "Import Metadata",
"admin.metadata-import.title": "Import Metadata",
"admin.metadata-import.page.header": "Import Metadata",
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to upload",
"admin.metadata-import.page.button.return": "Return",
"admin.metadata-import.page.button.proceed": "Proceed",
"admin.metadata-import.page.error.addFile": "Select file first!",
"auth.errors.invalid-user": "Invalid email address or password.",
@@ -3092,6 +3110,6 @@
"workflow-item.send-back.button.cancel": "Cancel",
"workflow-item.send-back.button.confirm": "Send back",
"workflow-item.send-back.button.confirm": "Send back"
}