From e47b42bc89e0c34271e1a9fac5f9bbf08f77720f Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 7 Nov 2022 20:56:05 +0530 Subject: [PATCH 01/41] [CST-7216] Angular: Import saf via URL --- .../batch-import-page.component.html | 12 +++ .../batch-import-page.component.spec.ts | 87 ++++++++++++++++++- .../batch-import-page.component.ts | 29 ++++++- src/app/admin/admin.module.ts | 4 +- src/assets/i18n/en.json5 | 6 ++ 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index dbc8c74437..190eb0d409 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -20,12 +20,24 @@ + + +
+ +
+
diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index 36ba1137c9..2c465d3f3d 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -86,10 +86,18 @@ describe('BatchImportPageComponent', () => { let fileMock: File; beforeEach(() => { + component.isUpload = true; fileMock = new File([''], 'filename.zip', { type: 'application/zip' }); component.setFile(fileMock); }); + it('should show the file dropzone', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeTruthy(); + expect(fileUrlInput).toBeFalsy(); + }); + describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { component.validateOnly = false; @@ -99,9 +107,9 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }) ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); }); it('success notification is shown', () => { @@ -121,8 +129,8 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); @@ -148,4 +156,77 @@ describe('BatchImportPageComponent', () => { }); }); }); + + describe('if url is set', () => { + beforeEach(fakeAsync(() => { + component.isUpload = false; + component.fileURL = 'example.fileURL.com'; + fixture.detectChanges(); + })); + + it('should show the file url input', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeFalsy(); + expect(fileUrlInput).toBeTruthy(); + }); + + describe('if proceed button is pressed without validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = false; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --u and the file url', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }) + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed button is pressed with validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = true; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --u and the file url and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }), + Object.assign(new ProcessParameter(), { name: '-v', value: true }), + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed is pressed; but script invoke fails', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500)); + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('error notification is shown', () => { + expect(notificationService.error).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 7171c67585..79da641cc6 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -8,7 +8,7 @@ import { ProcessParameter } from '../../process-page/processes/process-parameter import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Process } from '../../process-page/processes/process.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; import { ImportBatchSelectorComponent @@ -32,11 +32,22 @@ export class BatchImportPageComponent { * The validate only flag */ validateOnly = true; + /** * dso object for community or collection */ dso: DSpaceObject = null; + /** + * The flag between upload and url + */ + isUpload = true; + + /** + * File URL when flag is for url + */ + fileURL: string; + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -72,13 +83,18 @@ export class BatchImportPageComponent { * Starts import-metadata script with --zip fileName (and the selected file) */ public importMetadata() { - if (this.fileObject == null) { + if (this.fileObject == null && isEmpty(this.fileURL)) { this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); } else { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), Object.assign(new ProcessParameter(), { name: '--add' }) ]; + if (this.isUpload) { + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name })); + } else { + this.fileObject = null; + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--u', value: this.fileURL })); + } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } @@ -121,4 +137,11 @@ export class BatchImportPageComponent { removeDspaceObject(): void { this.dso = null; } + + /** + * toggle the flag between upload and url + */ + toggleUpload() { + this.isUpload = !this.isUpload; + } } diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index dff2e506c3..768e3120df 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -10,7 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module'; import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; -import { UploadModule } from '../shared/upload/upload.module'; +import { UiSwitchModule } from 'ngx-ui-switch'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -27,7 +27,7 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, - UploadModule, + UiSwitchModule ], declarations: [ AdminCurationTasksComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fc4c6aa74d..5eb069c804 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -586,6 +586,12 @@ "admin.batch-import.page.error.addFile": "Select Zip file first!", + "admin.metadata-import.page.toggle.upload": "Upload", + + "admin.metadata-import.page.toggle.url": "URL", + + "admin.metadata-import.page.urlMsg": "Insert the batch ZIP url to import", + "admin.metadata-import.page.validateOnly": "Validate Only", "admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.", From 98da08ead0c0b8c197f0aed998c9a2368915f6a9 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Nov 2022 19:31:32 +0530 Subject: [PATCH 02/41] [CST-7216] Design fixes and parameter changed --- .../batch-import-page.component.html | 7 ++++++- .../batch-import-page.component.spec.ts | 8 ++++---- .../batch-import-page.component.ts | 8 ++++++-- src/assets/i18n/en.json5 | 4 ++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index 190eb0d409..1092443436 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -20,10 +20,15 @@
- + + {{'admin.batch-import.page.toggle.help' | translate}} + + { proceed.click(); fixture.detectChanges(); })); - it('metadata-import script is invoked with --u and the file url', () => { + it('metadata-import script is invoked with --url and the file url', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }), - Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }) + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }) ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); }); @@ -200,10 +200,10 @@ describe('BatchImportPageComponent', () => { proceed.click(); fixture.detectChanges(); })); - it('metadata-import script is invoked with --u and the file url and -v validate-only', () => { + it('metadata-import script is invoked with --url and the file url and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }), - Object.assign(new ProcessParameter(), { name: '--u', value: 'example.fileURL.com' }), + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 79da641cc6..744b3aecce 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -84,7 +84,11 @@ export class BatchImportPageComponent { */ public importMetadata() { if (this.fileObject == null && isEmpty(this.fileURL)) { - this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + if (this.isUpload) { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + } else { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFileUrl')); + } } else { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--add' }) @@ -93,7 +97,7 @@ export class BatchImportPageComponent { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name })); } else { this.fileObject = null; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--u', value: this.fileURL })); + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--url', value: this.fileURL })); } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5eb069c804..6d7ce0b884 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -568,6 +568,8 @@ "admin.batch-import.page.help": "Select the Collection to import into. Then, drop or browse to a Simple Archive Format (SAF) zip file that includes the Items to import", + "admin.batch-import.page.toggle.help": "It is possible to perform import either with file upload or via URL, use above toggle to set the input source", + "admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import", "admin.batch-import.page.dropMsg": "Drop a batch ZIP to import", @@ -584,6 +586,8 @@ "admin.metadata-import.page.error.addFile": "Select file first!", + "admin.metadata-import.page.error.addFileUrl": "Insert file url first!", + "admin.batch-import.page.error.addFile": "Select Zip file first!", "admin.metadata-import.page.toggle.upload": "Upload", From b31fdf0be6c138f4d114eaa66abfb095cd8863e0 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 23 Mar 2023 11:59:50 +0100 Subject: [PATCH 03/41] [CST-7216] Imported UploadModule in AdminModule to fix build error --- src/app/admin/admin.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 768e3120df..3dc0036854 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -11,6 +11,7 @@ import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-sect import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { UiSwitchModule } from 'ngx-ui-switch'; +import { UploadModule } from '../shared/upload/upload.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -27,7 +28,8 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, - UiSwitchModule + UiSwitchModule, + UploadModule, ], declarations: [ AdminCurationTasksComponent, From 7008afd05f19703e91c92f9f194feb54139c3d92 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 25 Apr 2023 16:12:00 +0200 Subject: [PATCH 04/41] 1578: primaryBitstream implementation --- .../edit-bitstream-page.component.spec.ts | 152 ++++++++++++++++-- .../edit-bitstream-page.component.ts | 136 +++++++++++----- src/app/core/shared/bundle.model.ts | 8 +- 3 files changed, 243 insertions(+), 53 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 44e48182fd..13f3ec7f27 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -24,6 +24,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; import { MetadataValueFilter } from '../../core/shared/metadata.models'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { Bundle } from '../../core/shared/bundle.model'; +import { BundleDataService } from '../../core/data/bundle-data.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -35,9 +37,14 @@ let bitstreamService: BitstreamDataService; let bitstreamFormatService: BitstreamFormatDataService; let dsoNameService: DSONameService; let bitstream: Bitstream; +let bitstreamID: string; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; +let bundleDataService; +let bundleWithCurrentPrimary: Bundle; +let bundleWithDifferentPrimary: Bundle; +let bundleWithNoPrimary: Bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; @@ -45,6 +52,12 @@ let fixture: ComponentFixture; describe('EditBitstreamPageComponent', () => { beforeEach(() => { + bitstreamID = 'current-bitstream-id'; + bundleWithCurrentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': bitstreamID }); + bundleWithDifferentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': '12345-abcde-54321-edcba' }); + bundleWithNoPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': null }); + bundleDataService = jasmine.createSpyObj('BundleDataService', ['patch']); + bundleDataService.patch.and.callFake((a, b) => createSuccessfulRemoteDataObject$(a)); allFormats = [ Object.assign({ id: '1', @@ -53,7 +66,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Unknown, mimetype: 'application/octet-stream', _links: { - self: {href: 'format-selflink-1'} + self: { href: 'format-selflink-1' } } }), Object.assign({ @@ -63,7 +76,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/png', _links: { - self: {href: 'format-selflink-2'} + self: { href: 'format-selflink-2' } } }), Object.assign({ @@ -73,7 +86,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/gif', _links: { - self: {href: 'format-selflink-3'} + self: { href: 'format-selflink-3' } } }) ] as BitstreamFormat[]; @@ -112,6 +125,8 @@ describe('EditBitstreamPageComponent', () => { const bundleName = 'ORIGINAL'; bitstream = Object.assign(new Bitstream(), { + uuid: bitstreamID, + id: bitstreamID, metadata: { 'dc.description': [ { @@ -155,17 +170,19 @@ describe('EditBitstreamPageComponent', () => { imports: [TranslateModule.forRoot(), RouterTestingModule], declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], providers: [ - {provide: NotificationsService, useValue: notificationsService}, - {provide: DynamicFormService, useValue: formService}, - {provide: ActivatedRoute, + { provide: NotificationsService, useValue: notificationsService }, + { provide: DynamicFormService, useValue: formService }, + { + provide: ActivatedRoute, useValue: { - data: observableOf({bitstream: createSuccessfulRemoteDataObject(bitstream)}), - snapshot: {queryParams: {}} + data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), + snapshot: { queryParams: {} } } }, - {provide: BitstreamDataService, useValue: bitstreamService}, - {provide: DSONameService, useValue: dsoNameService}, - {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BitstreamDataService, useValue: bitstreamService }, + { provide: DSONameService, useValue: dsoNameService }, + { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -203,6 +220,27 @@ describe('EditBitstreamPageComponent', () => { it('should put the \"New Format\" input on invisible', () => { expect(comp.formLayout.newFormat.grid.host).toContain('invisible'); }); + describe('when the bitstream is the primary bitstream on the bundle', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithCurrentPrimary; + comp.setForm(); + rawForm = comp.formGroup.getRawValue(); + + }); + it('should enable the primary bitstream toggle', () => { + expect(rawForm.fileNamePrimaryContainer.primaryBitstream).toEqual(true); + }); + }); + describe('when the bitstream is not the primary bitstream on the bundle', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithDifferentPrimary; + comp.setForm(); + rawForm = comp.formGroup.getRawValue(); + }); + it('should disable the primary bitstream toggle', () => { + expect(rawForm.fileNamePrimaryContainer.primaryBitstream).toEqual(false); + }); + }); }); describe('when an unknown format is selected', () => { @@ -216,6 +254,95 @@ describe('EditBitstreamPageComponent', () => { }); describe('onSubmit', () => { + describe('when the primaryBitstream changed', () => { + describe('to the current bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + }); + + describe('from a different primary bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithDifferentPrimary; + comp.onSubmit(); + }); + + it('should call patch with a replace operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ + op: 'replace' + })]); + }); + + it('should call patch with the correct bitstream uuid', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ + value: bitstreamID + })]); + }); + }); + describe('from no primary bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithNoPrimary; + comp.onSubmit(); + }); + + it('should call patch with an add operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ + op: 'add' + })]); + }); + + it('should call patch with the correct bitstream uuid', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ + value: bitstreamID + })]); + }); + }); + }); + describe('to no primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + }); + + describe('from the current bitstream', () => { + beforeEach(() => { + (comp as any).bundle = bundleWithCurrentPrimary; + comp.onSubmit(); + }); + + it('should call patch with a remove operation', () => { + expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithCurrentPrimary, [jasmine.objectContaining({ + op: 'remove' + })]); + }); + }); + }); + }); + describe('when the primaryBitstream did not changed', () => { + describe('the current bitstream stayed the primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + (comp as any).bundle = bundleWithCurrentPrimary; + comp.onSubmit(); + }); + it('should not call patch on the bundle data service', () => { + expect(bundleDataService.patch).not.toHaveBeenCalled(); + }); + }); + describe('the bitstream was not and did not become the primary bitstream', () => { + beforeEach(() => { + const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); + spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); + (comp as any).bundle = bundleWithDifferentPrimary; + comp.onSubmit(); + }); + it('should not call patch on the bundle data service', () => { + expect(bundleDataService.patch).not.toHaveBeenCalled(); + }); + }); + }); + describe('when selected format hasn\'t changed', () => { beforeEach(() => { comp.onSubmit(); @@ -357,6 +484,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -371,7 +499,6 @@ describe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - describe('on startup', () => { let rawForm; @@ -475,6 +602,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: BundleDataService, useValue: bundleDataService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 8e63ec939f..3c3965a70a 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -52,6 +52,7 @@ import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; +import { BundleDataService } from '../../core/data/bundle-data.service'; @Component({ selector: 'ds-edit-bitstream-page', @@ -191,19 +192,19 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * The Dynamic Input Model for the iiif label */ iiifLabelModel = new DsDynamicInputModel({ - hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', - id: 'iiifLabel', - name: 'iiifLabel' - }, + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifLabel', + name: 'iiifLabel' + }, { - grid: { - host: 'col col-lg-6 d-inline-block' - } + grid: { + host: 'col col-lg-6 d-inline-block' + } }); iiifLabelContainer = new DynamicFormGroupModel({ id: 'iiifLabelContainer', group: [this.iiifLabelModel] - },{ + }, { grid: { host: 'form-row' } @@ -213,7 +214,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifToc', name: 'iiifToc', - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -221,7 +222,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifTocContainer = new DynamicFormGroupModel({ id: 'iiifTocContainer', group: [this.iiifTocModel] - },{ + }, { grid: { host: 'form-row' } @@ -231,7 +232,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifWidth', name: 'iiifWidth', - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -239,7 +240,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifWidthContainer = new DynamicFormGroupModel({ id: 'iiifWidthContainer', group: [this.iiifWidthModel] - },{ + }, { grid: { host: 'form-row' } @@ -249,7 +250,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'iiifHeight', name: 'iiifHeight' - },{ + }, { grid: { host: 'col col-lg-6 d-inline-block' } @@ -257,7 +258,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifHeightContainer = new DynamicFormGroupModel({ id: 'iiifHeightContainer', group: [this.iiifHeightModel] - },{ + }, { grid: { host: 'form-row' } @@ -280,11 +281,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.fileNameModel, this.primaryBitstreamModel ] - },{ - grid: { - host: 'form-row' - } - }), + }, { + grid: { + host: 'form-row' + } + }), new DynamicFormGroupModel({ id: 'descriptionContainer', group: [ @@ -380,13 +381,23 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ isIIIF = false; - /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ protected subs: Subscription[] = []; + /** + * The parent bundle containing the Bitstream + * @private + */ + private bundle: Bundle; + + /** + * Path to patch primary bitstream on the bundle + * @private + */ + private readonly primaryBitstreamPath = '/primarybitstream'; constructor(private route: ActivatedRoute, private router: Router, @@ -397,7 +408,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { private bitstreamService: BitstreamDataService, private dsoNameService: DSONameService, private notificationsService: NotificationsService, - private bitstreamFormatService: BitstreamFormatDataService) { + private bitstreamFormatService: BitstreamFormatDataService, + private bundleService: BundleDataService) { } /** @@ -423,13 +435,20 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getRemoteDataPayload() ); + const bundle$ = bitstream$.pipe( + switchMap((bitstream: Bitstream) => bitstream.bundle), + getFirstSucceededRemoteDataPayload(), + ); + this.subs.push( observableCombineLatest( bitstream$, - allFormats$ - ).subscribe(([bitstream, allFormats]) => { + allFormats$, + bundle$ + ).subscribe(([bitstream, allFormats, bundle]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; + this.bundle = bundle; this.setIiifStatus(this.bitstream); }) ); @@ -437,8 +456,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.subs.push( this.translate.onLangChange .subscribe(() => { - this.updateFieldTranslations(); - }) + this.updateFieldTranslations(); + }) ); } @@ -460,7 +479,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ fileNamePrimaryContainer: { fileName: bitstream.name, - primaryBitstream: false + primaryBitstream: this.bundle.primaryBitstreamUUID === bitstream.uuid }, descriptionContainer: { description: bitstream.firstMetadataValue('dc.description') @@ -563,6 +582,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } + /** * Check for changes against the bitstream and send update requests to the REST API */ @@ -571,9 +591,45 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const updatedBitstream = this.formToBitstream(updatedValues); const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); const isNewFormat = selectedFormat.id !== this.originalFormat.id; + const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; + const wasPrimary = this.bundle.primaryBitstreamUUID === this.bitstream.uuid; let bitstream$; + let bundle$: Observable; + if (wasPrimary !== isPrimary) { + let patchOperation; + // No longer primary bitstream: remove + if (wasPrimary) { + patchOperation = { + path: this.primaryBitstreamPath, + op: 'remove' + }; + } else { + // Has become primary bitstream + // If it already had a value: replace, otherwise: add + patchOperation = { + path: this.primaryBitstreamPath, + op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', + value: this.bitstream.uuid + }; + } + bundle$ = this.bundleService.patch(this.bundle, [patchOperation]).pipe( + getFirstCompletedRemoteData(), + map((bundleResponse: RemoteData) => { + if (hasValue(bundleResponse) && bundleResponse.hasFailed) { + this.notificationsService.error( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), + bundleResponse.errorMessage + ); + } else { + return bundleResponse.payload; + } + }) + ); + } else { + bundle$ = observableOf(this.bundle); + } if (isNewFormat) { bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( getFirstCompletedRemoteData(), @@ -592,7 +648,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bitstream$ = observableOf(this.bitstream); } - bitstream$.pipe( + combineLatest([bundle$, bitstream$]).pipe( switchMap(() => { return this.bitstreamService.update(updatedBitstream).pipe( getFirstSucceededRemoteDataPayload() @@ -633,11 +689,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } else { Metadata.setFirstValue(newMetadata, this.IIIF_LABEL_METADATA, rawForm.iiifLabelContainer.iiifLabel); } - if (isEmpty(rawForm.iiifTocContainer.iiifToc)) { - delete newMetadata[this.IIIF_TOC_METADATA]; - } else { + if (isEmpty(rawForm.iiifTocContainer.iiifToc)) { + delete newMetadata[this.IIIF_TOC_METADATA]; + } else { Metadata.setFirstValue(newMetadata, this.IIIF_TOC_METADATA, rawForm.iiifTocContainer.iiifToc); - } + } if (isEmpty(rawForm.iiifWidthContainer.iiifWidth)) { delete newMetadata[this.IMAGE_WIDTH_METADATA]; } else { @@ -672,10 +728,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } else { this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), - mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) - .subscribe((item) => { - this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); - }); + mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) + .subscribe((item) => { + this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); + }); } } @@ -701,11 +757,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const isEnabled$ = this.bitstream.bundle.pipe( getFirstSucceededRemoteData(), map((bundle: RemoteData) => bundle.payload.item.pipe( - getFirstSucceededRemoteData(), - map((item: RemoteData) => - (item.payload.firstMetadataValue('dspace.iiif.enabled') && - item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) - )))); + getFirstSucceededRemoteData(), + map((item: RemoteData) => + (item.payload.firstMetadataValue('dspace.iiif.enabled') && + item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) + )))); const iiifSub = combineLatest( isImage$, diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 36b7012e47..72c466d2d6 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { deserialize, inheritSerialization } from 'cerialize'; +import { autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; @@ -29,6 +29,12 @@ export class Bundle extends DSpaceObject { item: HALLink; }; + /** + * The ID of the primaryBitstream of this Bundle + */ + @autoserializeAs('primarybitstream') + primaryBitstreamUUID: string; + /** * The primary Bitstream of this Bundle * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. From 222c220c5667fb76c183ca9eb25273aaf68aa1f8 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 10:54:36 +0200 Subject: [PATCH 05/41] 101289: #1578 fixed redirection bug --- .../edit-bitstream-page.component.spec.ts | 9 +- .../edit-bitstream-page.component.ts | 116 +++++++----------- src/assets/i18n/en.json5 | 2 + 3 files changed, 48 insertions(+), 79 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 13f3ec7f27..258ff2432f 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -388,20 +388,13 @@ describe('EditBitstreamPageComponent', () => { expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); }); }); - describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { + describe('when navigateToItemEditBitstreams is called', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { comp.itemId = 'some-uuid1'; comp.navigateToItemEditBitstreams(); expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); }); }); - describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { - it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { - comp.itemId = undefined; - comp.navigateToItemEditBitstreams(); - expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); - }); - }); }); describe('EditBitstreamPageComponent with IIIF fields', () => { diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 3c3965a70a..5110a5e936 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,58 +1,32 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - OnDestroy, - OnInit -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, mergeMap, switchMap } from 'rxjs/operators'; -import { - combineLatest, - combineLatest as observableCombineLatest, - Observable, - of as observableOf, - Subscription -} from 'rxjs'; -import { - DynamicFormControlModel, - DynamicFormGroupModel, - DynamicFormLayout, - DynamicFormService, - DynamicInputModel, - DynamicSelectModel -} from '@ng-dynamic-forms/core'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { FormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import cloneDeep from 'lodash/cloneDeep'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; -import { - getAllSucceededRemoteDataPayload, - getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload, - getRemoteDataPayload -} from '../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; -import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; -import { getEntityEditRoute, getItemEditRoute } from '../../item-page/item-page-routing-paths'; +import { getEntityEditRoute } from '../../item-page/item-page-routing-paths'; import { Bundle } from '../../core/shared/bundle.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Item } from '../../core/shared/item.model'; -import { - DsDynamicInputModel -} from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { BundleDataService } from '../../core/data/bundle-data.service'; +import { Operation } from 'fast-json-patch'; @Component({ selector: 'ds-edit-bitstream-page', @@ -440,17 +414,24 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataPayload(), ); + const item$ = bundle$.pipe( + switchMap((bundle: Bundle) => bundle.item), + getFirstSucceededRemoteDataPayload() + ); this.subs.push( observableCombineLatest( bitstream$, allFormats$, - bundle$ - ).subscribe(([bitstream, allFormats, bundle]) => { - this.bitstream = bitstream as Bitstream; - this.formats = allFormats.page; - this.bundle = bundle; - this.setIiifStatus(this.bitstream); - }) + bundle$, + item$, + ).pipe() + .subscribe(([bitstream, allFormats, bundle, item]) => { + this.bitstream = bitstream as Bitstream; + this.formats = allFormats.page; + this.bundle = bundle; + this.itemId = item.uuid; + this.setIiifStatus(this.bitstream); + }) ); this.subs.push( @@ -582,7 +563,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } - /** * Check for changes against the bitstream and send update requests to the REST API */ @@ -598,23 +578,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { let bundle$: Observable; if (wasPrimary !== isPrimary) { - let patchOperation; - // No longer primary bitstream: remove - if (wasPrimary) { - patchOperation = { - path: this.primaryBitstreamPath, - op: 'remove' - }; - } else { - // Has become primary bitstream - // If it already had a value: replace, otherwise: add - patchOperation = { - path: this.primaryBitstreamPath, - op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', - value: this.bitstream.uuid - }; - } - bundle$ = this.bundleService.patch(this.bundle, [patchOperation]).pipe( + const patchOperations: Operation[] = this.retrieveBundlePatch(wasPrimary); + bundle$ = this.bundleService.patch(this.bundle, patchOperations).pipe( getFirstCompletedRemoteData(), map((bundleResponse: RemoteData) => { if (hasValue(bundleResponse) && bundleResponse.hasFailed) { @@ -649,6 +614,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } combineLatest([bundle$, bitstream$]).pipe( + tap(([bundle]) => this.bundle = bundle), switchMap(() => { return this.bitstreamService.update(updatedBitstream).pipe( getFirstSucceededRemoteDataPayload() @@ -664,6 +630,24 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { }); } + private retrieveBundlePatch(wasPrimary: boolean): Operation[] { + // No longer primary bitstream: remove + if (wasPrimary) { + return [{ + path: this.primaryBitstreamPath, + op: 'remove' + }]; + } else { + // Has become primary bitstream + // If it already had a value: replace, otherwise: add + return [{ + path: this.primaryBitstreamPath, + op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', + value: this.bitstream.uuid + }]; + } + } + /** * Parse form data to an updated bitstream object * @param rawForm Raw form data @@ -671,8 +655,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { formToBitstream(rawForm): Bitstream { const updatedBitstream = cloneDeep(this.bitstream); const newMetadata = updatedBitstream.metadata; - // TODO: Set bitstream to primary when supported - const primary = rawForm.fileNamePrimaryContainer.primaryBitstream; Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName); if (isEmpty(rawForm.descriptionContainer.description)) { delete newMetadata['dc.description']; @@ -724,15 +706,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * otherwise retrieve the item ID based on the owning bundle's link */ navigateToItemEditBitstreams() { - if (hasValue(this.itemId)) { - this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); - } else { - this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), - mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload()))) - .subscribe((item) => { - this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); - }); - } + this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 66824e56b3..5f09574ab0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -705,6 +705,8 @@ "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", + "bitstream.edit.notifications.error.primaryBitstream.title": "An error occurred saving the primary bitstream", + "bitstream.edit.form.iiifLabel.label": "IIIF Label", "bitstream.edit.form.iiifLabel.hint": "Canvas label for this image. If not provided default label will be used.", From 94f52721d45159fa9037111ad8063815dc77a0d5 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 11:29:17 +0200 Subject: [PATCH 06/41] 101289: #1578 Removed border from primaryBitstream switch --- .../edit-bitstream-page/edit-bitstream-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 5110a5e936..df8f285a6a 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -291,7 +291,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { }, primaryBitstream: { grid: { - host: 'col col-sm-4 d-inline-block switch' + host: 'col col-sm-4 d-inline-block switch border-0' } }, description: { From 3ee09834b1d070c11630d1221ccbbb746982c801 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Apr 2023 13:47:39 +0200 Subject: [PATCH 07/41] 101289: #1578 Changed primarybitstream field to primaryBitstreamUUID --- .../edit-bitstream-page/edit-bitstream-page.component.ts | 2 +- src/app/core/shared/bundle.model.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index df8f285a6a..d84b8ae4e6 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -371,7 +371,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * Path to patch primary bitstream on the bundle * @private */ - private readonly primaryBitstreamPath = '/primarybitstream'; + private readonly primaryBitstreamPath = '/primaryBitstreamUUID'; constructor(private route: ActivatedRoute, private router: Router, diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 72c466d2d6..bcaa582703 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { autoserializeAs, deserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; @@ -30,9 +30,9 @@ export class Bundle extends DSpaceObject { }; /** - * The ID of the primaryBitstream of this Bundle + * The UUID of the primaryBitstream of this Bundle */ - @autoserializeAs('primarybitstream') + @autoserialize primaryBitstreamUUID: string; /** From 14fb3794196c7c3d28badedf340bb41f6520bdf4 Mon Sep 17 00:00:00 2001 From: samuel Date: Wed, 23 Nov 2022 15:40:00 +0100 Subject: [PATCH 08/41] 96598: Test feedback pt. 3: submission - repair auto-refresh --- src/app/submission/sections/form/section-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 9d9fe361de..000e8b0834 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -223,8 +223,8 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { const sectionDataToCheck = {}; Object.keys(sectionData).forEach((key) => { - if (this.sectionMetadata && this.sectionMetadata.includes(key)) { - sectionDataToCheck[key] = sectionData[key]; + if (this.sectionData.data && hasValue(this.sectionData.data[key])) { + sectionDataToCheck[key] = this.sectionData.data[key]; } }); From dff343f62eb67f061a06a235487d8371074d322c Mon Sep 17 00:00:00 2001 From: samuel Date: Wed, 23 Nov 2022 16:16:12 +0100 Subject: [PATCH 09/41] 96598: Test feedback pt. 3: submission - repair auto-refresh - repair tests --- .../submission/sections/form/section-form.component.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index 592691e677..4a6bcf5b9a 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -295,7 +295,9 @@ describe('SubmissionSectionFormComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = {}; - compAsAny.sectionMetadata = ['dc.title']; + compAsAny.sectionData.data = { + 'dc.title': [new FormFieldMetadataValueObject('test')] + }; expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy(); }); From 4abdea5f62e02dd2d30ae6d91930bbc3c1e983fe Mon Sep 17 00:00:00 2001 From: samuel Date: Thu, 9 Feb 2023 16:55:48 +0100 Subject: [PATCH 10/41] 99221: Fix minor issues with Relationships in submission - hide disabled buttons --- src/app/shared/form/form.component.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 16ced852b1..b48675b304 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -13,22 +13,20 @@ (ngbEvent)="onCustomEvent($event)"> -
-
+
From b318d7281ad9602f1afaf159f926391cc58f7ed6 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Fri, 12 May 2023 17:18:44 +0200 Subject: [PATCH 11/41] 101654: Fixed relationship fields not being hidden when they are out of scope --- .../form/section-form.component.spec.ts | 32 +++++++++++++++++++ .../sections/form/section-form.component.ts | 13 ++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index cd5005f852..4ea37b93e0 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -347,6 +347,22 @@ describe('SubmissionSectionFormComponent test suite', () => { } as FormFieldModel ] }, + { + fields: [ + { + selectableMetadata: [{ metadata: 'scoped.workflow.relation' }], + scope: 'WORKFLOW', + } as FormFieldModel, + ], + }, + { + fields: [ + { + selectableMetadata: [{ metadata: 'scoped.workspace.relation' }], + scope: 'WORKSPACE', + } as FormFieldModel, + ], + }, { fields: [ { @@ -375,6 +391,14 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should return false for fields scoped to workflow', () => { expect((comp as any).inCurrentSubmissionScope('scoped.workflow')).toBe(false); }); + + it('should return true for relation fields scoped to workspace', () => { + expect((comp as any).inCurrentSubmissionScope('scoped.workspace.relation')).toBe(true); + }); + + it('should return false for relation fields scoped to workflow', () => { + expect((comp as any).inCurrentSubmissionScope('scoped.workflow.relation')).toBe(false); + }); }); describe('in workflow scope', () => { @@ -394,6 +418,14 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should return false for fields scoped to workspace', () => { expect((comp as any).inCurrentSubmissionScope('scoped.workspace')).toBe(false); }); + + it('should return true for relation fields scoped to workflow', () => { + expect((comp as any).inCurrentSubmissionScope('scoped.workflow.relation')).toBe(true); + }); + + it('should return false for relation fields scoped to workspace', () => { + expect((comp as any).inCurrentSubmissionScope('scoped.workspace.relation')).toBe(false); + }); }); }); diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index ed7573799f..59f8ba09e5 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -38,6 +38,7 @@ import { WorkflowItem } from '../../../core/submission/models/workflowitem.model import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionSectionObject } from '../../objects/submission-section-object.model'; import { SubmissionSectionError } from '../../objects/submission-section-error.model'; +import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; /** * This component represents a section that contains a Form. @@ -255,9 +256,15 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { * @private */ private inCurrentSubmissionScope(field: string): boolean { - const scope = this.formConfig?.rows.find(row => { - return row.fields?.[0]?.selectableMetadata?.[0]?.metadata === field; - }).fields?.[0]?.scope; + const scope = this.formConfig?.rows.find((row: FormRowModel) => { + if (row.fields?.[0]?.selectableMetadata) { + return row.fields?.[0]?.selectableMetadata?.[0]?.metadata === field; + } else if (row.fields?.[0]?.selectableRelationship) { + return row.fields?.[0]?.selectableRelationship.relationshipType === field.replace(/^relationship\./g, ''); + } else { + return false; + } + })?.fields?.[0]?.scope; switch (scope) { case SubmissionScopeType.WorkspaceItem: { From 536cb62cc06f83374475e7e31d4b7e920f506304 Mon Sep 17 00:00:00 2001 From: damian Date: Mon, 15 May 2023 16:09:17 +0200 Subject: [PATCH 12/41] Displaying item counts at Communities and Collections list view. --- .../community-list/community-list.component.html | 1 + src/app/core/shared/collection.model.ts | 7 ++++++- src/app/core/shared/community.model.ts | 7 ++++++- .../collection-list-element.component.html | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index ea772bb891..efc1f086ae 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -37,6 +37,7 @@ {{node.name}} + [{{node.payload.archivedItems}}]
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index d1c49c8d4b..81efc33510 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,4 +1,4 @@ -import { deserialize, inheritSerialization } from 'cerialize'; +import {autoserialize, deserialize, inheritSerialization} from 'cerialize'; import { Observable } from 'rxjs'; import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list.model'; @@ -16,12 +16,17 @@ import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; import { HandleObject } from './handle-object.model'; +import {excludeFromEquals} from '../utilities/equals.decorators'; @typedObject @inheritSerialization(DSpaceObject) export class Collection extends DSpaceObject implements ChildHALResource, HandleObject { static type = COLLECTION; + @excludeFromEquals + @autoserialize + archivedItems: number; + /** * The {@link HALLink}s for this Collection */ diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index f16aad9645..0bab8cfa22 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -1,4 +1,4 @@ -import { deserialize, inheritSerialization } from 'cerialize'; +import {autoserialize, deserialize, inheritSerialization} from 'cerialize'; import { Observable } from 'rxjs'; import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list.model'; @@ -12,12 +12,17 @@ import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; import { ChildHALResource } from './child-hal-resource.model'; import { HandleObject } from './handle-object.model'; +import {excludeFromEquals} from '../utilities/equals.decorators'; @typedObject @inheritSerialization(DSpaceObject) export class Community extends DSpaceObject implements ChildHALResource, HandleObject { static type = COMMUNITY; + @excludeFromEquals + @autoserialize + archivedItems: number; + /** * The {@link HALLink}s for this Community */ diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index c61adf5dad..b45a9a03bc 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -3,7 +3,9 @@ {{object.name}} + [{{object.archivedItems}}] +
{{object.shortDescription}}
From da7cb11bcb5ba54290d8a04f3a30a20075a89f40 Mon Sep 17 00:00:00 2001 From: damian Date: Mon, 15 May 2023 20:34:46 +0200 Subject: [PATCH 13/41] Archived items moved outside initial span element(fix for tests) --- .../collection-list-element.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index b45a9a03bc..b1900908a5 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -3,9 +3,10 @@ {{object.name}} - [{{object.archivedItems}}] +[{{object.archivedItems}}] +
{{object.shortDescription}}
From 388c08b9a13cf636eb0487e595a9e1017bad1d0f Mon Sep 17 00:00:00 2001 From: damian Date: Mon, 15 May 2023 20:34:46 +0200 Subject: [PATCH 14/41] Archived items moved outside initial span element(fix for tests). Tests added. --- .../community-list.component.html | 2 +- .../collection-list-element.component.html | 4 +- .../collection-list-element.component.spec.ts | 52 ++++++++++++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index efc1f086ae..5f940b4405 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -37,7 +37,7 @@ {{node.name}} - [{{node.payload.archivedItems}}] + [{{node.payload.archivedItems}}]
diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index b1900908a5..1fdb97f17b 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -4,9 +4,7 @@ {{object.name}} - -[{{object.archivedItems}}] - +[{{object.archivedItems}}]
{{object.shortDescription}}
diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts index c41d5a7314..c1d9665bc3 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts @@ -7,6 +7,29 @@ import { Collection } from '../../../core/shared/collection.model'; let collectionListElementComponent: CollectionListElementComponent; let fixture: ComponentFixture; +const mockCollectionWithArchivedItems: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItems: 1 +}); + +const mockCollectionWithoutArchivedItems: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItems: 0 +}); + + const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { metadata: { 'dc.description.abstract': [ @@ -15,7 +38,7 @@ const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { value: 'Short description' } ] - } + }, archivedItems: 1 }); const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), { @@ -26,7 +49,7 @@ const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection() value: 'Test title' } ] - } + }, archivedItems: 1 }); describe('CollectionListElementComponent', () => { @@ -71,4 +94,29 @@ describe('CollectionListElementComponent', () => { expect(collectionAbstractField).toBeNull(); }); }); + + + describe('When the collection has archived items', () => { + beforeEach(() => { + collectionListElementComponent.object = mockCollectionWithArchivedItems; + fixture.detectChanges(); + }); + + it('should show the archived items paragraph', () => { + const field = fixture.debugElement.query(By.css('span.archived-items-lead')); + expect(field).not.toBeNull(); + }); + }); + + describe('When the collection has no archived items', () => { + beforeEach(() => { + collectionListElementComponent.object = mockCollectionWithoutArchivedItems; + fixture.detectChanges(); + }); + + it('should not show the archived items paragraph', () => { + const field = fixture.debugElement.query(By.css('span.archived-items-lead')); + expect(field).toBeNull(); + }); + }); }); From d69adab419cc415295e8240ffa05992701459182 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 7 Jun 2023 16:22:57 +0200 Subject: [PATCH 15/41] refactor primarybitstreams to work with DELETE, POST and PUT instead of PATCh --- .../bitstream-page/bitstream-page.resolver.ts | 2 +- .../edit-bitstream-page.component.ts | 96 ++++++++----- .../data/primary-bitstream-data.service.ts | 134 ++++++++++++++++++ src/app/core/shared/bundle.model.ts | 6 - 4 files changed, 197 insertions(+), 41 deletions(-) create mode 100644 src/app/core/data/primary-bitstream-data.service.ts diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index be92041dfc..db2af2d554 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -12,7 +12,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; * Requesting them as embeds will limit the number of requests */ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('bundle', {}, followLink('item')), + followLink('bundle', {}, followLink('primaryBitstream'), followLink('item')), followLink('format') ]; diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 10d9652a65..8b3dad3c1c 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap, tap, filter } from 'rxjs/operators'; import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -14,7 +14,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; @@ -27,6 +27,8 @@ import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-u import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { BundleDataService } from '../../core/data/bundle-data.service'; import { Operation } from 'fast-json-patch'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { hasSucceeded } from '../../core/data/request-entry-state.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -51,6 +53,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ bitstreamFormatsRD$: Observable>>; + /** + * The UUID of the primary bitstream for this bundle + */ + primaryBitstreamUUID: string; + /** * The bitstream to edit */ @@ -383,6 +390,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { public dsoNameService: DSONameService, private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService, + private primaryBitstreamService: PrimaryBitstreamService, private bundleService: BundleDataService) { } @@ -414,6 +422,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { getFirstSucceededRemoteDataPayload(), ); + const primaryBitstream$ = bundle$.pipe( + hasValueOperator(), + switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), + getFirstSucceededRemoteDataPayload() + ); + const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), getFirstSucceededRemoteDataPayload() @@ -423,12 +437,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bitstream$, allFormats$, bundle$, + primaryBitstream$, item$, ).pipe() - .subscribe(([bitstream, allFormats, bundle, item]) => { + .subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; this.bundle = bundle; + // hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will + // be a success response, but empty + this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null; this.itemId = item.uuid; this.setIiifStatus(this.bitstream); }) @@ -460,7 +478,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ fileNamePrimaryContainer: { fileName: bitstream.name, - primaryBitstream: this.bundle.primaryBitstreamUUID === bitstream.uuid + primaryBitstream: this.primaryBitstreamUUID === bitstream.uuid }, descriptionContainer: { description: bitstream.firstMetadataValue('dc.description') @@ -572,26 +590,52 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); const isNewFormat = selectedFormat.id !== this.originalFormat.id; const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream; - const wasPrimary = this.bundle.primaryBitstreamUUID === this.bitstream.uuid; + const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid; let bitstream$; let bundle$: Observable; + let errorWhileSaving = false; if (wasPrimary !== isPrimary) { - const patchOperations: Operation[] = this.retrieveBundlePatch(wasPrimary); - bundle$ = this.bundleService.patch(this.bundle, patchOperations).pipe( - getFirstCompletedRemoteData(), - map((bundleResponse: RemoteData) => { - if (hasValue(bundleResponse) && bundleResponse.hasFailed) { - this.notificationsService.error( - this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), - bundleResponse.errorMessage - ); + let bundleRd$: Observable>; + if (wasPrimary) { + bundleRd$ = this.primaryBitstreamService.delete(this.bundle); + } else if (hasValue(this.primaryBitstreamUUID)) { + bundleRd$ = this.primaryBitstreamService.put(this.bitstream, this.bundle); + } else { + bundleRd$ = this.primaryBitstreamService.create(this.bitstream, this.bundle); + } + + const completedBundleRd$ = bundleRd$.pipe(getFirstCompletedRemoteData()); + + this.subs.push(completedBundleRd$.pipe( + filter((bundleRd: RemoteData) => bundleRd.hasFailed) + ).subscribe((bundleRd: RemoteData) => { + this.notificationsService.error( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.primaryBitstream.title'), + bundleRd.errorMessage + ); + errorWhileSaving = true; + })); + + bundle$ = completedBundleRd$.pipe( + map((bundleRd: RemoteData) => { + if (bundleRd.hasSucceeded) { + return bundleRd.payload } else { - return bundleResponse.payload; + return this.bundle; } }) ); + + this.subs.push(bundle$.pipe( + hasValueOperator(), + switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href, false)), + getFirstSucceededRemoteDataPayload() + ).subscribe((bitstream: Bitstream) => { + this.primaryBitstreamUUID = hasValue(bitstream) ? bitstream.uuid : null; + })); + } else { bundle$ = observableOf(this.bundle); } @@ -626,28 +670,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.title'), this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.content') ); - this.navigateToItemEditBitstreams(); + if (!errorWhileSaving) { + this.navigateToItemEditBitstreams(); + } }); } - private retrieveBundlePatch(wasPrimary: boolean): Operation[] { - // No longer primary bitstream: remove - if (wasPrimary) { - return [{ - path: this.primaryBitstreamPath, - op: 'remove' - }]; - } else { - // Has become primary bitstream - // If it already had a value: replace, otherwise: add - return [{ - path: this.primaryBitstreamPath, - op: hasValue(this.bundle.primaryBitstreamUUID) ? 'replace' : 'add', - value: this.bitstream.uuid - }]; - } - } - /** * Parse form data to an updated bitstream object * @param rawForm Raw form data diff --git a/src/app/core/data/primary-bitstream-data.service.ts b/src/app/core/data/primary-bitstream-data.service.ts new file mode 100644 index 0000000000..6380a6f148 --- /dev/null +++ b/src/app/core/data/primary-bitstream-data.service.ts @@ -0,0 +1,134 @@ +import { Bitstream } from '../shared/bitstream.model'; +import { DeleteDataImpl } from './base/delete-data'; +import { PutDataImpl } from './base/put-data'; +import { CreateDataImpl } from './base/create-data'; +import { Injectable } from '@angular/core'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Observable, switchMap } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { Bundle } from '../shared/bundle.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { constructIdEndpointDefault } from './base/identifiable-data.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HttpHeaders } from '@angular/common/http'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { PutRequest, PostRequest, DeleteRequest } from './request.models'; +import { getAllCompletedRemoteData } from '../shared/operators'; +import { NoContent } from '../shared/NoContent.model'; +import { BundleDataService } from './bundle-data.service'; + +@Injectable({ + providedIn: 'root', +}) +export class PrimaryBitstreamService { + private createData: CreateDataImpl; + private putData: PutDataImpl; + private deleteData: DeleteDataImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected bundleDataService: BundleDataService, + ) { + // linkPath can be undefined because we'll only use them to do things "byHref" + this.createData = new CreateDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined); + this.putData = new PutDataImpl(undefined, requestService, rdbService, objectCache, halService, undefined); + this.deleteData = new DeleteDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined, constructIdEndpointDefault); + } + + /** + * Returns the type of HttpOptions object needed from primary bitstream requests. + * i.e. with a Content-Type header set to `text/uri-list` + * @protected + */ + protected getHttpOptions(): HttpOptions { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return options; + } + + /** + * Send a request of the given type to the endpointURL with an optional primaryBitstreamSelfLink + * as payload, and return the resulting Observable + * + * @param requestType The type of request: PostRequest, PutRequest, or DeleteRequest + * @param endpointURL The endpoint URL + * @param primaryBitstreamSelfLink + * @protected + */ + protected createAndSendRequest( + requestType: GenericConstructor, + endpointURL: string, + primaryBitstreamSelfLink?: string, + ): Observable> { + const requestId = this.requestService.generateRequestId(); + const request = new requestType( + requestId, + endpointURL, + primaryBitstreamSelfLink, + this.getHttpOptions() + ); + + this.requestService.send(request); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + /** + * Create a new primaryBitstream + * + * @param primaryBitstream The object to create + * @param bundle The bundle to create it on + */ + create(primaryBitstream: Bitstream, bundle: Bundle): Observable> { + return this.createAndSendRequest( + PostRequest, + bundle._links.primaryBitstream.href, + primaryBitstream.self + ) as Observable>; + } + + /** + * Update an exiting primaryBitstream + * + * @param primaryBitstream The object to update + * @param bundle The bundle to update it on + */ + put(primaryBitstream: Bitstream, bundle: Bundle): Observable> { + return this.createAndSendRequest( + PutRequest, + bundle._links.primaryBitstream.href, + primaryBitstream.self + ) as Observable>; + } + + /** + * Delete an exiting primaryBitstream + * + * @param bundle The bundle to delete it from + */ + delete(bundle: Bundle): Observable> { + return this.createAndSendRequest( + DeleteRequest, + bundle._links.primaryBitstream.href + ).pipe( + getAllCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return this.bundleDataService.findByHref(bundle.self, false); + } else { + [bundle]; + } + }) + ); + } + +} diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index bcaa582703..f37cd96545 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -29,12 +29,6 @@ export class Bundle extends DSpaceObject { item: HALLink; }; - /** - * The UUID of the primaryBitstream of this Bundle - */ - @autoserialize - primaryBitstreamUUID: string; - /** * The primary Bitstream of this Bundle * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. From 56cba82c2df9e2fd2cd9816db7c5fe7a2dfe419a Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 14:16:04 +0200 Subject: [PATCH 16/41] 101289: intermediate commit for test issues --- .../edit-bitstream-page.component.spec.ts | 140 ++++++++++-------- .../edit-bitstream-page.component.ts | 31 ++-- 2 files changed, 91 insertions(+), 80 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 04ab4e47b0..3be3297880 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -24,8 +24,7 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; import { MetadataValueFilter } from '../../core/shared/metadata.models'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { Bundle } from '../../core/shared/bundle.model'; -import { BundleDataService } from '../../core/data/bundle-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -34,6 +33,7 @@ const successNotification: INotification = new Notification('id', NotificationTy let notificationsService: NotificationsService; let formService: DynamicFormService; let bitstreamService: BitstreamDataService; +let primaryBitstreamService: PrimaryBitstreamService; let bitstreamFormatService: BitstreamFormatDataService; let dsoNameService: DSONameService; let bitstream: Bitstream; @@ -41,23 +41,19 @@ let bitstreamID: string; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; -let bundleDataService; -let bundleWithCurrentPrimary: Bundle; -let bundleWithDifferentPrimary: Bundle; -let bundleWithNoPrimary: Bundle; - +let currentPrimary: string; +let differentPrimary: string; +let bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; -describe('EditBitstreamPageComponent', () => { +fdescribe('EditBitstreamPageComponent', () => { beforeEach(() => { bitstreamID = 'current-bitstream-id'; - bundleWithCurrentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': bitstreamID }); - bundleWithDifferentPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': '12345-abcde-54321-edcba' }); - bundleWithNoPrimary = Object.assign(new Bundle(), { 'primaryBitstreamUUID': null }); - bundleDataService = jasmine.createSpyObj('BundleDataService', ['patch']); - bundleDataService.patch.and.callFake((a, b) => createSuccessfulRemoteDataObject$(a)); + currentPrimary = bitstreamID; + differentPrimary = '12345-abcde-54321-edcba'; + allFormats = [ Object.assign({ id: '1', @@ -116,12 +112,47 @@ describe('EditBitstreamPageComponent', () => { success: successNotification } ); + + bundle = { + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }; + + const result = createSuccessfulRemoteDataObject$(bundle); + primaryBitstreamService = jasmine.createSpyObj('PrimaryBitstreamService', + { + put: result, + create: result, + delete: result, + }); + }); describe('EditBitstreamPageComponent no IIIF fields', () => { beforeEach(waitForAsync(() => { - + bundle = { + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }; const bundleName = 'ORIGINAL'; bitstream = Object.assign(new Bitstream(), { @@ -143,17 +174,11 @@ describe('EditBitstreamPageComponent', () => { _links: { self: 'bitstream-selflink' }, - bundle: createSuccessfulRemoteDataObject$({ - item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { - uuid: 'some-uuid', - firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { - return undefined; - }, - })) - }) + bundle: createSuccessfulRemoteDataObject$(bundle) }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, @@ -182,7 +207,7 @@ describe('EditBitstreamPageComponent', () => { { provide: BitstreamDataService, useValue: bitstreamService }, { provide: DSONameService, useValue: dsoNameService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -198,7 +223,7 @@ describe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - describe('on startup', () => { + fdescribe('on startup', () => { let rawForm; beforeEach(() => { @@ -222,7 +247,7 @@ describe('EditBitstreamPageComponent', () => { }); describe('when the bitstream is the primary bitstream on the bundle', () => { beforeEach(() => { - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.setForm(); rawForm = comp.formGroup.getRawValue(); @@ -233,7 +258,7 @@ describe('EditBitstreamPageComponent', () => { }); describe('when the bitstream is not the primary bitstream on the bundle', () => { beforeEach(() => { - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.setForm(); rawForm = comp.formGroup.getRawValue(); }); @@ -263,38 +288,23 @@ describe('EditBitstreamPageComponent', () => { describe('from a different primary bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.onSubmit(); }); - it('should call patch with a replace operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ - op: 'replace' - })]); - }); - - it('should call patch with the correct bitstream uuid', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithDifferentPrimary, [jasmine.objectContaining({ - value: bitstreamID - })]); + it('should call put with the correct bitstream on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle); }); }); + describe('from no primary bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithNoPrimary; + (comp as any).primaryBitstreamUUID = null; comp.onSubmit(); }); - it('should call patch with an add operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ - op: 'add' - })]); - }); - - it('should call patch with the correct bitstream uuid', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithNoPrimary, [jasmine.objectContaining({ - value: bitstreamID - })]); + it('should call create with the correct bitstream on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.create).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle); }); }); }); @@ -306,39 +316,42 @@ describe('EditBitstreamPageComponent', () => { describe('from the current bitstream', () => { beforeEach(() => { - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.onSubmit(); }); - it('should call patch with a remove operation', () => { - expect(bundleDataService.patch).toHaveBeenCalledWith(bundleWithCurrentPrimary, [jasmine.objectContaining({ - op: 'remove' - })]); + it('should call delete on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.delete).toHaveBeenCalledWith(jasmine.objectContaining(bundle)); }); }); }); }); - describe('when the primaryBitstream did not changed', () => { + describe('when the primaryBitstream did not change', () => { describe('the current bitstream stayed the primary bitstream', () => { beforeEach(() => { const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: true } }); spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); - (comp as any).bundle = bundleWithCurrentPrimary; + (comp as any).primaryBitstreamUUID = currentPrimary; comp.onSubmit(); }); - it('should not call patch on the bundle data service', () => { - expect(bundleDataService.patch).not.toHaveBeenCalled(); + it('should not call anything on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).not.toHaveBeenCalled(); + expect(primaryBitstreamService.delete).not.toHaveBeenCalled(); + expect(primaryBitstreamService.create).not.toHaveBeenCalled(); }); }); + describe('the bitstream was not and did not become the primary bitstream', () => { beforeEach(() => { const rawValue = Object.assign(comp.formGroup.getRawValue(), { fileNamePrimaryContainer: { primaryBitstream: false } }); spyOn(comp.formGroup, 'getRawValue').and.returnValue(rawValue); - (comp as any).bundle = bundleWithDifferentPrimary; + (comp as any).primaryBitstreamUUID = differentPrimary; comp.onSubmit(); }); - it('should not call patch on the bundle data service', () => { - expect(bundleDataService.patch).not.toHaveBeenCalled(); + it('should not call anything on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).not.toHaveBeenCalled(); + expect(primaryBitstreamService.delete).not.toHaveBeenCalled(); + expect(primaryBitstreamService.create).not.toHaveBeenCalled(); }); }); }); @@ -451,6 +464,7 @@ describe('EditBitstreamPageComponent', () => { }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, @@ -477,7 +491,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -595,7 +609,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, - { provide: BundleDataService, useValue: bundleDataService }, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -617,7 +631,7 @@ describe('EditBitstreamPageComponent', () => { rawForm = comp.formGroup.getRawValue(); }); - it('should NOT set isIIIF to true', () => { + it('should NOT set is IIIF to true', () => { expect(comp.isIIIF).toBeFalse(); }); it('should put the \"IIIF Label\" input not to be shown', () => { diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 8b3dad3c1c..25c7f41868 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, switchMap, tap, filter } from 'rxjs/operators'; +import { filter, map, switchMap, tap } from 'rxjs/operators'; import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -14,7 +14,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; -import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; @@ -25,10 +25,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Item } from '../../core/shared/item.model'; import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; -import { BundleDataService } from '../../core/data/bundle-data.service'; -import { Operation } from 'fast-json-patch'; import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; -import { hasSucceeded } from '../../core/data/request-entry-state.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -374,12 +371,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ private bundle: Bundle; - /** - * Path to patch primary bitstream on the bundle - * @private - */ - private readonly primaryBitstreamPath = '/primaryBitstreamUUID'; - constructor(private route: ActivatedRoute, private router: Router, private changeDetectorRef: ChangeDetectorRef, @@ -391,7 +382,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService, private primaryBitstreamService: PrimaryBitstreamService, - private bundleService: BundleDataService) { + ) { } /** @@ -404,33 +395,39 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.itemId = this.route.snapshot.queryParams.itemId; this.entityType = this.route.snapshot.queryParams.entityType; - this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream)); + this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream)); this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); const bitstream$ = this.bitstreamRD$.pipe( getFirstSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), + tap(t => console.log(t)), ); const allFormats$ = this.bitstreamFormatsRD$.pipe( getFirstSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), + tap(t => console.log(t)), ); const bundle$ = bitstream$.pipe( switchMap((bitstream: Bitstream) => bitstream.bundle), getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); const primaryBitstream$ = bundle$.pipe( hasValueOperator(), + tap(t => console.log(t._links.primaryBitstream.href)), switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), + tap(t => console.log(t)), ); this.subs.push( observableCombineLatest( From c9324b0714fdaadd7716e9184d4e0876238b759a Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 8 Jun 2023 15:44:30 +0200 Subject: [PATCH 17/41] fix issue where iiif tests would fail --- .../edit-bitstream-page.component.spec.ts | 19 +++++++++++++++---- .../edit-bitstream-page.component.ts | 6 ------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 3be3297880..2e8c6368c1 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -47,7 +47,7 @@ let bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; -fdescribe('EditBitstreamPageComponent', () => { +describe('EditBitstreamPageComponent', () => { beforeEach(() => { bitstreamID = 'current-bitstream-id'; @@ -223,7 +223,7 @@ fdescribe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - fdescribe('on startup', () => { + describe('on startup', () => { let rawForm; beforeEach(() => { @@ -454,13 +454,18 @@ fdescribe('EditBitstreamPageComponent', () => { self: 'bitstream-selflink' }, bundle: createSuccessfulRemoteDataObject$({ + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { uuid: 'some-uuid', firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { return 'True'; } })) - }) + }), }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), @@ -574,16 +579,22 @@ fdescribe('EditBitstreamPageComponent', () => { self: 'bitstream-selflink' }, bundle: createSuccessfulRemoteDataObject$({ + _links: { + primaryBitstream: { + href: 'bundle-selflink' + } + }, item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { uuid: 'some-uuid', firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { return 'True'; } })) - }) + }), }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream), + findByHref: createSuccessfulRemoteDataObject$(bitstream), update: createSuccessfulRemoteDataObject$(bitstream), updateFormat: createSuccessfulRemoteDataObject$(bitstream), commitUpdates: {}, diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 25c7f41868..0d5e1eecc1 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -401,33 +401,27 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const bitstream$ = this.bitstreamRD$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - tap(t => console.log(t)), ); const allFormats$ = this.bitstreamFormatsRD$.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - tap(t => console.log(t)), ); const bundle$ = bitstream$.pipe( switchMap((bitstream: Bitstream) => bitstream.bundle), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); const primaryBitstream$ = bundle$.pipe( hasValueOperator(), - tap(t => console.log(t._links.primaryBitstream.href)), switchMap((bundle: Bundle) => this.bitstreamService.findByHref(bundle._links.primaryBitstream.href)), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); const item$ = bundle$.pipe( switchMap((bundle: Bundle) => bundle.item), getFirstSucceededRemoteDataPayload(), - tap(t => console.log(t)), ); this.subs.push( observableCombineLatest( From a936878c9673dc79d98404939595cf5c6b2f3ded Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 16:02:33 +0200 Subject: [PATCH 18/41] 101289: tests --- .../edit-bitstream-page.component.spec.ts | 2 +- .../edit-bitstream-page.component.ts | 2 +- .../data/primary-bitstream.service.spec.ts | 158 ++++++++++++++++++ ...ervice.ts => primary-bitstream.service.ts} | 6 +- 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/app/core/data/primary-bitstream.service.spec.ts rename src/app/core/data/{primary-bitstream-data.service.ts => primary-bitstream.service.ts} (97%) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 2e8c6368c1..b83f2b9664 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -24,7 +24,7 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; import { MetadataValueFilter } from '../../core/shared/metadata.models'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 0d5e1eecc1..cdc905d456 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -25,7 +25,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Item } from '../../core/shared/item.model'; import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; -import { PrimaryBitstreamService } from '../../core/data/primary-bitstream-data.service'; +import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; @Component({ selector: 'ds-edit-bitstream-page', diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts new file mode 100644 index 0000000000..93e9882d04 --- /dev/null +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -0,0 +1,158 @@ +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RequestService } from './request.service'; +import { Bitstream } from '../shared/bitstream.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { PrimaryBitstreamService } from './primary-bitstream.service'; +import { BundleDataService } from './bundle-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { CreateRequest, DeleteRequest, PostRequest, PutRequest } from './request.models'; +import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Bundle } from '../shared/bundle.model'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + +fdescribe('PrimaryBitstreamService', () => { + let service: PrimaryBitstreamService; + let objectCache: ObjectCacheService; + let requestService: RequestService; + let halService: HALEndpointService; + let rdbService: RemoteDataBuildService; + let notificationService: NotificationsService; + let bundleDataService: BundleDataService; + + const bitstream = Object.assign(new Bitstream(), { + uuid: 'fake-bitstream', + _links: { + self: { href: 'fake-bitstream-self' } + } + }); + + const bundle = Object.assign(new Bundle(), { + uuid: 'fake-bundle', + _links: { + self: { href: 'fake-bundle-self' }, + primaryBitstream: { href: 'fake-primary-bitstream-self' }, + } + }); + + const url = 'fake-bitstream-url'; + + beforeEach(() => { + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + halService = Object.assign(new HALEndpointServiceStub(url)); + + rdbService = getMockRemoteDataBuildService(); + notificationService = new NotificationsServiceStub() as any; + bundleDataService = jasmine.createSpyObj('bundleDataService', {'findByHref': createSuccessfulRemoteDataObject$(bundle)}); + service = new PrimaryBitstreamService(requestService, rdbService, objectCache, halService, notificationService, bundleDataService); + }); + + describe('getHttpOptions', () => { + it('should return a HttpOptions object with text/url-list Context-Type header', () => { + const result = (service as any).getHttpOptions() + expect(result.headers.get('Content-Type')).toEqual('text/uri-list'); + }); + }); + + describe('createAndSendRequest', () => { + const testId = '12345-12345'; + const options = {}; + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + + beforeEach(() => { + spyOn(service as any, 'getHttpOptions').and.returnValue(options); + (requestService.generateRequestId as jasmine.Spy).and.returnValue(testId); + spyOn(rdbService, 'buildFromRequestUUID').and.returnValue(observableOf(testResult)); + }); + + it('should return a Request object with the given constructor and the given parameters', () => { + const result = (service as any).createAndSendRequest(CreateRequest, url, bitstream.self); + const request = new CreateRequest(testId, url, bitstream.self, options); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect(requestService.send).toHaveBeenCalledWith(request); + expect(rdbService.buildFromRequestUUID).toHaveBeenCalledWith(testId); + }); + }); + + describe('create', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest', () => { + const result = service.create(bitstream, bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + PostRequest, + bundle._links.primaryBitstream.href, + bitstream.self + ); + }); + }); + describe('put', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest and return the requested bundle', () => { + const result = service.put(bitstream, bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + PutRequest, + bundle._links.primaryBitstream.href, + bitstream.self + ); + }); + }); + describe('delete', () => { + describe('when the delete request succeeds', () => { + const testResult = createSuccessfulRemoteDataObject(new Bundle()); + const bundleServiceResult = createSuccessfulRemoteDataObject(bundle); + + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + (bundleDataService.findByHref as jasmine.Spy).and.returnValue(observableOf(bundleServiceResult)); + }); + + it('should delegate the call to createAndSendRequest', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + DeleteRequest, + bundle._links.primaryBitstream.href, + ); + }); + }); + describe('when the delete request fails', () => { + const testResult = createFailedRemoteDataObject(); + + beforeEach(() => { + spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + }); + + it('should delegate the call to createAndSendRequest and retrieve the bundle from the rdbService', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundle }); + + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + DeleteRequest, + bundle._links.primaryBitstream.href, + ); + }); + }); + }); +}); diff --git a/src/app/core/data/primary-bitstream-data.service.ts b/src/app/core/data/primary-bitstream.service.ts similarity index 97% rename from src/app/core/data/primary-bitstream-data.service.ts rename to src/app/core/data/primary-bitstream.service.ts index 6380a6f148..5c1c3c52ac 100644 --- a/src/app/core/data/primary-bitstream-data.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -97,7 +97,7 @@ export class PrimaryBitstreamService { } /** - * Update an exiting primaryBitstream + * Update an existing primaryBitstream * * @param primaryBitstream The object to update * @param bundle The bundle to update it on @@ -111,7 +111,7 @@ export class PrimaryBitstreamService { } /** - * Delete an exiting primaryBitstream + * Delete an existing primaryBitstream * * @param bundle The bundle to delete it from */ @@ -125,7 +125,7 @@ export class PrimaryBitstreamService { if (rd.hasSucceeded) { return this.bundleDataService.findByHref(bundle.self, false); } else { - [bundle]; + return this.rdbService.buildSingle(bundle.self); } }) ); From 9d2fed4186186e22fab2b72a07e200c8c6a004de Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 8 Jun 2023 16:42:25 +0200 Subject: [PATCH 19/41] 101289: Fixed test issues --- .../edit-bitstream-page.component.ts | 2 +- .../data/primary-bitstream.service.spec.ts | 45 ++++++++++++++----- .../core/data/primary-bitstream.service.ts | 6 +-- src/app/core/shared/bundle.model.ts | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index cdc905d456..b77d2151a9 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -612,7 +612,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bundle$ = completedBundleRd$.pipe( map((bundleRd: RemoteData) => { if (bundleRd.hasSucceeded) { - return bundleRd.payload + return bundleRd.payload; } else { return this.bundle; } diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts index 93e9882d04..00d6d7f03c 100644 --- a/src/app/core/data/primary-bitstream.service.spec.ts +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -16,7 +16,7 @@ import { Bundle } from '../shared/bundle.model'; import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -fdescribe('PrimaryBitstreamService', () => { +describe('PrimaryBitstreamService', () => { let service: PrimaryBitstreamService; let objectCache: ObjectCacheService; let requestService: RequestService; @@ -57,7 +57,7 @@ fdescribe('PrimaryBitstreamService', () => { describe('getHttpOptions', () => { it('should return a HttpOptions object with text/url-list Context-Type header', () => { - const result = (service as any).getHttpOptions() + const result = (service as any).getHttpOptions(); expect(result.headers.get('Content-Type')).toEqual('text/uri-list'); }); }); @@ -118,9 +118,20 @@ fdescribe('PrimaryBitstreamService', () => { }); }); describe('delete', () => { + const testBundle = Object.assign(new Bundle(), { + _links: { + self: { + href: 'test-href' + }, + primaryBitstream: { + href: 'test-primaryBitstream-href' + } + } + }); + describe('when the delete request succeeds', () => { const testResult = createSuccessfulRemoteDataObject(new Bundle()); - const bundleServiceResult = createSuccessfulRemoteDataObject(bundle); + const bundleServiceResult = createSuccessfulRemoteDataObject(testBundle); beforeEach(() => { spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); @@ -128,30 +139,42 @@ fdescribe('PrimaryBitstreamService', () => { }); it('should delegate the call to createAndSendRequest', () => { - const result = service.delete(bundle); - getTestScheduler().expectObservable(result).toBe('(a|)', { a: testResult }); + const result = service.delete(testBundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundleServiceResult }); + + result.subscribe(); + + expect(bundleDataService.findByHref).toHaveBeenCalledWith(testBundle.self, false); expect((service as any).createAndSendRequest).toHaveBeenCalledWith( DeleteRequest, - bundle._links.primaryBitstream.href, + testBundle._links.primaryBitstream.href, ); }); }); describe('when the delete request fails', () => { const testResult = createFailedRemoteDataObject(); + const bundleServiceResult = createSuccessfulRemoteDataObject(testBundle); beforeEach(() => { spyOn((service as any), 'createAndSendRequest').and.returnValue(observableOf(testResult)); + (bundleDataService.findByHref as jasmine.Spy).and.returnValue(observableOf(bundleServiceResult)); }); - it('should delegate the call to createAndSendRequest and retrieve the bundle from the rdbService', () => { - const result = service.delete(bundle); - getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundle }); - + it('should delegate the call to createAndSendRequest and request the bundle from the bundleDataService', () => { + const result = service.delete(testBundle); + result.subscribe(); expect((service as any).createAndSendRequest).toHaveBeenCalledWith( DeleteRequest, - bundle._links.primaryBitstream.href, + testBundle._links.primaryBitstream.href, ); + expect(bundleDataService.findByHref).toHaveBeenCalledWith(testBundle.self, true); + + }); + + it('should delegate the call to createAndSendRequest and', () => { + const result = service.delete(bundle); + getTestScheduler().expectObservable(result).toBe('(a|)', { a: bundleServiceResult }); }); }); }); diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 5c1c3c52ac..646e8271e8 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -122,11 +122,7 @@ export class PrimaryBitstreamService { ).pipe( getAllCompletedRemoteData(), switchMap((rd: RemoteData) => { - if (rd.hasSucceeded) { - return this.bundleDataService.findByHref(bundle.self, false); - } else { - return this.rdbService.buildSingle(bundle.self); - } + return this.bundleDataService.findByHref(bundle.self, rd.hasFailed); }) ); } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index f37cd96545..36b7012e47 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { deserialize, inheritSerialization } from 'cerialize'; import { Observable } from 'rxjs'; From daf297b94b61556ddb697b440a35f1a941b127e1 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 9 Jun 2023 11:17:00 +0200 Subject: [PATCH 20/41] 101289: Removed unnecessary data services from primary bitstream service --- src/app/core/data/primary-bitstream.service.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 646e8271e8..488cb5d22e 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -1,7 +1,4 @@ import { Bitstream } from '../shared/bitstream.model'; -import { DeleteDataImpl } from './base/delete-data'; -import { PutDataImpl } from './base/put-data'; -import { CreateDataImpl } from './base/create-data'; import { Injectable } from '@angular/core'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -11,7 +8,6 @@ import { Observable, switchMap } from 'rxjs'; import { RemoteData } from './remote-data'; import { Bundle } from '../shared/bundle.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { constructIdEndpointDefault } from './base/identifiable-data.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpHeaders } from '@angular/common/http'; import { GenericConstructor } from '../shared/generic-constructor'; @@ -24,9 +20,6 @@ import { BundleDataService } from './bundle-data.service'; providedIn: 'root', }) export class PrimaryBitstreamService { - private createData: CreateDataImpl; - private putData: PutDataImpl; - private deleteData: DeleteDataImpl; constructor( protected requestService: RequestService, @@ -36,10 +29,6 @@ export class PrimaryBitstreamService { protected notificationsService: NotificationsService, protected bundleDataService: BundleDataService, ) { - // linkPath can be undefined because we'll only use them to do things "byHref" - this.createData = new CreateDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined); - this.putData = new PutDataImpl(undefined, requestService, rdbService, objectCache, halService, undefined); - this.deleteData = new DeleteDataImpl(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined, constructIdEndpointDefault); } /** From c4b25653733323812b4531952d0fd98acb906208 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 9 Jun 2023 13:39:28 +0200 Subject: [PATCH 21/41] Workaround: don't use form-global data for check Note: we're balancing multiple bugs against eachother here, not ideal! - the diff doesn't catch removed Relationship fields; instead, form reload is triggered by a dspace.entity.type change - if we add the original `this.sectionMetadata.includes(key)` check, we filter out this change and the form fails to update - if we add `relation.*` fields to `this.sectionMetadata`, newly added Relationship entries are duplicated As of this commit, the form _seems_ to work in a stable way, but these issues shoud really investigated in more detail. --- src/app/submission/sections/form/section-form.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 9612db523c..3fa9000039 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -229,8 +229,10 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { const sectionDataToCheck = {}; Object.keys(sectionData).forEach((key) => { - if (this.sectionData.data && hasValue(this.sectionData.data[key]) && this.inCurrentSubmissionScope(key)) { - sectionDataToCheck[key] = this.sectionData.data[key]; + // todo: removing Relationships works due to a bug -- dspace.entity.type is included in sectionData, which is what triggers the update; + // if we use this.sectionMetadata.includes(key), this field is filtered out and removed Relationships won't disappear from the form. + if (this.inCurrentSubmissionScope(key)) { + sectionDataToCheck[key] = sectionData[key]; } }); From 8cc96060ff6b9e8ee59f39f3b2ba8144403f161c Mon Sep 17 00:00:00 2001 From: damian Date: Fri, 9 Jun 2023 16:44:29 +0200 Subject: [PATCH 22/41] Variable name change. --- .../community-list/community-list.component.html | 2 +- src/app/core/shared/collection.model.ts | 4 ++-- src/app/core/shared/community.model.ts | 2 +- .../collection-list-element.component.html | 2 +- .../collection-list-element.component.spec.ts | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index 5f940b4405..7f8dcf0c45 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -37,7 +37,7 @@ {{node.name}} - [{{node.payload.archivedItems}}] + [{{node.payload.archivedItemsCount}}]
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 81efc33510..c97c61eceb 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -16,7 +16,7 @@ import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; import { HandleObject } from './handle-object.model'; -import {excludeFromEquals} from '../utilities/equals.decorators'; +import { excludeFromEquals } from '../utilities/equals.decorators'; @typedObject @inheritSerialization(DSpaceObject) @@ -25,7 +25,7 @@ export class Collection extends DSpaceObject implements ChildHALResource, Handle @excludeFromEquals @autoserialize - archivedItems: number; + archivedItemsCount: number; /** * The {@link HALLink}s for this Collection diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 0bab8cfa22..03b47fb024 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -21,7 +21,7 @@ export class Community extends DSpaceObject implements ChildHALResource, HandleO @excludeFromEquals @autoserialize - archivedItems: number; + archivedItemsCount: number; /** * The {@link HALLink}s for this Community diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index 1fdb97f17b..68a84a88e5 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -4,7 +4,7 @@ {{object.name}} -[{{object.archivedItems}}] +[{{object.archivedItemsCount}}]
{{object.shortDescription}}
diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts index c1d9665bc3..b911bdd2eb 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts @@ -15,7 +15,7 @@ const mockCollectionWithArchivedItems: Collection = Object.assign(new Collection value: 'Test title' } ] - }, archivedItems: 1 + }, archivedItemsCount: 1 }); const mockCollectionWithoutArchivedItems: Collection = Object.assign(new Collection(), { @@ -26,7 +26,7 @@ const mockCollectionWithoutArchivedItems: Collection = Object.assign(new Collect value: 'Test title' } ] - }, archivedItems: 0 + }, archivedItemsCount: 0 }); @@ -38,7 +38,7 @@ const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { value: 'Short description' } ] - }, archivedItems: 1 + }, archivedItemsCount: 1 }); const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), { @@ -49,7 +49,7 @@ const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection() value: 'Test title' } ] - }, archivedItems: 1 + }, archivedItemsCount: 1 }); describe('CollectionListElementComponent', () => { From b8d282ebe4ba14765e2cb5d38817934e12f1bb98 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Sun, 11 Jun 2023 16:17:14 +0200 Subject: [PATCH 23/41] Fix proxy timeout error for browse by pages --- src/app/browse-by/browse-by-guard.spec.ts | 35 +++++++++++++++-- src/app/browse-by/browse-by-guard.ts | 48 ++++++++++++++--------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index fc483d87e2..7f57c17ac1 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,10 +1,10 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; import { of as observableOf } from 'rxjs'; -import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; +import { RouterStub } from '../shared/testing/router.stub'; describe('BrowseByGuard', () => { describe('canActivate', () => { @@ -12,6 +12,7 @@ describe('BrowseByGuard', () => { let dsoService: any; let translateService: any; let browseDefinitionService: any; + let router: any; const name = 'An interesting DSO'; const title = 'Author'; @@ -34,7 +35,9 @@ describe('BrowseByGuard', () => { findById: () => createSuccessfulRemoteDataObject$(browseDefinition) }; - guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService); + router = new RouterStub() as any; + + guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, router); }); it('should return true, and sets up the data correctly, with a scope and value', () => { @@ -64,6 +67,7 @@ describe('BrowseByGuard', () => { value: '"' + value + '"' }; expect(scopedRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -96,6 +100,7 @@ describe('BrowseByGuard', () => { value: '' }; expect(scopedNoValueRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -127,9 +132,33 @@ describe('BrowseByGuard', () => { value: '"' + value + '"' }; expect(route.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); }); + + it('should return false, and sets up the data correctly, without a scope and with a value', () => { + jasmine.getEnv().allowRespy(true); + spyOn(browseDefinitionService, 'findById').and.returnValue(createFailedRemoteDataObject$()); + const scopedRoute = { + data: { + title: field, + }, + params: { + id, + }, + queryParams: { + scope, + value + } + }; + guard.canActivate(scopedRoute as any, undefined) + .pipe(first()) + .subscribe((canActivate) => { + expect(router.navigate).toHaveBeenCalled(); + expect(canActivate).toEqual(false); + }); + }); }); }); diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index e4582cb77a..ed6a627558 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,13 +1,15 @@ -import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { Injectable } from '@angular/core'; import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; import { map, switchMap } from 'rxjs/operators'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; +import { RemoteData } from '../core/data/remote-data'; +import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; @Injectable() /** @@ -17,15 +19,20 @@ export class BrowseByGuard implements CanActivate { constructor(protected dsoService: DSpaceObjectDataService, protected translate: TranslateService, - protected browseDefinitionService: BrowseDefinitionDataService) { + protected browseDefinitionService: BrowseDefinitionDataService, + protected router: Router, + ) { } - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const title = route.data.title; const id = route.params.id || route.queryParams.id || route.data.id; - let browseDefinition$: Observable; + let browseDefinition$: Observable; if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { - browseDefinition$ = this.browseDefinitionService.findById(id).pipe(getFirstSucceededRemoteDataPayload()); + browseDefinition$ = this.browseDefinitionService.findById(id).pipe( + getFirstCompletedRemoteData(), + map((browseDefinitionRD: RemoteData) => browseDefinitionRD.payload), + ); } else { browseDefinition$ = observableOf(route.data.browseDefinition); } @@ -33,19 +40,24 @@ export class BrowseByGuard implements CanActivate { const value = route.queryParams.value; const metadataTranslated = this.translate.instant('browse.metadata.' + id); return browseDefinition$.pipe( - switchMap((browseDefinition) => { - if (hasValue(scope)) { - const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); - return dsoAndMetadata$.pipe( - map((dsoRD) => { - const name = dsoRD.payload.name; - route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); - return true; - }) - ); + switchMap((browseDefinition: BrowseDefinition | undefined) => { + if (hasValue(browseDefinition)) { + if (hasValue(scope)) { + const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData()); + return dsoAndMetadata$.pipe( + map((dsoRD) => { + const name = dsoRD.payload.name; + route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); + return true; + }) + ); + } else { + route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); + return observableOf(true); + } } else { - route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); - return observableOf(true); + void this.router.navigate([PAGE_NOT_FOUND_PATH]); + return observableOf(false); } }) ); From 58a3ec397293b57b030f15ffc4d89bbc7ef3f89d Mon Sep 17 00:00:00 2001 From: enea4science <127771679+enea4science@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:50:26 +0200 Subject: [PATCH 24/41] Update orcid-sync-settings.component.ts --- .../orcid-sync-settings/orcid-sync-settings.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 1aec416d62..0bcbc295ac 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -156,8 +156,7 @@ export class OrcidSyncSettingsComponent implements OnInit { } }), ).subscribe((remoteData: RemoteData) => { - // hasSucceeded is true if the response is success or successStale - if (remoteData.hasSucceeded) { + if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); this.settingsUpdated.emit(); } else { From e85f9f2b255ccbd6169b6394d93efe692917dd95 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 12 Jun 2023 14:40:21 +0200 Subject: [PATCH 25/41] [CST-5729] fix issue with signposting endpoint url replace --- src/app/core/data/signposting-data.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index d051ecf8db..34d3ffdab9 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -25,7 +25,8 @@ export class SignpostingDataService { * @param uuid */ getLinks(uuid: string): Observable { - const baseUrl = this.halService.getRootHref().replace('/api', ''); + const regex = /\/api$/gm; + const baseUrl = this.halService.getRootHref().replace(regex, ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { From 5d6edade22ff116c980465bc322ef4bf56772475 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 12 Jun 2023 15:22:33 +0200 Subject: [PATCH 26/41] [CST-5729] turn to use app config baseurl --- .../data/signposting-data.service.spec.ts | 26 +++++++++---------- src/app/core/data/signposting-data.service.ts | 9 +++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index c76899221e..f34ce6538f 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -1,14 +1,16 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { of } from 'rxjs'; + import { SignpostingDataService } from './signposting-data.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { of } from 'rxjs'; import { SignpostingLink } from './signposting-links.model'; +import { APP_CONFIG } from '../../../config/app-config.interface'; describe('SignpostingDataService', () => { let service: SignpostingDataService; let restServiceSpy: jasmine.SpyObj; - let halServiceSpy: jasmine.SpyObj; + const mocklink = { href: 'http://test.org', rel: 'test', @@ -30,21 +32,25 @@ describe('SignpostingDataService', () => { statusCode: 500 }; + const environmentRest = { + rest: { + baseUrl: 'http://localhost:8080' + } + }; + beforeEach(() => { const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); - const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']); TestBed.configureTestingModule({ providers: [ SignpostingDataService, - { provide: DspaceRestService, useValue: restSpy }, - { provide: HALEndpointService, useValue: halSpy } + { provide: APP_CONFIG, useValue: environmentRest }, + { provide: DspaceRestService, useValue: restSpy } ] }); service = TestBed.inject(SignpostingDataService); restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj; - halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj; }); it('should be created', () => { @@ -55,8 +61,6 @@ describe('SignpostingDataService', () => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; - halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(mockResponse)); let result: SignpostingLink[]; @@ -70,7 +74,6 @@ describe('SignpostingDataService', () => { tick(); expect(result).toEqual(expectedResult); - expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); @@ -78,8 +81,6 @@ describe('SignpostingDataService', () => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; - halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(mockErrResponse)); let result: any; @@ -91,7 +92,6 @@ describe('SignpostingDataService', () => { tick(); expect(result).toEqual([]); - expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); }); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 34d3ffdab9..638b04dfdd 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { catchError, map } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { SignpostingLink } from './signposting-links.model'; +import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; /** * Service responsible for handling requests related to the Signposting endpoint @@ -16,7 +16,7 @@ import { SignpostingLink } from './signposting-links.model'; }) export class SignpostingDataService { - constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { + constructor(@Inject(APP_CONFIG) protected appConfig: AppConfig, private restService: DspaceRestService) { } /** @@ -25,8 +25,7 @@ export class SignpostingDataService { * @param uuid */ getLinks(uuid: string): Observable { - const regex = /\/api$/gm; - const baseUrl = this.halService.getRootHref().replace(regex, ''); + const baseUrl = `${this.appConfig.rest.baseUrl}`; return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { From de0d7bf33ae0fb59c971f88da037f7e8d948af04 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 4 May 2023 15:01:23 -0500 Subject: [PATCH 27/41] Replace lorem ipsum with text donated by DSpaceDirect --- .../end-user-agreement-content.component.html | 132 +++++++++++++----- .../privacy-content.component.html | 128 ++++++++++++----- src/assets/i18n/en.json5 | 6 +- 3 files changed, 190 insertions(+), 76 deletions(-) diff --git a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html index 1ee8712444..3ae0d0efbe 100644 --- a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html +++ b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html @@ -1,37 +1,95 @@ -

{{ 'info.end-user-agreement.head' | translate }}

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas. -

-

- In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus. -

-

- Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean. -

-

- Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas. -

-

- Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi. -

-

- Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat. -

-

- Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. -

-

- Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in. -

-

- Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique. -

-

- Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce. -

-

- Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. -

-

- Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut. -

+

{{ 'info.end-user-agreement.head' | translate }}

+

Last updated May 4, 2023

+ +

Agreement to terms

+

These Terms of Use constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you") and {{ 'repository.title' | translate }} ("Company", "we", "us", or "our"), concerning your access to and use of this website as well as any other media form, media channel, mobile website or mobile application related, linked, or otherwise connected thereto (collectively, the "Site"). You agree that by accessing the Site, you have read, understood, and agreed to be bound by all of these Terms of Use and any future amendments thereof.

+

Supplemental terms and conditions or documents that may be posted on the Site from time to time are hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or modifications to these Terms of Use at any time and for any reason. We will alert you about any changes by updating the "Last updated" date of these Terms of Use, and you waive any right to receive specific notice of each such change. Please ensure that you check the applicable Terms every time you use our Site so that you understand which Terms apply. You will be subject to, and will be deemed to have been made aware of and to have accepted, the changes in any revised Terms of Use by your continued use of the Site after the date such revised Terms of Use are posted.

+

The information provided on the Site is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those persons who choose to access the Site from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable.

+ +

Intellectual property rights

+

Unless otherwise indicated, the Site is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of {{ 'info.end-user-agreement.hosting-country' | translate }}, international copyright laws, and international conventions. The Content and the Marks are provided on the Site "AS IS" for your information and personal use only. Except as expressly provided in these Terms of Use, no part of the Site and no Content[a] or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose whatsoever, without our express prior written permission.

+

Provided that you are eligible to use the Site, you are granted a limited license to access and use the Site and to download or print a copy of any portion of the Content to which you have properly gained access solely for your personal, non-commercial use. We reserve all rights not expressly granted to you in and to the Site, the Content and the Marks.

+ +

User representations

+

By using the Site, you represent and warrant that: (1) all registration information you submit will be true, accurate, current, and complete; (2) you will maintain the accuracy of such information and promptly update such registration information as necessary; (3) you have the legal capacity and you agree to comply with these Terms of Use; (4) you will not use the Site for any illegal or unauthorized purpose; and (5) your use of the Site will not violate any applicable law or regulation.

+

If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to suspend or terminate your account and refuse any and all current or future use of the Site (or any portion thereof).

+ +

User registration

+

You may be required to register with the Site. You agree to keep your password confidential and will be responsible for all use of your account and password. We reserve the right to remove, reclaim, or change a username you select if we determine, in our sole discretion, that such username is inappropriate, obscene, or otherwise objectionable.

+ +

Prohibited activities

+

You may not access or use the Site for any purpose other than that for which we make the Site available. The Site may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us.

+

As a user of the Site, you agree not to:

+
    +
  • Systematically retrieve data or other content from the Site to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us.
  • +
  • Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords.
  • +
  • Circumvent, disable, or otherwise interfere with security-related features of the Site, including features that prevent or restrict the use or copying of any Content or enforce limitations on the use of the Site and/or the Content contained therein.
  • +
  • Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Site.
  • +
  • Use any information obtained from the Site in order to harass, abuse, or harm another person.
  • +
  • Make improper use of our support services or submit false reports of abuse or misconduct.
  • +
  • Use the Site in a manner inconsistent with any applicable laws or regulations.
  • +
  • Engage in unauthorized framing of or linking to the Site.
  • +
  • Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any party's uninterrupted use and enjoyment of the Site or modifies, impairs, disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the Site.
  • +
  • Delete the copyright or other proprietary rights notice from any Content.
  • +
  • Attempt to impersonate another user or person or use the username and password of another user.
  • +
  • Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information collection or transmission mechanism, including without limitation, clear graphics interchange formats ("gifs"), 1x1 pixels, web bugs, cookies, or other similar devices (sometimes referred to as "spyware" or "passive collection mechanisms" or "pcms").
  • +
  • Interfere with, disrupt, or create an undue burden on the Site or the networks or services connected to the Site.
  • +
  • Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion of the Site to you.
  • +
  • Attempt to bypass any measures of the Site designed to prevent or restrict access to the Site, or any portion of the Site.
  • +
  • Make any unauthorized use of the Site, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email or other forms of electronic communication, or creating user accounts by automated means or under false pretenses.
  • +
  • Use the Site as part of any effort to compete with us or otherwise use the Site and/or the Content for any revenue-generating endeavor or commercial enterprise.
  • +
+ +

User generated contributions

+

The Site may provide you with the opportunity to create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials to us or on the Site, including but not limited to text, writings, video, audio, photographs, graphics, comments, suggestions, or personal information or other material (collectively, "Contributions"). Contributions may be viewable by other users of the Site and through third-party websites. As such, any Contributions you transmit may be treated as non-confidential and non-proprietary. When you create or make available any Contributions, you thereby represent and warrant that:

+
    +
  • The creation, distribution, transmission, public display, or performance, and the accessing, downloading, or copying of your Contributions do not and will not infringe the proprietary rights, including but not limited to the copyright, patent, trademark, trade secret, or moral rights of any third party.
  • +
  • You are the creator and owner of or have the necessary licenses, rights, consents, releases, and permissions to use and to authorize us, the Site, and other users of the Site to use your Contributions in any manner contemplated by the Site and these Terms of Use.
  • +
  • You have the written consent, release, and/or permission of each and every identifiable individual person in your Contributions to use the name or likeness of each and every such identifiable individual person to enable inclusion and use of your Contributions in any manner contemplated by the Site and these Terms of Use.
  • +
  • Your Contributions are not false, inaccurate, or misleading.
  • +
  • Your Contributions are not unsolicited or unauthorized advertising, promotional materials, pyramid schemes, chain letters, spam, mass mailings, or other forms of solicitation.
  • +
  • Your Contributions are not obscene, lewd, lascivious, filthy, violent, harassing, libelous, slanderous, or otherwise objectionable (as determined by us).
  • +
  • Your Contributions do not ridicule, mock, disparage, intimidate, or abuse anyone.
  • +
  • Your Contributions are not used to harass or threaten (in the legal sense of those terms) any other person, do not create and are not used to promote violence against a specific person or class of people.
  • +
  • Your Contributions do not violate any applicable law, regulation, or rule.
  • +
  • Your Contributions do not violate the privacy or publicity rights of any third party.
  • +
  • Your Contributions do not violate any applicable law concerning child pornography, or otherwise intended to protect the health or well-being of minors.
  • +
  • Your Contributions do not include any offensive comments that are connected to race, national origin, gender, sexual preference, colour, religion, creed or physical handicap.
  • +
  • Your Contributions do not otherwise violate, or link to material that violates, any provision of these Terms of Use, or any applicable law or regulation.
  • +
+

Any use of the Site in violation of the foregoing violates these Terms of Use and may result in, among other things, termination or suspension of your rights to use the Site.

+ +

Site management

+

We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Use; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Use, including without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or liability, to remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the Site in a manner designed to protect our rights and property and to facilitate the proper functioning of the Site.

+ +

Privacy policy

+

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

+

Please be advised the Site is hosted in {{ 'info.end-user-agreement.hosting-country' | translate }}. If you access the Site from any other region of the world with laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in {{ 'info.end-user-agreement.hosting-country' | translate }}, then through your continued use of the Site, you are transferring your data to {{ 'info.end-user-agreement.hosting-country' | translate }}, and you agree to have your data transferred to and processed in {{ 'info.end-user-agreement.hosting-country' | translate }}.

+ +

Term and termination

+

These Terms of Use shall remain in full force and effect while you use the Site. WITHOUT LIMITING ANY OTHER PROVISION OF THESE TERMS OF USE, WE RESERVE THE RIGHT TO, IN OUR SOLE DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SITE (INCLUDING BLOCKING CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE TERMS OF USE OR OF ANY APPLICABLE LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SITE OR DELETE YOUR ACCOUNT AND ANY CONTENT OR INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION.

+

If we terminate or suspend your account for any reason, you are prohibited from registering and creating a new account under your name, a fake or borrowed name, or the name of any third party, even if you may be acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the right to take appropriate legal action, including without limitation pursuing civil, criminal, and injunctive redress.

+ +

Modifications and interruptions

+

We reserve the right to change, modify, or remove the contents of the Site at any time or for any reason at our sole discretion without notice. However, we have no obligation to update any information on our Site. We also reserve the right to modify or discontinue all or part of the Site without notice at any time. We will not be liable to you or any third party for any modification, change, suspension, or discontinuance of the Site.

+

We cannot guarantee the Site will be available at all times. We may experience hardware, software, or other problems or need to perform maintenance related to the Site, resulting in interruptions, delays, or errors. We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the Site at any time or for any reason without notice to you. You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by your inability to access or use the Site during any downtime or discontinuance of the Site. Nothing in these Terms of Use will be construed to obligate us to maintain and support the Site or to supply any corrections, updates, or releases in connection therewith.

+ +

Corrections

+

There may be information on the Site that contains typographical errors, inaccuracies, or omissions. We reserve the right to correct any errors, inaccuracies, or omissions and to change or update the information on the Site at any time, without prior notice.

+ +

Disclaimer

+

THE SITE IS PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SITE AND OUR SERVICES WILL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN CONNECTION WITH THE SITE AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SITE'S CONTENT OR THE CONTENT OF ANY WEBSITES LINKED TO THE SITE AND WE WILL ASSUME NO LIABILITY OR RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SITE, (3) ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SITE, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE SITE BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE SITE. WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH THE SITE, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE.

+ +

Limitations of liability

+

IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SITE, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

+ +

Indemnification

+

You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys' fees and expenses, made by any third party due to or arising out of: (1) your Contributions; (2) use of the Site; (3) breach of these Terms of Use; (4) any breach of your representations and warranties set forth in these Terms of Use; (5) your violation of the rights of a third party, including but not limited to intellectual property rights; or (6) any overt harmful act toward any other user of the Site with whom you connected via the Site. Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and control of any matter for which you are required to indemnify us, and you agree to cooperate, at your expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim, action, or proceeding which is subject to this indemnification upon becoming aware of it.

+ +

User Data

+

We will maintain certain data that you transmit to the Site for the purpose of managing the performance of the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of data, you are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the Site. You agree that we shall have no liability to you for any loss or corruption of any such data, and you hereby waive any right of action against us arising from any such loss or corruption of such data.

+ +

Miscellaneous

+

These Terms of Use and any policies or operating rules posted by us on the Site or in respect to the Site constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or provision of these Terms of Use shall not operate as a waiver of such right or provision. These Terms of Use operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our reasonable control. If any provision or part of a provision of these Terms of Use is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed severable from these Terms of Use and does not affect the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or agency relationship created between you and us as a result of these Terms of Use or use of the Site. You agree that these Terms of Use will not be construed against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the electronic form of these Terms of Use and the lack of signing by the parties hereto to execute these Terms of Use.

+ +

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file diff --git a/src/app/info/privacy/privacy-content/privacy-content.component.html b/src/app/info/privacy/privacy-content/privacy-content.component.html index a5bbb3fe10..33504b1522 100644 --- a/src/app/info/privacy/privacy-content/privacy-content.component.html +++ b/src/app/info/privacy/privacy-content/privacy-content.component.html @@ -1,37 +1,91 @@ -

{{ 'info.privacy.head' | translate }}

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas. -

-

- In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus. -

-

- Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean. -

-

- Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas. -

-

- Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi. -

-

- Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat. -

-

- Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. -

-

- Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in. -

-

- Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique. -

-

- Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce. -

-

- Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. -

-

- Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut. -

+

{{ 'info.privacy.head' | translate }}

+

Last updated May 4, 2023

+ +

Introduction

+

{{ 'repository.title' | translate }} ("Company" or "We") respects your privacy and is committed to protecting it through our compliance with this policy.

+

This policy describes the types of information we may collect from you or that you may provide when you visit this website (our "Website") and our practices for collecting, using, maintaining, protecting, and disclosing that information.

+

This policy applies to information we collect:

+
    +
  • On this Website.
  • +
  • In email, text, and other electronic messages between you and this Website.
  • +
+

It does not apply to information collected by:

+
    +
  • us offline or through any other means, including on any other website operated by Company or any third party; or
  • +
  • any third party, including through any application or content (including advertising) that may link to or be accessible from or on the Website.
  • +
+

Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, your choice is not to use our Website. By accessing or using this Website, you agree to this privacy policy. This policy may change from time to time. Your continued use of this Website after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates.

+ +

Children under the age of 13

+

Our Website is not intended for children under 13 years of age. No one under age 13 may provide any personal information to or on the Website. We do not knowingly collect personal information from children under 13. If you are under 13, do not use or provide any information on this Website or provide any information about yourself to us, including your name, address, telephone number, email address, or any screen name or username you may use. If we learn we have collected or received personal information from a child under 13 without verification of parental consent, we will delete that information.

+ +

Information we collect about you and how we collect it

+

We collect several types of information from and about users of our Website, including information:

+
    +
  • by which you may be personally identified, such as name, e-mail address, telephone number, or any other identifier by which you may be contacted online or offline ("personal information"); and/or
  • +
  • about your internet connection, the equipment you use to access our Website and usage details.
  • +
+

We collect this information:

+
    +
  • directly from you when you provide it to us.
  • +
  • automatically as you navigate through the site. Information collected automatically may include usage details, IP addresses, and information collected through cookies, web beacons, and other tracking technologies; and/or
  • +
  • from third parties, for example, our business partners.
  • +
+ +

Information you provide to us

+

The information we collect on or through our Website may include:

+
    +
  • Information that you provide by filling in forms on our Website. We may also ask you for information when you report a problem with our Website.
  • +
  • Records and copies of your correspondence (including email addresses), if you contact us.
  • +
  • Your responses to surveys that we might ask you to complete for research purpose.
  • +
+ +

Information we collect through automatic data collection technologies

+

As you navigate through and interact with our Website, we may use automatic data collection technologies to collect certain information about your equipment, browsing actions, and patterns, including:

+
    +
  • Details of your visits to our Website, including traffic data, location data, logs, and other communication data and the resources that you access and use on the Website.
  • +
  • Information about your computer and internet connection, including your IP address, operating system, and browser type.
  • +
+

The information we collect automatically is statistical data and does not include personal information, but we may maintain it or associate it with personal information we collect in other ways or receive from third parties. It helps us to improve our Website and to deliver a better and more personalized service, including by enabling us to:

+
    +
  • Estimate our audience size and usage patterns.
  • +
  • Store information about your preferences, allowing us to customize our Website according to your individual interests.
  • +
  • Speed up your searches.
  • +
  • Recognize you when you return to our Website.
  • +
+

The technologies we use for this automatic data collection may include:

+
    +
  • Cookies (or browser cookies). A cookie is a small file placed on the hard drive of your computer. You may refuse to accept browser cookies by activating the appropriate setting on your browser. However, if you select this setting you may be unable to access certain parts of our Website. Unless you have adjusted your browser setting so that it will refuse cookies, our system will issue cookies when you direct your browser to our Website.
  • +
+ +

How we use your information

+

We use information that we collect about you or that you provide to us, including any personal information:

+
    +
  • To present our Website and its contents to you.
  • +
  • To provide you with information, products, or services that you request from us.
  • +
  • To fulfill any other purpose for which you provide it.
  • +
  • To carry out our obligations and enforce our rights arising from any contracts entered into between you and us, including for billing and collection.
  • +
  • To notify you about changes to our Website or any products or services we offer or provide through it.
  • +
  • In any other way we may describe when you provide the information.
  • +
  • For any other purpose with your consent.
  • +
+ +

Disclosure of your information

+

We may disclose aggregated information about our users, and information that does not identify any individual, without restriction.

+

We may disclose personal information that we collect or you provide as described in this privacy policy:

+
    +
  • To contractors, service providers, and other third parties we use to support our business and who are bound by contractual obligations to keep personal information confidential and use it only for the purposes for which we disclose it to them.
  • +
  • To a buyer or other successor in the event of a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Company's assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which personal information held by Company about our Website users is among the assets transferred.
  • +
  • To fulfill the purpose for which you provide it.
  • +
  • For any other purpose disclosed by us when you provide the information.
  • +
  • With your consent.
  • +
+

We may also disclose your personal information:

+
    +
  • To comply with any court order, law, or legal process, including to respond to any government or regulatory request.
  • +
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • +
  • If we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others.
  • +
+ +

Changes to our privacy policy

+

It is our policy to post any changes we make to our privacy policy on this page. The date the privacy policy was last revised is identified at the top of the page. You are responsible for periodically visiting our Website and this privacy policy to check for any changes.

\ No newline at end of file diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2df03302d7..b8dbd20459 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1916,6 +1916,8 @@ "info.end-user-agreement.title": "End User Agreement", + "info.end-user-agreement.hosting-country": "the United States", + "info.privacy.breadcrumbs": "Privacy Statement", "info.privacy.head": "Privacy Statement", @@ -3649,9 +3651,9 @@ "repository.image.logo": "Repository logo", - "repository.title.prefix": "DSpace Angular :: ", + "repository.title": "DSpace Repository", - "repository.title.prefixDSpace": "DSpace Angular ::", + "repository.title.prefix": "DSpace Repository :: ", "resource-policies.add.button": "Add", From c3854355fd4150c93349d998e0fad3edbbc047d1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 4 May 2023 16:02:04 -0500 Subject: [PATCH 28/41] Fix e2e test to check for new title prefix --- cypress/e2e/homepage.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/homepage.cy.ts b/cypress/e2e/homepage.cy.ts index 8fdf61dbf7..a387c31a2a 100644 --- a/cypress/e2e/homepage.cy.ts +++ b/cypress/e2e/homepage.cy.ts @@ -6,8 +6,8 @@ describe('Homepage', () => { cy.visit('/'); }); - it('should display translated title "DSpace Angular :: Home"', () => { - cy.title().should('eq', 'DSpace Angular :: Home'); + it('should display translated title "DSpace Repository :: Home"', () => { + cy.title().should('eq', 'DSpace Repository :: Home'); }); it('should contain a news section', () => { From 685fbf630a7e6c5dbeadc99df52ae9c7fac63e9b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jun 2023 17:04:02 -0500 Subject: [PATCH 29/41] Address feedback: Make links open in new window/tab. --- .../end-user-agreement-content.component.html | 4 ++-- .../privacy/privacy-content/privacy-content.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html index 3ae0d0efbe..d5e6de85d4 100644 --- a/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html +++ b/src/app/info/end-user-agreement/end-user-agreement-content/end-user-agreement-content.component.html @@ -63,7 +63,7 @@

We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Use; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Use, including without limitation, reporting such user to law enforcement authorities; (3) in our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion and without limitation, notice, or liability, to remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the Site in a manner designed to protect our rights and property and to facilitate the proper functioning of the Site.

Privacy policy

-

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

+

We care about data privacy and security. Please review our Privacy Policy. By using the Site, you agree to be bound by our Privacy Policy, which is incorporated into these Terms of Use.

Please be advised the Site is hosted in {{ 'info.end-user-agreement.hosting-country' | translate }}. If you access the Site from any other region of the world with laws or other requirements governing personal data collection, use, or disclosure that differ from applicable laws in {{ 'info.end-user-agreement.hosting-country' | translate }}, then through your continued use of the Site, you are transferring your data to {{ 'info.end-user-agreement.hosting-country' | translate }}, and you agree to have your data transferred to and processed in {{ 'info.end-user-agreement.hosting-country' | translate }}.

Term and termination

@@ -92,4 +92,4 @@

Miscellaneous

These Terms of Use and any policies or operating rules posted by us on the Site or in respect to the Site constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any right or provision of these Terms of Use shall not operate as a waiver of such right or provision. These Terms of Use operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay, or failure to act caused by any cause beyond our reasonable control. If any provision or part of a provision of these Terms of Use is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed severable from these Terms of Use and does not affect the validity and enforceability of any remaining provisions. There is no joint venture, partnership, employment or agency relationship created between you and us as a result of these Terms of Use or use of the Site. You agree that these Terms of Use will not be construed against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the electronic form of these Terms of Use and the lack of signing by the parties hereto to execute these Terms of Use.

-

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file +

[a] The DSpace software used to run this site is open source. Options for reuse and reproduction of the DSpace software is governed by its open source license: https://github.com/DSpace/DSpace/blob/main/LICENSE

\ No newline at end of file diff --git a/src/app/info/privacy/privacy-content/privacy-content.component.html b/src/app/info/privacy/privacy-content/privacy-content.component.html index 33504b1522..f29a786e8b 100644 --- a/src/app/info/privacy/privacy-content/privacy-content.component.html +++ b/src/app/info/privacy/privacy-content/privacy-content.component.html @@ -83,7 +83,7 @@

We may also disclose your personal information:

  • To comply with any court order, law, or legal process, including to respond to any government or regulatory request.
  • -
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • +
  • To enforce or apply our End User Agreement and other agreements, including for billing and collection purposes.
  • If we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others.
From 486aefebc7b3b3ac729ec4de35ebad017d179dc0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 13 Jun 2023 19:00:33 +0200 Subject: [PATCH 30/41] [CST-5729] Add possibility to store response's headers into the SSR cache --- server.ts | 18 +++++++++++++----- src/config/cache-config.interface.ts | 2 ++ src/config/default-app-config.ts | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/server.ts b/server.ts index d64b80b4ab..791baf7236 100644 --- a/server.ts +++ b/server.ts @@ -53,7 +53,7 @@ import { buildAppConfig } from './src/config/config.server'; import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; -import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; +import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; /* @@ -374,9 +374,15 @@ function cacheCheck(req, res, next) { } // If cached copy exists, return it to the user. - if (cachedCopy) { + if (cachedCopy && cachedCopy.page) { + if (cachedCopy.headers && Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { + environment.cache.serverSide.headers.forEach((header) => { + if (environment.cache.serverSide.debug) { console.log(`Restore cached ${header} header`); } + res.setHeader(header, cachedCopy.headers[header.toLowerCase()]); + }); + } res.locals.ssr = true; // mark response as SSR-generated (enables text compression) - res.send(cachedCopy); + res.send(cachedCopy.page); // Tell Express to skip all other handlers for this path // This ensures we don't try to re-render the page since we've already returned the cached copy @@ -452,16 +458,18 @@ function saveToCache(req, page: any) { // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) if (key.startsWith('/reload')) { return; } + // Retrieve response headers + const headers = req.res.getHeaders(); // If bot cache is enabled, save it to that cache if it doesn't exist or is expired // (NOTE: has() will return false if page is expired in cache) if (botCacheEnabled() && !botCache.has(key)) { - botCache.set(key, page); + botCache.set(key, { page, headers }); if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); } } // If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired if (anonymousCacheEnabled() && !anonymousCache.has(key)) { - anonymousCache.set(key, page); + anonymousCache.set(key, { page, headers }); if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } } } diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index 9560fe46a5..14af509bbf 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -13,6 +13,8 @@ export interface CacheConfig extends Config { serverSide: { // Debug server-side caching. Set to true to see cache hits/misses/refreshes in console logs. debug: boolean, + // List of headers to restore from the cache hit + headers: string[], // Cache specific to known bots. Allows you to serve cached contents to bots only. botCache: { // Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache. diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 80420407c7..a6e9e092e4 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -78,6 +78,8 @@ export class DefaultAppConfig implements AppConfig { // In-memory cache of server-side rendered content serverSide: { debug: false, + // Link header is used for signposting functionality + headers: ['Link'], // Cache specific to known bots. Allows you to serve cached contents to bots only. // Defaults to caching 1,000 pages. Each page expires after 1 day botCache: { From ac9be25faf9135424f4c201a24472c1d24b5f7f7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 13 Jun 2023 19:30:40 +0200 Subject: [PATCH 31/41] [CST-5729] check if header exists --- server.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server.ts b/server.ts index 791baf7236..f73bd1b774 100644 --- a/server.ts +++ b/server.ts @@ -377,8 +377,12 @@ function cacheCheck(req, res, next) { if (cachedCopy && cachedCopy.page) { if (cachedCopy.headers && Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { environment.cache.serverSide.headers.forEach((header) => { - if (environment.cache.serverSide.debug) { console.log(`Restore cached ${header} header`); } - res.setHeader(header, cachedCopy.headers[header.toLowerCase()]); + if (cachedCopy.headers[header.toLowerCase()]) { + if (environment.cache.serverSide.debug) { + console.log(`Restore cached ${header} header`); + } + res.setHeader(header, cachedCopy.headers[header.toLowerCase()]); + } }); } res.locals.ssr = true; // mark response as SSR-generated (enables text compression) From 8bcceff085825c7aad27a099c7b426b67ba0e4fe Mon Sep 17 00:00:00 2001 From: damian Date: Tue, 13 Jun 2023 19:57:30 +0200 Subject: [PATCH 32/41] Empty communities and collections items count are now shown. Show communities items count at the home page. --- .../community-list/community-list.component.html | 2 +- .../collection-list-element.component.html | 2 +- .../collection-list-element.component.spec.ts | 8 ++++---- .../community-list-element.component.html | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index 7f8dcf0c45..55b043f8e8 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -37,7 +37,7 @@ {{node.name}} - [{{node.payload.archivedItemsCount}}] + [{{node.payload.archivedItemsCount}}] diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index 68a84a88e5..b007db510e 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -4,7 +4,7 @@ {{object.name}} -[{{object.archivedItemsCount}}] +[{{object.archivedItemsCount}}]
{{object.shortDescription}}
diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts index b911bdd2eb..2324cc3c1f 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts @@ -18,7 +18,7 @@ const mockCollectionWithArchivedItems: Collection = Object.assign(new Collection }, archivedItemsCount: 1 }); -const mockCollectionWithoutArchivedItems: Collection = Object.assign(new Collection(), { +const mockCollectionWithArchivedItemsDisabledAtBackend: Collection = Object.assign(new Collection(), { metadata: { 'dc.title': [ { @@ -26,7 +26,7 @@ const mockCollectionWithoutArchivedItems: Collection = Object.assign(new Collect value: 'Test title' } ] - }, archivedItemsCount: 0 + }, archivedItemsCount: -1 }); @@ -108,9 +108,9 @@ describe('CollectionListElementComponent', () => { }); }); - describe('When the collection has no archived items', () => { + describe('When the collection archived items are disabled at backend', () => { beforeEach(() => { - collectionListElementComponent.object = mockCollectionWithoutArchivedItems; + collectionListElementComponent.object = mockCollectionWithArchivedItemsDisabledAtBackend; fixture.detectChanges(); }); diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.html b/src/app/shared/object-list/community-list-element/community-list-element.component.html index af01999ca7..26ff040dd6 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.html +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.html @@ -4,6 +4,7 @@ {{object.name}} +[{{object.archivedItemsCount}}]
{{object.shortDescription}}
From adebc30d3bf61a1266124c61003b29fa62a37790 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 14 Jun 2023 10:49:49 +0300 Subject: [PATCH 33/41] src/assets/i18n/en.json5: lint Normalize whitespace in i18n/en.json5. This file should be clean, as it is the "upstream" of all other language assets and produces a lot of whitespace changes every time strings are merged. --- src/assets/i18n/en.json5 | 362 +++++++-------------------------------- 1 file changed, 65 insertions(+), 297 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b8dbd20459..480bf8834e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6,8 +6,6 @@ "401.unauthorized": "unauthorized", - - "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", "403.link.home-page": "Take me to the home page", @@ -20,7 +18,6 @@ "500.link.home-page": "Take me to the home page", - "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", "404.link.home-page": "Take me to the home page", @@ -130,6 +127,7 @@ "admin.registries.bitstream-formats.table.mimetype": "MIME Type", "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.id": "ID", "admin.registries.bitstream-formats.table.return": "Back", @@ -144,8 +142,6 @@ "admin.registries.bitstream-formats.title": "Bitstream Format Registry", - - "admin.registries.metadata.breadcrumbs": "Metadata registry", "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", @@ -172,8 +168,6 @@ "admin.registries.metadata.title": "Metadata Registry", - - "admin.registries.schema.breadcrumbs": "Metadata schema", "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", @@ -185,6 +179,7 @@ "admin.registries.schema.fields.table.delete": "Delete selected", "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.id": "ID", "admin.registries.schema.fields.table.scopenote": "Scope Note", @@ -225,8 +220,6 @@ "admin.registries.schema.title": "Metadata Schema Registry", - - "admin.access-control.epeople.actions.delete": "Delete EPerson", "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", @@ -321,8 +314,6 @@ "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", - - "admin.access-control.groups.title": "Groups", "admin.access-control.groups.breadcrumbs": "Groups", @@ -369,8 +360,6 @@ "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"", - - "admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.", "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", @@ -531,9 +520,6 @@ "administrativeView.search.results.head": "Administrative Search", - - - "admin.workflow.breadcrumbs": "Administer Workflow", "admin.workflow.title": "Administer Workflow", @@ -550,8 +536,6 @@ "admin.workflow.item.supervision": "Supervision", - - "admin.metadata-import.breadcrumbs": "Import Metadata", "admin.batch-import.breadcrumbs": "Import Batch", @@ -612,12 +596,10 @@ "advanced-workflow-action.rating.description-requiredDescription": "Please select a rating below and also add a review", - "advanced-workflow-action.select-reviewer.description-single": "Please select a single reviewer below before submitting", "advanced-workflow-action.select-reviewer.description-multiple": "Please select one or more reviewers below before submitting", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.head": "EPeople", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.head": "Add EPeople", @@ -674,12 +656,9 @@ "auth.messages.token-refresh-failed": "Refreshing your session token failed. Please log in again.", + "bitstream.download.page": "Now downloading {{bitstream}}...", - - "bitstream.download.page": "Now downloading {{bitstream}}..." , - - "bitstream.download.page.back": "Back" , - + "bitstream.download.page.back": "Back", "bitstream.edit.authorizations.link": "Edit bitstream's Policies", @@ -731,7 +710,6 @@ "bitstream.edit.form.iiifHeight.hint": "The canvas height should usually match the image height.", - "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", "bitstream.edit.notifications.saved.title": "Bitstream saved", @@ -747,6 +725,7 @@ "bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ", "bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ", + "bitstream-request-a-copy.intro.bitstream.all": "Requesting all files. ", "bitstream-request-a-copy.name.label": "Name *", @@ -775,8 +754,6 @@ "bitstream-request-a-copy.submit.error": "Something went wrong with submitting the item request.", - - "browse.back.all-results": "All browse results", "browse.comcol.by.author": "By Author", @@ -871,20 +848,16 @@ "browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}", - "search.browse.item-back": "Back to Results", - "chips.remove": "Remove chip", - "claimed-approved-search-result-list-element.title": "Approved", "claimed-declined-search-result-list-element.title": "Rejected, sent back to submitter", "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", - "collection.create.head": "Create a Collection", "collection.create.notifications.success": "Successfully created the Collection", @@ -907,16 +880,12 @@ "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", - - "collection.edit.delete": "Delete this collection", "collection.edit.head": "Edit Collection", "collection.edit.breadcrumbs": "Edit Collection", - - "collection.edit.tabs.mapper.head": "Item Mapper", "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", @@ -957,7 +926,6 @@ "collection.edit.item-mapper.tabs.map": "Map new items", - "collection.edit.logo.delete.title": "Delete logo", "collection.edit.logo.delete-undo.title": "Undo delete", @@ -976,14 +944,10 @@ "collection.edit.logo.upload": "Drop a Collection Logo to upload", - - "collection.edit.notifications.success": "Successfully edited the Collection", "collection.edit.return": "Back", - - "collection.edit.tabs.curate.head": "Curate", "collection.edit.tabs.curate.title": "Collection Edit - Curate", @@ -1042,8 +1006,6 @@ "collection.edit.tabs.source.title": "Collection Edit - Content Source", - - "collection.edit.template.add-button": "Add", "collection.edit.template.breadcrumbs": "Item template", @@ -1068,8 +1030,6 @@ "collection.edit.template.title": "Edit Template Item", - - "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -1088,12 +1048,8 @@ "collection.form.entityType": "Entity Type", - - "collection.listelement.badge": "Collection", - - "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.empty": "No items to show", @@ -1106,46 +1062,62 @@ "collection.page.news": "News", - - "collection.select.confirm": "Confirm selected", "collection.select.empty": "No collections to show", "collection.select.table.title": "Title", - "collection.source.controls.head": "Harvest Controls", - "collection.source.controls.test.submit.error": "Something went wrong with initiating the testing of the settings", - "collection.source.controls.test.failed": "The script to test the settings has failed", - "collection.source.controls.test.completed": "The script to test the settings has successfully finished", - "collection.source.controls.test.submit": "Test configuration", - "collection.source.controls.test.running": "Testing configuration...", - "collection.source.controls.import.submit.success": "The import has been successfully initiated", - "collection.source.controls.import.submit.error": "Something went wrong with initiating the import", - "collection.source.controls.import.submit": "Import now", - "collection.source.controls.import.running": "Importing...", - "collection.source.controls.import.failed": "An error occurred during the import", - "collection.source.controls.import.completed": "The import completed", - "collection.source.controls.reset.submit.success": "The reset and reimport has been successfully initiated", - "collection.source.controls.reset.submit.error": "Something went wrong with initiating the reset and reimport", - "collection.source.controls.reset.failed": "An error occurred during the reset and reimport", - "collection.source.controls.reset.completed": "The reset and reimport completed", - "collection.source.controls.reset.submit": "Reset and reimport", - "collection.source.controls.reset.running": "Resetting and reimporting...", - "collection.source.controls.harvest.status": "Harvest status:", - "collection.source.controls.harvest.start": "Harvest start time:", - "collection.source.controls.harvest.last": "Last time harvested:", - "collection.source.controls.harvest.message": "Harvest info:", - "collection.source.controls.harvest.no-information": "N/A", + "collection.source.controls.test.submit.error": "Something went wrong with initiating the testing of the settings", + + "collection.source.controls.test.failed": "The script to test the settings has failed", + + "collection.source.controls.test.completed": "The script to test the settings has successfully finished", + + "collection.source.controls.test.submit": "Test configuration", + + "collection.source.controls.test.running": "Testing configuration...", + + "collection.source.controls.import.submit.success": "The import has been successfully initiated", + + "collection.source.controls.import.submit.error": "Something went wrong with initiating the import", + + "collection.source.controls.import.submit": "Import now", + + "collection.source.controls.import.running": "Importing...", + + "collection.source.controls.import.failed": "An error occurred during the import", + + "collection.source.controls.import.completed": "The import completed", + + "collection.source.controls.reset.submit.success": "The reset and reimport has been successfully initiated", + + "collection.source.controls.reset.submit.error": "Something went wrong with initiating the reset and reimport", + + "collection.source.controls.reset.failed": "An error occurred during the reset and reimport", + + "collection.source.controls.reset.completed": "The reset and reimport completed", + + "collection.source.controls.reset.submit": "Reset and reimport", + + "collection.source.controls.reset.running": "Resetting and reimporting...", + + "collection.source.controls.harvest.status": "Harvest status:", + + "collection.source.controls.harvest.start": "Harvest start time:", + + "collection.source.controls.harvest.last": "Last time harvested:", + + "collection.source.controls.harvest.message": "Harvest info:", + + "collection.source.controls.harvest.no-information": "N/A", "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", "collection.source.update.notifications.error.title": "Server Error", - - "communityList.breadcrumbs": "Community List", "communityList.tabTitle": "Community List", @@ -1154,8 +1126,6 @@ "communityList.showMore": "Show More", - - "community.create.head": "Create a Community", "community.create.notifications.success": "Successfully created the Community", @@ -1184,7 +1154,6 @@ "community.edit.breadcrumbs": "Edit Community", - "community.edit.logo.delete.title": "Delete logo", "community.edit.logo.delete-undo.title": "Undo delete", @@ -1203,8 +1172,6 @@ "community.edit.logo.upload": "Drop a Community Logo to upload", - - "community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.unauthorized": "You do not have privileges to make this change", @@ -1213,8 +1180,6 @@ "community.edit.return": "Back", - - "community.edit.tabs.curate.head": "Curate", "community.edit.tabs.curate.title": "Community Edit - Curate", @@ -1231,12 +1196,8 @@ "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - - "community.listelement.badge": "Community", - - "comcol-role.edit.no-group": "None", "comcol-role.edit.create": "Create", @@ -1249,57 +1210,46 @@ "comcol-role.edit.delete.error.title": "Failed to delete the '{{ role }}' role's group", - "comcol-role.edit.community-admin.name": "Administrators", "comcol-role.edit.collection-admin.name": "Administrators", - "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", - "comcol-role.edit.submitters.name": "Submitters", "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", - "comcol-role.edit.item_read.name": "Default item read access", "comcol-role.edit.item_read.description": "E-People and Groups that can read new items submitted to this collection. Changes to this role are not retroactive. Existing items in the system will still be viewable by those who had read access at the time of their addition.", "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", - "comcol-role.edit.bitstream_read.name": "Default bitstream read access", "comcol-role.edit.bitstream_read.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", - "comcol-role.edit.editor.name": "Editors", "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", - "comcol-role.edit.finaleditor.name": "Final editors", "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", - "comcol-role.edit.reviewer.name": "Reviewers", "comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.", - "comcol-role.edit.scorereviewers.name": "Score Reviewers", "comcol-role.edit.scorereviewers.description": "Reviewers are able to give a score to incoming submissions, this will define whether the submission will be rejected or not.", - - "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -1326,8 +1276,6 @@ "community.sub-community-list.head": "Communities of this Community", - - "cookies.consent.accept-all": "Accept all", "cookies.consent.accept-selected": "Accept selected", @@ -1380,30 +1328,22 @@ "cookies.consent.app.description.authentication": "Required for signing you in", - "cookies.consent.app.title.preferences": "Preferences", "cookies.consent.app.description.preferences": "Required for saving your preferences", - - "cookies.consent.app.title.acknowledgement": "Acknowledgement", "cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents", - - "cookies.consent.app.title.google-analytics": "Google Analytics", "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", - - "cookies.consent.app.title.google-recaptcha": "Google reCaptcha", "cookies.consent.app.description.google-recaptcha": "We use google reCAPTCHA service during registration and password recovery", - "cookies.consent.purpose.functional": "Functional", "cookies.consent.purpose.statistical": "Statistical", @@ -1428,8 +1368,6 @@ "curation-task.task.register-doi.label": "Register DOI", - - "curation.form.task-select.label": "Task:", "curation.form.submit": "Start", @@ -1448,8 +1386,6 @@ "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - - "deny-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I regret to inform you that it's not possible to send you a copy of the file(s) you have requested, concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "deny-request-copy.email.subject": "Request copy of document", @@ -1462,14 +1398,10 @@ "deny-request-copy.success": "Successfully denied item request", - - "dso.name.untitled": "Untitled", "dso.name.unnamed": "Unnamed", - - "dso-selector.create.collection.head": "New collection", "dso-selector.create.collection.sub-level": "Create a new collection in", @@ -1656,11 +1588,8 @@ "feed.description": "Syndication feed", - "file-section.error.header": "Error obtaining files for this item", - - "footer.copyright": "copyright © 2002-{{ year }}", "footer.link.dspace": "DSpace software", @@ -1675,8 +1604,6 @@ "footer.link.feedback": "Send Feedback", - - "forgot-email.form.header": "Forgot Password", "forgot-email.form.info": "Enter the email address associated with the account.", @@ -1699,8 +1626,6 @@ "forgot-email.form.error.content": "An error occured when attempting to reset the password for the account associated with the following email address: {{ email }}", - - "forgot-password.title": "Forgot Password", "forgot-password.form.head": "Forgot Password", @@ -1729,7 +1654,6 @@ "forgot-password.form.submit": "Submit password", - "form.add": "Add more", "form.add-help": "Click here to add the current entry and to add another one", @@ -1798,8 +1722,6 @@ "form.repeatable.sort.tip": "Drop the item in the new position", - - "grant-deny-request-copy.deny": "Don't send copy", "grant-deny-request-copy.email.back": "Back", @@ -1830,8 +1752,6 @@ "grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.", - - "grant-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "grant-request-copy.email.subject": "Request copy of document", @@ -1844,7 +1764,6 @@ "grant-request-copy.success": "Successfully granted item request", - "health.breadcrumbs": "Health", "health-page.heading": "Health", @@ -1885,7 +1804,6 @@ "health-page.section.no-issues": "No issues detected", - "home.description": "", "home.breadcrumbs": "Home", @@ -1898,8 +1816,6 @@ "home.top-level-communities.help": "Select a community to browse its collections.", - - "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", @@ -1950,26 +1866,18 @@ "info.feedback.page_help": "Tha page related to your feedback", - - "item.alerts.private": "This item is non-discoverable", "item.alerts.withdrawn": "This item has been withdrawn", - - "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", "item.edit.authorizations.title": "Edit item's Policies", - - "item.badge.private": "Non-discoverable", "item.badge.withdrawn": "Withdrawn", - - "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle.placeholder": "Select a bundle or input new bundle name", @@ -1992,8 +1900,6 @@ "item.bitstreams.upload.title": "Upload bitstream", - - "item.edit.bitstreams.bundle.edit.buttons.upload": "Upload", "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", @@ -2054,8 +1960,6 @@ "item.edit.bitstreams.upload-button": "Upload", - - "item.edit.delete.cancel": "Cancel", "item.edit.delete.confirm": "Delete", @@ -2074,7 +1978,6 @@ "item.edit.tabs.disabled.tooltip": "You're not authorized to access this tab", - "item.edit.tabs.mapper.head": "Collection Mapper", "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", @@ -2157,8 +2060,6 @@ "item.edit.item-mapper.tabs.map": "Map new collections", - - "item.edit.metadata.add-button": "Add", "item.edit.metadata.discard-button": "Discard", @@ -2215,16 +2116,12 @@ "item.edit.metadata.save-button": "Save", - - "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.value": "Value", - - "item.edit.move.cancel": "Back", "item.edit.move.save-button": "Save", @@ -2251,8 +2148,6 @@ "item.edit.move.title": "Move item", - - "item.edit.private.cancel": "Cancel", "item.edit.private.confirm": "Make it non-discoverable", @@ -2265,8 +2160,6 @@ "item.edit.private.success": "The item is now non-discoverable", - - "item.edit.public.cancel": "Cancel", "item.edit.public.confirm": "Make it discoverable", @@ -2279,8 +2172,6 @@ "item.edit.public.success": "The item is now discoverable", - - "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.confirm": "Reinstate", @@ -2293,8 +2184,6 @@ "item.edit.reinstate.success": "The item was reinstated successfully", - - "item.edit.relationships.discard-button": "Discard", "item.edit.relationships.edit.buttons.add": "Add", @@ -2325,10 +2214,8 @@ "item.edit.relationships.no-entity-type": "Add 'dspace.entity.type' metadata to enable relationships for this item", - "item.edit.return": "Back", - "item.edit.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", @@ -2336,6 +2223,7 @@ "item.edit.tabs.curate.head": "Curate", "item.edit.tabs.curate.title": "Item Edit - Curate", + "item.edit.curate.title": "Curate Item: {{item}}", "item.edit.tabs.metadata.head": "Metadata", @@ -2404,8 +2292,6 @@ "item.edit.tabs.view.title": "Item Edit - View", - - "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.confirm": "Withdraw", @@ -2420,7 +2306,6 @@ "item.orcid.return": "Back", - "item.listelement.badge": "Item", "item.page.description": "Description", @@ -2459,8 +2344,6 @@ "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - - "item.page.abstract": "Abstract", "item.page.author": "Authors", @@ -2587,8 +2470,6 @@ "item.preview.oaire.fundingStream": "Funding Stream:", - - "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", @@ -2599,7 +2480,6 @@ "item.select.table.title": "Title", - "item.version.history.empty": "There are no other versions for this item yet.", "item.version.history.head": "Version History", @@ -2640,10 +2520,8 @@ "item.version.history.table.action.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", - "item.version.create.modal.header": "New version", "item.version.create.modal.text": "Create a new version for this item", @@ -2672,7 +2550,6 @@ "item.version.create.notification.inProgress": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.delete.modal.header": "Delete version", "item.version.delete.modal.text": "Do you want to delete version {{version}}?", @@ -2689,13 +2566,10 @@ "item.version.delete.notification.failure": "Version number {{version}} has not been deleted", - "item.version.edit.notification.success": "The summary of version number {{version}} has been changed", "item.version.edit.notification.failure": "The summary of version number {{version}} has not been changed", - - "itemtemplate.edit.metadata.add-button": "Add", "itemtemplate.edit.metadata.discard-button": "Discard", @@ -2750,8 +2624,6 @@ "itemtemplate.edit.metadata.save-button": "Save", - - "journal.listelement.badge": "Journal", "journal.page.description": "Description", @@ -2772,8 +2644,6 @@ "journal.search.title": "Journal Search", - - "journalissue.listelement.badge": "Journal Issue", "journalissue.page.description": "Description", @@ -2792,8 +2662,6 @@ "journalissue.page.titleprefix": "Journal Issue: ", - - "journalvolume.listelement.badge": "Journal Volume", "journalvolume.page.description": "Description", @@ -2806,7 +2674,6 @@ "journalvolume.page.volume": "Volume", - "iiifsearchable.listelement.badge": "Document Media", "iiifsearchable.page.titleprefix": "Document: ", @@ -2829,7 +2696,6 @@ "iiif.page.description": "Description: ", - "loading.bitstream": "Loading bitstream...", "loading.bitstreams": "Loading bitstreams...", @@ -2866,8 +2732,6 @@ "loading.top-level-communities": "Loading top-level communities...", - - "login.form.email": "Email address", "login.form.forgot-password": "Have you forgotten your password?", @@ -2892,24 +2756,18 @@ "login.breadcrumbs": "Login", - - "logout.form.header": "Log out from DSpace", "logout.form.submit": "Log out", "logout.title": "Logout", - - "menu.header.admin": "Management", "menu.header.image.logo": "Repository logo", "menu.header.admin.description": "Management menu", - - "menu.section.access_control": "Access Control", "menu.section.access_control_authorizations": "Authorizations", @@ -2918,12 +2776,8 @@ "menu.section.access_control_people": "People", - - "menu.section.admin_search": "Admin Search", - - "menu.section.browse_community": "This Community", "menu.section.browse_community_by_author": "By Author", @@ -2946,14 +2800,10 @@ "menu.section.browse_global_communities_and_collections": "Communities & Collections", - - "menu.section.control_panel": "Control Panel", "menu.section.curation_task": "Curation Task", - - "menu.section.edit": "Edit", "menu.section.edit_collection": "Collection", @@ -2962,8 +2812,6 @@ "menu.section.edit_item": "Item", - - "menu.section.export": "Export", "menu.section.export_collection": "Collection", @@ -2976,7 +2824,6 @@ "menu.section.export_batch": "Batch Export (ZIP)", - "menu.section.icon.access_control": "Access Control menu section", "menu.section.icon.admin_search": "Admin search menu section", @@ -3009,16 +2856,12 @@ "menu.section.icon.unpin": "Unpin sidebar", - - "menu.section.import": "Import", "menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_metadata": "Metadata", - - "menu.section.new": "New", "menu.section.new_collection": "Collection", @@ -3031,34 +2874,24 @@ "menu.section.new_process": "Process", - - "menu.section.pin": "Pin sidebar", "menu.section.unpin": "Unpin sidebar", - - "menu.section.processes": "Processes", "menu.section.health": "Health", - - "menu.section.registries": "Registries", "menu.section.registries_format": "Format", "menu.section.registries_metadata": "Metadata", - - "menu.section.statistics": "Statistics", "menu.section.statistics_task": "Statistics Task", - - "menu.section.toggle.access_control": "Toggle Access Control section", "menu.section.toggle.control_panel": "Toggle Control Panel section", @@ -3079,14 +2912,13 @@ "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - "menu.section.workflow": "Administer Workflow", - "metadata-export-search.tooltip": "Export search results as CSV", - "metadata-export-search.submit.success": "The export was started successfully", - "metadata-export-search.submit.error": "Starting the export has failed", + "metadata-export-search.submit.success": "The export was started successfully", + + "metadata-export-search.submit.error": "Starting the export has failed", "mydspace.breadcrumbs": "MyDSpace", @@ -3172,8 +3004,6 @@ "mydspace.view-btn": "View", - - "nav.browse.header": "All of DSpace", "nav.community-browse.header": "By Community", @@ -3198,7 +3028,6 @@ "nav.search.button": "Submit search", - "nav.statistics.header": "Statistics", "nav.stop-impersonating": "Stop impersonating EPerson", @@ -3211,7 +3040,6 @@ "none.listelement.badge": "Item", - "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.no-title": "Untitled", @@ -3230,8 +3058,6 @@ "orgunit.page.titleprefix": "Organizational Unit: ", - - "pagination.options.description": "Pagination options", "pagination.results-per-page": "Results Per Page", @@ -3242,8 +3068,6 @@ "pagination.sort-direction": "Sort Options", - - "person.listelement.badge": "Person", "person.listelement.no-title": "No name found", @@ -3276,8 +3100,6 @@ "person.search.title": "Person Search", - - "process.new.select-parameters": "Parameters", "process.new.cancel": "Cancel", @@ -3318,8 +3140,6 @@ "process.new.breadcrumbs": "Create a new process", - - "process.detail.arguments": "Arguments", "process.detail.arguments.empty": "This process doesn't contain any arguments", @@ -3366,8 +3186,6 @@ "process.detail.delete.error": "Something went wrong when deleting the process", - - "process.overview.table.finish": "Finish time (UTC)", "process.overview.table.id": "Process ID", @@ -3404,8 +3222,6 @@ "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", - - "profile.breadcrumbs": "Update Profile", "profile.card.identify": "Identify", @@ -3492,8 +3308,6 @@ "project-relationships.search.results.head": "Project Search Results", - - "publication.listelement.badge": "Publication", "publication.page.description": "Description", @@ -3516,14 +3330,12 @@ "publication.search.title": "Publication Search", - "media-viewer.next": "Next", "media-viewer.previous": "Previous", "media-viewer.playlist": "Playlist", - "register-email.title": "New user registration", "register-page.create-profile.header": "Create Profile", @@ -3566,7 +3378,6 @@ "register-page.create-profile.submit.success.head": "Registration completed", - "register-page.registration.header": "New user registration", "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", @@ -3594,8 +3405,8 @@ "register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha", "register-page.registration.google-recaptcha.must-accept-cookies": "In order to register you must accept the Registration and Password recovery (Google reCaptcha) cookies.", - "register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", + "register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", "register-page.registration.google-recaptcha.open-cookie-settings": "Open cookie settings", @@ -3604,6 +3415,7 @@ "register-page.registration.google-recaptcha.notification.message.error": "An error occurred during reCaptcha verification", "register-page.registration.google-recaptcha.notification.message.expired": "Verification expired. Please verify again.", + "register-page.registration.info.maildomain": "Accounts can be registered for mail addresses of the domains", "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", @@ -3648,14 +3460,12 @@ "relationships.isFundingAgencyOf.OrgUnit": "Funder", - "repository.image.logo": "Repository logo", "repository.title": "DSpace Repository", "repository.title.prefix": "DSpace Repository :: ", - "resource-policies.add.button": "Add", "resource-policies.add.for.": "Add a new policy", @@ -3770,8 +3580,6 @@ "resource-policies.table.headers.title.for.collection": "Policies for Collection", - - "search.description": "", "search.switch-configuration.title": "Show", @@ -3782,7 +3590,6 @@ "search.search-form.placeholder": "Search the repository ...", - "search.filters.applied.f.author": "Author", "search.filters.applied.f.dateIssued.max": "End date", @@ -3815,8 +3622,6 @@ "search.filters.applied.f.withdrawn": "Withdrawn", - - "search.filters.filter.author.head": "Author", "search.filters.filter.author.placeholder": "Author name", @@ -3963,8 +3768,6 @@ "search.filters.filter.supervisedBy.label": "Search Supervised By", - - "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalVolume": "Journal Volume", @@ -3983,23 +3786,18 @@ "search.filters.withdrawn.false": "No", - "search.filters.head": "Filters", "search.filters.reset": "Reset filters", "search.filters.search.submit": "Submit", - - "search.form.search": "Search", "search.form.search_dspace": "All repository", "search.form.scope.all": "All of DSpace", - - "search.results.head": "Search Results", "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", @@ -4016,7 +3814,6 @@ "default-relationships.search.results.head": "Search Results", - "search.sidebar.close": "Back to results", "search.sidebar.filters.title": "Filters", @@ -4031,16 +3828,12 @@ "search.sidebar.settings.title": "Settings", - - "search.view-switch.show-detail": "Show detail", "search.view-switch.show-grid": "Show as grid", "search.view-switch.show-list": "Show as list", - - "sorting.ASC": "Ascending", "sorting.DESC": "Descending", @@ -4065,7 +3858,6 @@ "sorting.lastModified.DESC": "Last modified Descending", - "statistics.title": "Statistics", "statistics.header": "Statistics for {{ scope }}", @@ -4090,8 +3882,6 @@ "statistics.table.no-name": "(object name could not be loaded)", - - "submission.edit.breadcrumbs": "Edit Submission", "submission.edit.title": "Edit Submission", @@ -4120,7 +3910,6 @@ "submission.general.save-later": "Save for later", - "submission.import-external.page.title": "Import metadata from an external source", "submission.import-external.title": "Import metadata from an external source", @@ -4328,6 +4117,7 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", @@ -4343,9 +4133,11 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", @@ -4382,17 +4174,16 @@ "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfProject": "Funder of the Project", - - - "submission.sections.describe.relationship-lookup.selection-tab.search-form.placeholder": "Search...", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Journals", @@ -4400,6 +4191,7 @@ "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.Project": "Projects", "submission.sections.describe.relationship-lookup.title.Publication": "Publications", @@ -4431,6 +4223,7 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Selected Publications", @@ -4446,11 +4239,13 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Selected Journal Volume", "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Selected Funding Agency", "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Selected Issue", "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Selected Organizational Unit", @@ -4567,7 +4362,6 @@ "submission.sections.submit.progressbar.sherpaPolicies": "Publisher open access policy information", - "submission.sections.sherpa-policy.title-empty": "No publisher policy information available. If your work has an associated ISSN, please enter it above to see any related publisher open access policies.", "submission.sections.status.errors.title": "Errors", @@ -4690,7 +4484,6 @@ "submission.sections.license.notgranted": "You must accept the license", - "submission.sections.sherpa.publication.information": "Publication information", "submission.sections.sherpa.publication.information.title": "Title", @@ -4743,14 +4536,10 @@ "submission.sections.sherpa.error.message": "There was an error retrieving sherpa informations", - - "submission.submit.breadcrumbs": "New submission", "submission.submit.title": "New submission", - - "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete-help": "Select this option to discard this item. You will then be asked to confirm it.", @@ -4763,17 +4552,14 @@ "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - "submission.workflow.generic.submit_select_reviewer": "Select Reviewer", "submission.workflow.generic.submit_select_reviewer-help": "", - "submission.workflow.generic.submit_score": "Rate", "submission.workflow.generic.submit_score-help": "", - "submission.workflow.tasks.claimed.approve": "Approve", "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", @@ -4802,8 +4588,6 @@ "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - - "submission.workflow.tasks.generic.error": "Error occurred during operation...", "submission.workflow.tasks.generic.processing": "Processing...", @@ -4812,8 +4596,6 @@ "submission.workflow.tasks.generic.success": "Operation successful", - - "submission.workflow.tasks.pool.claim": "Claim", "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", @@ -4822,7 +4604,6 @@ "submission.workflow.tasks.pool.show-detail": "Show detail", - "submission.workspace.generic.view": "View", "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", @@ -4899,7 +4680,6 @@ "subscriptions.table.empty.message": "You do not have any subscriptions at this time. To subscribe to email updates for a Community or Collection, use the subscription button on the object's page.", - "thumbnail.default.alt": "Thumbnail Image", "thumbnail.default.placeholder": "No Thumbnail Available", @@ -4916,12 +4696,8 @@ "thumbnail.person.placeholder": "No Profile Picture Available", - - "title": "DSpace", - - "vocabulary-treeview.header": "Hierarchical tree view", "vocabulary-treeview.load-more": "Load more", @@ -4956,8 +4732,6 @@ "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", - - "supervisedWorkspace.search.results.head": "Supervised Items", "workspace.search.results.head": "Your submissions", @@ -4968,8 +4742,6 @@ "supervision.search.results.head": "Workflow and Workspace tasks", - - "workflow-item.edit.breadcrumbs": "Edit workflowitem", "workflow-item.edit.title": "Edit workflowitem", @@ -4990,7 +4762,6 @@ "workflow-item.delete.button.confirm": "Delete", - "workflow-item.send-back.notification.success.title": "Sent back to submitter", "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", @@ -5047,7 +4818,6 @@ "workflow-item.selectrevieweraction.button.confirm": "Confirm", - "workflow-item.scorereviewaction.notification.success.title": "Rating review", "workflow-item.scorereviewaction.notification.success.content": "The rating for this item workflow item has been successfully submitted", @@ -5315,11 +5085,11 @@ "person.orcid.registry.queue": "ORCID Registry Queue", "person.orcid.registry.auth": "ORCID Authorizations", + "home.recent-submissions.head": "Recent Submissions", "listable-notification-object.default-message": "This object couldn't be retrieved", - "system-wide-alert-banner.retrieval.error": "Something went wrong retrieving the system-wide alert banner", "system-wide-alert-banner.countdown.prefix": "In", @@ -5330,8 +5100,6 @@ "system-wide-alert-banner.countdown.minutes": "{{minutes}} minute(s):", - - "menu.section.system-wide-alert": "System-wide Alert", "system-wide-alert.form.header": "System-wide Alert", @@ -5367,4 +5135,4 @@ "admin.system-wide-alert.breadcrumbs": "System-wide Alerts", "admin.system-wide-alert.title": "System-wide Alerts", -} +} \ No newline at end of file From 134eac5f39791db671bca4460f973cbbb7b622ed Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 14 Jun 2023 08:53:41 +0300 Subject: [PATCH 34/41] src/assets: update fi.json5 Add a few Finnish language UI strings. --- src/assets/i18n/fi.json5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index ca41138eda..62e7e6bffe 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -1922,6 +1922,9 @@ // "home.breadcrumbs": "Home", "home.breadcrumbs": "Etusivu", + // "home.search-form.placeholder": "Search the repository ...", + "home.search-form.placeholder": "Hae julkaisuarkistosta ...", + // "home.title": "DSpace Angular :: Home", "home.title": "DSpace Angular :: Etusivu", @@ -4020,6 +4023,8 @@ // "search.breadcrumbs": "Search", "search.breadcrumbs": "Hae", + // "search.search-form.placeholder": "Search the repository ...", + "search.search-form.placeholder": "Hae julkaisuarkistosta ...", // "search.filters.applied.f.author": "Author", "search.filters.applied.f.author": "Tekijä", From 93fcbe79aa2bdd9e41f8d6a22c455e211d227f50 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 11 May 2023 12:50:47 -0400 Subject: [PATCH 35/41] Make grant message an optional addition; main text is in backend. --- .../email-request-copy.component.html | 5 +--- .../grant-request-copy.component.ts | 30 ------------------- src/assets/i18n/en.json5 | 6 ++-- 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.html b/src/app/request-copy/email-request-copy/email-request-copy.component.html index d7633b0334..70146ab52c 100644 --- a/src/app/request-copy/email-request-copy/email-request-copy.component.html +++ b/src/app/request-copy/email-request-copy/email-request-copy.component.html @@ -8,10 +8,7 @@
- -
- {{ 'grant-deny-request-copy.email.message.empty' | translate }} -
+
diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts index 79bae360a0..3f0fd74bc7 100644 --- a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts @@ -9,12 +9,8 @@ import { import { RemoteData } from '../../core/data/remote-data'; import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest as observableCombineLatest } from 'rxjs'; import { ItemDataService } from '../../core/data/item-data.service'; -import { EPerson } from '../../core/eperson/models/eperson.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { Item } from '../../core/shared/item.model'; -import { isNotEmpty } from '../../shared/empty.util'; import { ItemRequestDataService } from '../../core/data/item-request-data.service'; import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -54,8 +50,6 @@ export class GrantRequestCopyComponent implements OnInit { private route: ActivatedRoute, private authService: AuthService, private translateService: TranslateService, - private itemDataService: ItemDataService, - private nameService: DSONameService, private itemRequestService: ItemRequestDataService, private notificationsService: NotificationsService, ) { @@ -69,31 +63,7 @@ export class GrantRequestCopyComponent implements OnInit { redirectOn4xx(this.router, this.authService), ); - const msgParams$ = observableCombineLatest([ - this.itemRequestRD$.pipe(getFirstSucceededRemoteDataPayload()), - this.authService.getAuthenticatedUserFromStore(), - ]).pipe( - switchMap(([itemRequest, user]: [ItemRequest, EPerson]) => { - return this.itemDataService.findById(itemRequest.itemId).pipe( - getFirstSucceededRemoteDataPayload(), - map((item: Item) => { - const uri = item.firstMetadataValue('dc.identifier.uri'); - return Object.assign({ - recipientName: itemRequest.requestName, - itemUrl: isNotEmpty(uri) ? uri : item.handle, - itemName: this.nameService.getName(item), - authorName: this.nameService.getName(user), - authorEmail: user.email, - }); - }), - ); - }), - ); - this.subject$ = this.translateService.get('grant-request-copy.email.subject'); - this.message$ = msgParams$.pipe( - switchMap((params) => this.translateService.get('grant-request-copy.email.message', params)), - ); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 480bf8834e..b1bb88fce6 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1726,7 +1726,7 @@ "grant-deny-request-copy.email.back": "Back", - "grant-deny-request-copy.email.message": "Message", + "grant-deny-request-copy.email.message": "Optional additional message", "grant-deny-request-copy.email.message.empty": "Please enter a message", @@ -1752,15 +1752,13 @@ "grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.", - "grant-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", - "grant-request-copy.email.subject": "Request copy of document", "grant-request-copy.error": "An error occurred", "grant-request-copy.header": "Grant document copy request", - "grant-request-copy.intro": "This message will be sent to the applicant of the request. The requested document(s) will be attached.", + "grant-request-copy.intro": "A message will be sent to the applicant of the request. The requested document(s) will be attached.", "grant-request-copy.success": "Successfully granted item request", From 004645b5e44a10b93aa26b9b6bbd59c0e0400f28 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 11 May 2023 13:28:30 -0400 Subject: [PATCH 36/41] Remove lint. --- .../grant-request-copy/grant-request-copy.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts index 3f0fd74bc7..baf078df76 100644 --- a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts @@ -9,8 +9,6 @@ import { import { RemoteData } from '../../core/data/remote-data'; import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; -import { ItemDataService } from '../../core/data/item-data.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { ItemRequestDataService } from '../../core/data/item-request-data.service'; import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; From 2d716c7630b03be67d1df950baa808f8a07fbb96 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 11 May 2023 13:57:38 -0400 Subject: [PATCH 37/41] Remove test which is no longer valid. --- .../grant-request-copy.component.spec.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts b/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts index b9d51f710d..32fef125ea 100644 --- a/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts @@ -123,19 +123,6 @@ describe('GrantRequestCopyComponent', () => { spyOn(translateService, 'get').and.returnValue(observableOf('translated-message')); }); - it('message$ should be parameterized correctly', (done) => { - component.message$.subscribe(() => { - expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({ - recipientName: itemRequest.requestName, - itemUrl: itemUrl, - itemName: itemName, - authorName: user.name, - authorEmail: user.email, - })); - done(); - }); - }); - describe('grant', () => { let email: RequestCopyEmail; From 2f06a7cb17a3ede463789cba27f47858bb8961c2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 14 Jun 2023 18:26:55 +0200 Subject: [PATCH 38/41] [CST-5729] Change in order to save header only if configured and existing --- server.ts | 27 +++++++++++++++++++++------ src/config/cache-config.interface.ts | 2 +- src/environments/environment.test.ts | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/server.ts b/server.ts index f73bd1b774..3bbb28820a 100644 --- a/server.ts +++ b/server.ts @@ -375,13 +375,13 @@ function cacheCheck(req, res, next) { // If cached copy exists, return it to the user. if (cachedCopy && cachedCopy.page) { - if (cachedCopy.headers && Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { - environment.cache.serverSide.headers.forEach((header) => { - if (cachedCopy.headers[header.toLowerCase()]) { + if (cachedCopy.headers) { + Object.keys(cachedCopy.headers).forEach((header) => { + if (cachedCopy.headers[header]) { if (environment.cache.serverSide.debug) { console.log(`Restore cached ${header} header`); } - res.setHeader(header, cachedCopy.headers[header.toLowerCase()]); + res.setHeader(header, cachedCopy.headers[header]); } }); } @@ -462,8 +462,8 @@ function saveToCache(req, page: any) { // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) if (key.startsWith('/reload')) { return; } - // Retrieve response headers - const headers = req.res.getHeaders(); + // Retrieve response headers to save, if any + const headers = retrieveHeaders(req.res); // If bot cache is enabled, save it to that cache if it doesn't exist or is expired // (NOTE: has() will return false if page is expired in cache) if (botCacheEnabled() && !botCache.has(key)) { @@ -479,6 +479,21 @@ function saveToCache(req, page: any) { } } +function retrieveHeaders(response) { + const headers = Object.create({}); + if (Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { + environment.cache.serverSide.headers.forEach((header) => { + if (response.hasHeader(header)) { + if (environment.cache.serverSide.debug) { + console.log(`Save ${header} header to cache`); + } + headers[header] = response.getHeader(header); + } + }); + } + + return headers; +} /** * Whether a user is authenticated or not */ diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index 14af509bbf..73520c95ea 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -13,7 +13,7 @@ export interface CacheConfig extends Config { serverSide: { // Debug server-side caching. Set to true to see cache hits/misses/refreshes in console logs. debug: boolean, - // List of headers to restore from the cache hit + // List of response headers to save into the cache headers: string[], // Cache specific to known bots. Allows you to serve cached contents to bots only. botCache: { diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 9fe58f868c..cb9d2c7130 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -59,6 +59,7 @@ export const environment: BuildConfig = { // In-memory cache of server-side rendered pages. Disabled in test environment (max=0) serverSide: { debug: false, + headers: ['Link'], botCache: { max: 0, timeToLive: 24 * 60 * 60 * 1000, // 1 day From 47543b42373a7c0a95600a7e8ef2943dcbc3ef51 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 15 Jun 2023 17:18:00 +0200 Subject: [PATCH 39/41] [CST-5729] Fix duplicate links for download page --- .../bitstream-download-page/bitstream-download-page.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index cf8d8e7767..0b8e6a66e5 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -108,7 +108,6 @@ export class BitstreamDownloadPageComponent implements OnInit { signpostingLinks.forEach((link: SignpostingLink) => { links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; }); this.responseService.setHeader('Link', links); From 4c6cae911bcf7e55484c2d7411161f4e2c567272 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 14 Jun 2023 12:03:11 +0200 Subject: [PATCH 40/41] [DURACOM-157] Multiple uploaders in the same page don't work --- src/app/shared/upload/uploader/uploader.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/upload/uploader/uploader.component.html b/src/app/shared/upload/uploader/uploader.component.html index c55a3eee1a..88dc5926e4 100644 --- a/src/app/shared/upload/uploader/uploader.component.html +++ b/src/app/shared/upload/uploader/uploader.component.html @@ -22,10 +22,10 @@ {{dropMsg | translate}}{{'uploader.or' | translate}} -
From 69726f6fa0f1347158ae336abaf2a7188c7eb421 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 16 Jun 2023 15:00:52 +0200 Subject: [PATCH 41/41] resolve German translation differences --- src/assets/i18n/de.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 71dfabb1c0..b03dc21e5a 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -262,7 +262,7 @@ "admin.registries.schema.fields.table.id": "ID", // "admin.registries.schema.fields.table.scopenote": "Scope Note", - "admin.registries.schema.fields.table.scopenote": "Gültigkeitsbereich", + "admin.registries.schema.fields.table.scopenote": "Geltungs- bzw. Gültigkeitsbereich", // "admin.registries.schema.form.create": "Create metadata field", "admin.registries.schema.form.create": "Metadatenfeld anlegen", @@ -277,7 +277,7 @@ "admin.registries.schema.form.qualifier": "Qualifizierer", // "admin.registries.schema.form.scopenote": "Scope Note", - "admin.registries.schema.form.scopenote": "Geltungsbereich", + "admin.registries.schema.form.scopenote": "Geltungs- bzw. Gültigkeitsbereich", // "admin.registries.schema.head": "Metadata Schema", "admin.registries.schema.head": "Metadatenschema",