-
-
-
+
+
+
+
+
+
+
+
{{ 'collection.edit.tabs.source.head' | translate }}
+
+
+
+
+
{{ 'collection.edit.tabs.source.form.head' | translate }}
- {{ 'collection.edit.tabs.source.head' | translate }}
-
-
-
-
-
-
-
-
-
+
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
index 70b268d9b7..932092a4a4 100644
--- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
+++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
@@ -1,8 +1,13 @@
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
import {
- DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService,
- DynamicInputModel, DynamicOptionControlModel, DynamicRadioGroupModel,
+ DynamicFormControlModel,
+ DynamicFormGroupModel,
+ DynamicFormLayout,
+ DynamicFormService,
+ DynamicInputModel,
+ DynamicOptionControlModel,
+ DynamicRadioGroupModel,
DynamicSelectModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
@@ -12,16 +17,18 @@ import { ObjectUpdatesService } from '../../../core/data/object-updates/object-u
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FormGroup } from '@angular/forms';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
-import { ContentSource } from '../../../core/shared/content-source.model';
+import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model';
-import { first, map } from 'rxjs/operators';
+import { first, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Subscription } from 'rxjs/internal/Subscription';
import { cloneDeep } from 'lodash';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
+import { CollectionDataService } from '../../../core/data/collection-data.service';
+import { getSucceededRemoteData } from '../../../core/shared/operators';
/**
* Component for managing the content source of the collection
@@ -64,9 +71,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
/**
* The Dynamic Input Model for the OAI Provider
*/
- providerModel = new DynamicInputModel({
- id: 'provider',
- name: 'provider',
+ oaiSourceModel = new DynamicInputModel({
+ id: 'oaiSource',
+ name: 'oaiSource',
required: true,
validators: {
required: null
@@ -79,17 +86,17 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
/**
* The Dynamic Input Model for the OAI Set
*/
- setModel = new DynamicInputModel({
- id: 'set',
- name: 'set'
+ oaiSetIdModel = new DynamicInputModel({
+ id: 'oaiSetId',
+ name: 'oaiSetId'
});
/**
* The Dynamic Input Model for the Metadata Format used
*/
- formatModel = new DynamicSelectModel({
- id: 'format',
- name: 'format',
+ metadataConfigIdModel = new DynamicSelectModel({
+ id: 'metadataConfigId',
+ name: 'metadataConfigId',
options: [
{
value: 'dc'
@@ -100,24 +107,25 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
{
value: 'dim'
}
- ]
+ ],
+ value: 'dc'
});
/**
* The Dynamic Input Model for the type of harvesting
*/
- harvestModel = new DynamicRadioGroupModel
({
- id: 'harvest',
- name: 'harvest',
+ harvestTypeModel = new DynamicRadioGroupModel({
+ id: 'harvestType',
+ name: 'harvestType',
options: [
{
- value: 1
+ value: ContentSourceHarvestType.Metadata
},
{
- value: 2
+ value: ContentSourceHarvestType.MetadataAndRef
},
{
- value: 3
+ value: ContentSourceHarvestType.MetadataAndBitstreams
}
]
});
@@ -125,7 +133,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
/**
* All input models in a simple array for easier iterations
*/
- inputModels = [this.providerModel, this.setModel, this.formatModel, this.harvestModel];
+ inputModels = [this.oaiSourceModel, this.oaiSetIdModel, this.metadataConfigIdModel, this.harvestTypeModel];
/**
* The dynamic form fields used for editing the content source of a collection
@@ -133,22 +141,22 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
*/
formModel: DynamicFormControlModel[] = [
new DynamicFormGroupModel({
- id: 'providerContainer',
+ id: 'oaiSourceContainer',
group: [
- this.providerModel
+ this.oaiSourceModel
]
}),
new DynamicFormGroupModel({
- id: 'setContainer',
+ id: 'oaiSetContainer',
group: [
- this.setModel,
- this.formatModel
+ this.oaiSetIdModel,
+ this.metadataConfigIdModel
]
}),
new DynamicFormGroupModel({
- id: 'harvestContainer',
+ id: 'harvestTypeContainer',
group: [
- this.harvestModel
+ this.harvestTypeModel
]
})
];
@@ -157,38 +165,38 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
- provider: {
+ oaiSource: {
grid: {
host: 'col-12 d-inline-block'
}
},
- set: {
+ oaiSetId: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
- format: {
+ metadataConfigId: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
- harvest: {
+ harvestType: {
grid: {
host: 'col-12',
option: 'btn-outline-secondary'
}
},
- setContainer: {
+ oaiSetContainer: {
grid: {
host: 'row'
}
},
- providerContainer: {
+ oaiSourceContainer: {
grid: {
host: 'row'
}
},
- harvestContainer: {
+ harvestTypeContainer: {
grid: {
host: 'row'
}
@@ -205,6 +213,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
*/
updateSub: Subscription;
+ harvestTypeNone = ContentSourceHarvestType.None;
+
public constructor(public objectUpdatesService: ObjectUpdatesService,
public notificationsService: NotificationsService,
protected location: Location,
@@ -212,7 +222,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
protected translate: TranslateService,
protected route: ActivatedRoute,
protected router: Router,
- @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,) {
+ @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
+ protected collectionService: CollectionDataService) {
super(objectUpdatesService, notificationsService, translate);
}
@@ -229,16 +240,21 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
this.formGroup = this.formService.createFormGroup(this.formModel);
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
- /* Create a new ContentSource object - TODO: Update to be fetched from the collection */
- this.contentSource = new ContentSource();
+ this.collectionRD$.pipe(
+ getSucceededRemoteData(),
+ map((col) => col.payload.uuid),
+ switchMap((uuid) => this.collectionService.getContentSource(uuid)),
+ take(1)
+ ).subscribe((contentSource: ContentSource) => {
+ this.contentSource = contentSource;
+ this.initializeOriginalContentSource();
+ });
this.updateFieldTranslations();
this.translate.onLangChange
.subscribe(() => {
this.updateFieldTranslations();
});
-
- this.initializeOriginalContentSource();
}
/**
@@ -254,15 +270,15 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
if (update) {
const field = update.field as ContentSource;
this.formGroup.patchValue({
- providerContainer: {
- provider: field.provider
+ oaiSourceContainer: {
+ oaiSource: field.oaiSource
},
- setContainer: {
- set: field.set,
- format: field.format
+ oaiSetContainer: {
+ oaiSetId: field.oaiSetId,
+ metadataConfigId: field.metadataConfigId
},
- harvestContainer: {
- harvest: field.harvest
+ harvestTypeContainer: {
+ harvestType: field.harvestType
}
});
this.contentSource = cloneDeep(field);
@@ -307,7 +323,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
* @param event
*/
onChange(event) {
- this.updateContentSourceField(event.model);
+ this.updateContentSourceField(event.model, true);
this.saveFieldUpdate();
}
@@ -331,24 +347,28 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
* Is the current form valid to be submitted ?
*/
isValid(): boolean {
- return !this.contentSource.enabled || this.formGroup.valid;
+ return (this.contentSource.harvestType === ContentSourceHarvestType.None) || this.formGroup.valid;
}
/**
* Switch the external source on or off and fire a field update
*/
changeExternalSource() {
- this.contentSource.enabled = !this.contentSource.enabled;
- this.updateContentSource();
+ if (this.contentSource.harvestType === ContentSourceHarvestType.None) {
+ this.contentSource.harvestType = ContentSourceHarvestType.Metadata;
+ } else {
+ this.contentSource.harvestType = ContentSourceHarvestType.None;
+ }
+ this.updateContentSource(false);
}
/**
* Loop over all inputs and update the Content Source with their value
*/
- updateContentSource() {
+ updateContentSource(updateHarvestType: boolean) {
this.inputModels.forEach(
(fieldModel: DynamicInputModel) => {
- this.updateContentSourceField(fieldModel)
+ this.updateContentSourceField(fieldModel, updateHarvestType)
}
);
this.saveFieldUpdate();
@@ -358,8 +378,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
* Update the Content Source with the value from a DynamicInputModel
* @param fieldModel
*/
- updateContentSourceField(fieldModel: DynamicInputModel) {
- if (hasValue(fieldModel.value)) {
+ updateContentSourceField(fieldModel: DynamicInputModel, updateHarvestType: boolean) {
+ if (hasValue(fieldModel.value) && !(fieldModel.id === this.harvestTypeModel.id && !updateHarvestType)) {
this.contentSource[fieldModel.id] = fieldModel.value;
}
}
diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts
index 03233e616b..c9934d6abe 100644
--- a/src/app/core/cache/response.models.ts
+++ b/src/app/core/cache/response.models.ts
@@ -14,6 +14,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { MetadataSchema } from '../metadata/metadata-schema.model';
import { MetadataField } from '../metadata/metadata-field.model';
+import { ContentSource } from '../shared/content-source.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
@@ -288,4 +289,17 @@ export class FilteredDiscoveryQueryResponse extends RestResponse {
super(true, statusCode, statusText);
}
}
+
+/**
+ * A successful response containing exactly one MetadataSchema
+ */
+export class ContentSourceSuccessResponse extends RestResponse {
+ constructor(
+ public contentsource: ContentSource,
+ public statusCode: number,
+ public statusText: string,
+ ) {
+ super(true, statusCode, statusText);
+ }
+}
/* tslint:enable:max-classes-per-file */
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 1d22b5fefe..fe46bf88aa 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -117,6 +117,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
import { BrowseDefinition } from './shared/browse-definition.model';
+import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service';
const IMPORTS = [
CommonModule,
@@ -203,6 +204,7 @@ const PROVIDERS = [
TaskResponseParsingService,
ClaimedTaskDataService,
PoolTaskDataService,
+ ContentSourceResponseParsingService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,
diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts
index 993954a360..1a59498a72 100644
--- a/src/app/core/data/collection-data.service.ts
+++ b/src/app/core/data/collection-data.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
-import { filter, map, take } from 'rxjs/operators';
+import { filter, map, take, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -16,9 +16,12 @@ import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { Observable } from 'rxjs/internal/Observable';
-import { FindAllOptions } from './request.models';
+import { ContentSourceRequest, FindAllOptions, RestRequest } from './request.models';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list';
+import { ContentSource } from '../shared/content-source.model';
+import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../shared/operators';
+import { ContentSourceSuccessResponse } from '../cache/response.models';
@Injectable()
export class CollectionDataService extends ComColDataService {
@@ -57,4 +60,21 @@ export class CollectionDataService extends ComColDataService {
map((collections: RemoteData>) => collections.payload.totalElements > 0)
);
}
+
+ getHarvesterEndpoint(collectionId: string): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ map((href: string) => `${href}/${collectionId}/harvester`)
+ );
+ }
+
+ getContentSource(collectionId: string): Observable {
+ return this.getHarvesterEndpoint(collectionId).pipe(
+ map((href: string) => new ContentSourceRequest(this.requestService.generateRequestId(), href)),
+ configureRequest(this.requestService),
+ map((request: RestRequest) => request.href),
+ getRequestFromRequestHref(this.requestService),
+ filterSuccessfulResponses(),
+ map((response: ContentSourceSuccessResponse) => response.contentsource)
+ );
+ }
}
diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts
new file mode 100644
index 0000000000..34e9360dd7
--- /dev/null
+++ b/src/app/core/data/content-source-response-parsing.service.ts
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+import { ResponseParsingService } from './parsing.service';
+import { RestRequest } from './request.models';
+import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
+import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
+import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
+import { ContentSource } from '../shared/content-source.model';
+
+@Injectable()
+export class ContentSourceResponseParsingService implements ResponseParsingService {
+
+ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
+ const payload = data.payload;
+
+ const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload);
+ return new ContentSourceSuccessResponse(deserialized, data.statusCode, data.statusText);
+ }
+
+}
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 22d5fd3e77..2b96cb378a 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -93,14 +93,16 @@ export class ObjectUpdatesService {
const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(map((objectEntry) => {
const fieldUpdates: FieldUpdates = {};
- Object.keys(objectEntry.fieldStates).forEach((uuid) => {
- let fieldUpdate = objectEntry.fieldUpdates[uuid];
- if (isEmpty(fieldUpdate)) {
- const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
- fieldUpdate = { field: identifiable, changeType: undefined };
- }
- fieldUpdates[uuid] = fieldUpdate;
- });
+ if (hasValue(objectEntry)) {
+ Object.keys(objectEntry.fieldStates).forEach((uuid) => {
+ let fieldUpdate = objectEntry.fieldUpdates[uuid];
+ if (isEmpty(fieldUpdate)) {
+ const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
+ fieldUpdate = {field: identifiable, changeType: undefined};
+ }
+ fieldUpdates[uuid] = fieldUpdate;
+ });
+ }
return fieldUpdates;
}))
}
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
index a2b3423960..eab7af5751 100644
--- a/src/app/core/data/request.models.ts
+++ b/src/app/core/data/request.models.ts
@@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service';
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
import { URLCombiner } from '../url-combiner/url-combiner';
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
+import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
/* tslint:disable:max-classes-per-file */
@@ -358,6 +359,16 @@ export class CreateRequest extends PostRequest {
}
}
+export class ContentSourceRequest extends GetRequest {
+ constructor(uuid: string, href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return ContentSourceResponseParsingService;
+ }
+}
+
/**
* Request to delete an object based on its identifier
*/
diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts
index c6d6c03fab..68db549adb 100644
--- a/src/app/core/shared/content-source.model.ts
+++ b/src/app/core/shared/content-source.model.ts
@@ -1,41 +1,51 @@
-import { v4 as uuid } from 'uuid';
+import { autoserialize, autoserializeAs } from 'cerialize';
+
+export enum ContentSourceHarvestType {
+ None = 'NONE',
+ Metadata = 'METADATA_ONLY',
+ MetadataAndRef = 'METADATA_AND_REF',
+ MetadataAndBitstreams = 'METADATA_AND_BITSTREAMS'
+}
/**
* A model class that holds information about the Content Source of a Collection
*/
export class ContentSource {
/**
- * Unique identifier
+ * Unique identifier, this is necessary to store the ContentSource in FieldUpdates
+ * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink
*/
+ @autoserializeAs('self')
uuid: string;
/**
- * Does this collection harvest its content from an external source ?
+ * OAI Provider / Source
*/
- enabled = false;
-
- /**
- * OAI Provider
- */
- provider: string;
+ @autoserializeAs('oai_source')
+ oaiSource: string;
/**
* OAI Specific set ID
*/
- set: string;
+ @autoserializeAs('oai_set_id')
+ oaiSetId: string;
/**
- * Metadata Format
+ * The ID of the metadata format used
*/
- format = 'dc';
+ @autoserializeAs('metadata_config_id')
+ metadataConfigId = 'dc';
/**
* Type of content being harvested
+ * Defaults to 'NONE', meaning the collection doesn't harvest its content from an external source
*/
- harvest = 3;
+ @autoserializeAs('harvest_type')
+ harvestType = ContentSourceHarvestType.None;
- constructor() {
- // TODO: Remove this once the Content Source is fetched from the REST API and a custom generated UUID is not necessary anymore
- this.uuid = uuid();
- }
+ /**
+ * The REST link to itself
+ */
+ @autoserialize
+ self: string;
}