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', () => { diff --git a/server.ts b/server.ts index d64b80b4ab..3bbb28820a 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,19 @@ function cacheCheck(req, res, next) { } // If cached copy exists, return it to the user. - if (cachedCopy) { + if (cachedCopy && cachedCopy.page) { + 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]); + } + }); + } 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,21 +462,38 @@ function saveToCache(req, page: any) { // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) if (key.startsWith('/reload')) { return; } + // 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)) { - 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.`); } } } } +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/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..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,12 +20,29 @@ + + + {{'admin.batch-import.page.toggle.help' | translate}} + + + +
+ +
+
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..341aefb704 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 --url and the file url', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--url', 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 --url and the file url and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + 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]); + }); + 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 95e69277a8..673e1f23f5 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,22 @@ export class BatchImportPageComponent { * Starts import-metadata script with --zip fileName (and the selected file) */ public importMetadata() { - if (this.fileObject == null) { - this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + if (this.fileObject == null && isEmpty(this.fileURL)) { + 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: '--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: '--url', value: this.fileURL })); + } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } @@ -127,4 +147,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..3dc0036854 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -10,6 +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 { UiSwitchModule } from 'ngx-ui-switch'; import { UploadModule } from '../shared/upload/upload.module'; const ENTRY_COMPONENTS = [ @@ -27,6 +28,7 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, + UiSwitchModule, UploadModule, ], declarations: [ 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); 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.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 6081a076c0..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,6 +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.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -32,19 +33,27 @@ 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; +let bitstreamID: string; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; - +let currentPrimary: string; +let differentPrimary: string; +let bundle; let comp: EditBitstreamPageComponent; let fixture: ComponentFixture; describe('EditBitstreamPageComponent', () => { beforeEach(() => { + bitstreamID = 'current-bitstream-id'; + currentPrimary = bitstreamID; + differentPrimary = '12345-abcde-54321-edcba'; + allFormats = [ Object.assign({ id: '1', @@ -53,7 +62,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 +72,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/png', _links: { - self: {href: 'format-selflink-2'} + self: { href: 'format-selflink-2' } } }), Object.assign({ @@ -73,7 +82,7 @@ describe('EditBitstreamPageComponent', () => { supportLevel: BitstreamFormatSupportLevel.Known, mimetype: 'image/gif', _links: { - self: {href: 'format-selflink-3'} + self: { href: 'format-selflink-3' } } }) ] as BitstreamFormat[]; @@ -103,15 +112,52 @@ 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(), { + uuid: bitstreamID, + id: bitstreamID, metadata: { 'dc.description': [ { @@ -128,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: {}, @@ -155,17 +195,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: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -203,6 +245,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).primaryBitstreamUUID = currentPrimary; + 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).primaryBitstreamUUID = differentPrimary; + 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 +279,83 @@ 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).primaryBitstreamUUID = differentPrimary; + comp.onSubmit(); + }); + + 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).primaryBitstreamUUID = null; + comp.onSubmit(); + }); + + it('should call create with the correct bitstream on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.create).toHaveBeenCalledWith(jasmine.objectContaining({uuid: currentPrimary}), bundle); + }); + }); + }); + 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).primaryBitstreamUUID = currentPrimary; + comp.onSubmit(); + }); + + it('should call delete on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.delete).toHaveBeenCalledWith(jasmine.objectContaining(bundle)); + }); + }); + }); + }); + 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).primaryBitstreamUUID = currentPrimary; + comp.onSubmit(); + }); + 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).primaryBitstreamUUID = differentPrimary; + comp.onSubmit(); + }); + it('should not call anything on the PrimaryBitstreamService', () => { + expect(primaryBitstreamService.put).not.toHaveBeenCalled(); + expect(primaryBitstreamService.delete).not.toHaveBeenCalled(); + expect(primaryBitstreamService.create).not.toHaveBeenCalled(); + }); + }); + }); + describe('when selected format hasn\'t changed', () => { beforeEach(() => { comp.onSubmit(); @@ -261,20 +401,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', () => { @@ -321,16 +454,22 @@ describe('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: {}, @@ -357,6 +496,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -371,7 +511,6 @@ describe('EditBitstreamPageComponent', () => { spyOn(router, 'navigate'); }); - describe('on startup', () => { let rawForm; @@ -440,16 +579,22 @@ describe('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: {}, @@ -475,6 +620,7 @@ describe('EditBitstreamPageComponent', () => { {provide: BitstreamDataService, useValue: bitstreamService}, {provide: DSONameService, useValue: dsoNameService}, {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + { provide: PrimaryBitstreamService, useValue: primaryBitstreamService }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -496,7 +642,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 6134498aba..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 @@ -1,57 +1,31 @@ -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 { 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'; 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, 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'; 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 { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service'; @Component({ selector: 'ds-edit-bitstream-page', @@ -76,6 +50,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ bitstreamFormatsRD$: Observable>>; + /** + * The UUID of the primary bitstream for this bundle + */ + primaryBitstreamUUID: string; + /** * The bitstream to edit */ @@ -191,19 +170,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 +192,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 +200,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifTocContainer = new DynamicFormGroupModel({ id: 'iiifTocContainer', group: [this.iiifTocModel] - },{ + }, { grid: { host: 'form-row' } @@ -231,7 +210,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 +218,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifWidthContainer = new DynamicFormGroupModel({ id: 'iiifWidthContainer', group: [this.iiifWidthModel] - },{ + }, { grid: { host: 'form-row' } @@ -249,7 +228,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 +236,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { iiifHeightContainer = new DynamicFormGroupModel({ id: 'iiifHeightContainer', group: [this.iiifHeightModel] - },{ + }, { grid: { host: 'form-row' } @@ -280,11 +259,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.fileNameModel, this.primaryBitstreamModel ] - },{ - grid: { - host: 'form-row' - } - }), + }, { + grid: { + host: 'form-row' + } + }), new DynamicFormGroupModel({ id: 'descriptionContainer', group: [ @@ -316,7 +295,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: { @@ -380,13 +359,17 @@ 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; constructor(private route: ActivatedRoute, private router: Router, @@ -397,7 +380,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { private bitstreamService: BitstreamDataService, public dsoNameService: DSONameService, private notificationsService: NotificationsService, - private bitstreamFormatService: BitstreamFormatDataService) { + private bitstreamFormatService: BitstreamFormatDataService, + private primaryBitstreamService: PrimaryBitstreamService, + ) { } /** @@ -410,35 +395,59 @@ 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(), ); const allFormats$ = this.bitstreamFormatsRD$.pipe( getFirstSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), ); + const bundle$ = bitstream$.pipe( + switchMap((bitstream: Bitstream) => bitstream.bundle), + 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(), + ); this.subs.push( observableCombineLatest( bitstream$, - allFormats$ - ).subscribe(([bitstream, allFormats]) => { - this.bitstream = bitstream as Bitstream; - this.formats = allFormats.page; - this.setIiifStatus(this.bitstream); - }) + allFormats$, + bundle$, + primaryBitstream$, + item$, + ).pipe() + .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); + }) ); this.subs.push( this.translate.onLangChange .subscribe(() => { - this.updateFieldTranslations(); - }) + this.updateFieldTranslations(); + }) ); } @@ -460,7 +469,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ fileNamePrimaryContainer: { fileName: bitstream.name, - primaryBitstream: false + primaryBitstream: this.primaryBitstreamUUID === bitstream.uuid }, descriptionContainer: { description: bitstream.firstMetadataValue('dc.description') @@ -571,9 +580,56 @@ 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.primaryBitstreamUUID === this.bitstream.uuid; let bitstream$; + let bundle$: Observable; + let errorWhileSaving = false; + if (wasPrimary !== isPrimary) { + 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 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); + } if (isNewFormat) { bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( getFirstCompletedRemoteData(), @@ -592,7 +648,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { bitstream$ = observableOf(this.bitstream); } - bitstream$.pipe( + combineLatest([bundle$, bitstream$]).pipe( + tap(([bundle]) => this.bundle = bundle), switchMap(() => { return this.bitstreamService.update(updatedBitstream).pipe( getFirstSucceededRemoteDataPayload() @@ -604,7 +661,9 @@ 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(); + } }); } @@ -615,8 +674,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']; @@ -633,11 +690,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 { @@ -668,15 +725,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']); } /** @@ -701,11 +750,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/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index 9ef76b1212..aac5ba2723 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,11 +1,12 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model'; import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock'; import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { RouterStub } from '../shared/testing/router.stub'; describe('BrowseByGuard', () => { describe('canActivate', () => { @@ -13,6 +14,7 @@ describe('BrowseByGuard', () => { let dsoService: any; let translateService: any; let browseDefinitionService: any; + let router: any; const name = 'An interesting DSO'; const title = 'Author'; @@ -35,7 +37,9 @@ describe('BrowseByGuard', () => { findById: () => createSuccessfulRemoteDataObject$(browseDefinition) }; - guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, new DSONameServiceMock() as DSONameService); + router = new RouterStub() as any; + + guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, new DSONameServiceMock() as DSONameService, router); }); it('should return true, and sets up the data correctly, with a scope and value', () => { @@ -65,6 +69,7 @@ describe('BrowseByGuard', () => { value: '"' + value + '"' }; expect(scopedRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -97,6 +102,7 @@ describe('BrowseByGuard', () => { value: '' }; expect(scopedNoValueRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); expect(canActivate).toEqual(true); } ); @@ -128,9 +134,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 bf78a3c39f..dd87699a84 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,15 +1,17 @@ -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 { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } 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 { DSONameService } from '../core/breadcrumbs/dso-name.service'; import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { RemoteData } from '../core/data/remote-data'; +import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; @Injectable() /** @@ -21,15 +23,19 @@ export class BrowseByGuard implements CanActivate { protected translate: TranslateService, protected browseDefinitionService: BrowseDefinitionDataService, protected dsoNameService: DSONameService, + 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); } @@ -37,19 +43,24 @@ export class BrowseByGuard implements CanActivate { const value = route.queryParams.value; const metadataTranslated = this.translate.instant(`browse.metadata.${id}`); return browseDefinition$.pipe( - switchMap((browseDefinition: BrowseDefinition) => { - if (hasValue(scope)) { - const dso$: Observable = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteDataPayload()); - return dso$.pipe( - map((dso: DSpaceObject) => { - const name = this.dsoNameService.getName(dso); - route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); - return true; - }) - ); + switchMap((browseDefinition: BrowseDefinition | undefined) => { + if (hasValue(browseDefinition)) { + if (hasValue(scope)) { + const dso$: Observable = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteDataPayload()); + return dso$.pipe( + map((dso: DSpaceObject) => { + const name = this.dsoNameService.getName(dso); + 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); } }) ); 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 94c14236c1..5043d45c93 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 @@ {{ dsoNameService.getName(node.payload) }} + [{{node.payload.archivedItemsCount}}]
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..00d6d7f03c --- /dev/null +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -0,0 +1,181 @@ +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'; + +describe('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', () => { + 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(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', () => { + 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, + 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 request the bundle from the bundleDataService', () => { + const result = service.delete(testBundle); + result.subscribe(); + expect((service as any).createAndSendRequest).toHaveBeenCalledWith( + DeleteRequest, + 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 new file mode 100644 index 0000000000..488cb5d22e --- /dev/null +++ b/src/app/core/data/primary-bitstream.service.ts @@ -0,0 +1,119 @@ +import { Bitstream } from '../shared/bitstream.model'; +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 { 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 { + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected bundleDataService: BundleDataService, + ) { + } + + /** + * 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 existing 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 existing 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) => { + return this.bundleDataService.findByHref(bundle.self, rd.hasFailed); + }) + ); + } + +} 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 d051ecf8db..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,7 +25,7 @@ export class SignpostingDataService { * @param uuid */ getLinks(uuid: string): Observable { - const baseUrl = this.halService.getRootHref().replace('/api', ''); + const baseUrl = `${this.appConfig.rest.baseUrl}`; return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index d1c49c8d4b..c97c61eceb 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 + 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 f16aad9645..03b47fb024 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 + archivedItemsCount: number; + /** * The {@link HALLink}s for this Community */ 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..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 @@ -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..f29a786e8b 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/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.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; 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..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,12 +9,6 @@ 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 +48,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 +61,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/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 0410cfb5dd..1b27c9d308 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -13,22 +13,20 @@ (ngbEvent)="onCustomEvent($event)"> -
-
+
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 6c2cff5215..83d88b6a63 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,6 +4,7 @@ {{ dsoNameService.getName(object) }} +[{{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 04715983c9..97f9eb88c2 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 @@ -9,6 +9,29 @@ import { DSONameServiceMock } from '../../mocks/dso-name.service.mock'; let collectionListElementComponent: CollectionListElementComponent; let fixture: ComponentFixture; +const mockCollectionWithArchivedItems: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItemsCount: 1 +}); + +const mockCollectionWithArchivedItemsDisabledAtBackend: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItemsCount: -1 +}); + + const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { metadata: { 'dc.description.abstract': [ @@ -17,7 +40,7 @@ const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { value: 'Short description' } ] - } + }, archivedItemsCount: 1 }); const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), { @@ -28,7 +51,7 @@ const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection() value: 'Test title' } ] - } + }, archivedItemsCount: 1 }); describe('CollectionListElementComponent', () => { @@ -74,4 +97,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 archived items are disabled at backend', () => { + beforeEach(() => { + collectionListElementComponent.object = mockCollectionWithArchivedItemsDisabledAtBackend; + fixture.detectChanges(); + }); + + it('should not show the archived items paragraph', () => { + const field = fixture.debugElement.query(By.css('span.archived-items-lead')); + expect(field).toBeNull(); + }); + }); }); 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 462db7be91..8ade44a4df 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 @@ {{ dsoNameService.getName(object) }} +[{{object.archivedItemsCount}}]
{{object.shortDescription}}
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}} -
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 889f17c393..4ea37b93e0 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -294,7 +294,9 @@ describe('SubmissionSectionFormComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = {}; - compAsAny.sectionMetadata = ['dc.title']; + compAsAny.sectionData.data = { + 'dc.title': [new FormFieldMetadataValueObject('test')] + }; spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough(); expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy(); @@ -306,7 +308,9 @@ describe('SubmissionSectionFormComponent test suite', () => { 'dc.title': [new FormFieldMetadataValueObject('test')] }; compAsAny.formData = newSectionData; - compAsAny.sectionMetadata = ['dc.title']; + compAsAny.sectionData.data = { + 'dc.title': [new FormFieldMetadataValueObject('test')] + }; spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough(); expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); @@ -343,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: [ { @@ -371,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', () => { @@ -390,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 326d277ffb..3fa9000039 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -39,6 +39,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. @@ -228,7 +229,9 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { const sectionDataToCheck = {}; Object.keys(sectionData).forEach((key) => { - if (this.sectionMetadata && this.sectionMetadata.includes(key) && this.inCurrentSubmissionScope(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]; } }); @@ -256,9 +259,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: { 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", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ac0da7e15d..af578f3194 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", @@ -568,6 +552,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,8 +570,16 @@ "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", + + "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.", @@ -602,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", @@ -664,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", @@ -705,6 +694,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.", @@ -721,7 +712,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", @@ -737,6 +727,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 *", @@ -765,8 +756,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", @@ -861,20 +850,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", @@ -897,16 +882,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", @@ -947,7 +928,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", @@ -966,14 +946,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", @@ -1032,8 +1008,6 @@ "collection.edit.tabs.source.title": "Collection Edit - Content Source", - - "collection.edit.template.add-button": "Add", "collection.edit.template.breadcrumbs": "Item template", @@ -1058,8 +1032,6 @@ "collection.edit.template.title": "Edit Template Item", - - "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -1078,12 +1050,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", @@ -1096,46 +1064,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", @@ -1144,8 +1128,6 @@ "communityList.showMore": "Show More", - - "community.create.head": "Create a Community", "community.create.notifications.success": "Successfully created the Community", @@ -1174,7 +1156,6 @@ "community.edit.breadcrumbs": "Edit Community", - "community.edit.logo.delete.title": "Delete logo", "community.edit.logo.delete-undo.title": "Undo delete", @@ -1193,8 +1174,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", @@ -1203,8 +1182,6 @@ "community.edit.return": "Back", - - "community.edit.tabs.curate.head": "Curate", "community.edit.tabs.curate.title": "Community Edit - Curate", @@ -1221,12 +1198,8 @@ "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - - "community.listelement.badge": "Community", - - "comcol-role.edit.no-group": "None", "comcol-role.edit.create": "Create", @@ -1239,57 +1212,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)", @@ -1316,8 +1278,6 @@ "community.sub-community-list.head": "Communities of this Community", - - "cookies.consent.accept-all": "Accept all", "cookies.consent.accept-selected": "Accept selected", @@ -1370,30 +1330,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", @@ -1418,8 +1370,6 @@ "curation-task.task.register-doi.label": "Register DOI", - - "curation.form.task-select.label": "Task:", "curation.form.submit": "Start", @@ -1438,8 +1388,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", @@ -1452,14 +1400,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", @@ -1646,11 +1590,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", @@ -1665,8 +1606,6 @@ "footer.link.feedback": "Send Feedback", - - "forgot-email.form.header": "Forgot Password", "forgot-email.form.info": "Enter the email address associated with the account.", @@ -1689,8 +1628,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", @@ -1719,7 +1656,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", @@ -1788,13 +1724,11 @@ "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", - "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", @@ -1820,21 +1754,16 @@ "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", - "health.breadcrumbs": "Health", "health-page.heading": "Health", @@ -1875,7 +1804,6 @@ "health-page.section.no-issues": "No issues detected", - "home.description": "", "home.breadcrumbs": "Home", @@ -1888,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", @@ -1906,6 +1832,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", @@ -1938,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", @@ -1980,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 }}.", @@ -2042,8 +1960,6 @@ "item.edit.bitstreams.upload-button": "Upload", - - "item.edit.delete.cancel": "Cancel", "item.edit.delete.confirm": "Delete", @@ -2062,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", @@ -2145,8 +2060,6 @@ "item.edit.item-mapper.tabs.map": "Map new collections", - - "item.edit.metadata.add-button": "Add", "item.edit.metadata.discard-button": "Discard", @@ -2203,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", @@ -2239,8 +2148,6 @@ "item.edit.move.title": "Move item", - - "item.edit.private.cancel": "Cancel", "item.edit.private.confirm": "Make it non-discoverable", @@ -2253,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", @@ -2267,8 +2172,6 @@ "item.edit.public.success": "The item is now discoverable", - - "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.confirm": "Reinstate", @@ -2281,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", @@ -2313,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", @@ -2324,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", @@ -2392,8 +2292,6 @@ "item.edit.tabs.view.title": "Item Edit - View", - - "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.confirm": "Withdraw", @@ -2408,7 +2306,6 @@ "item.orcid.return": "Back", - "item.listelement.badge": "Item", "item.page.description": "Description", @@ -2447,8 +2344,6 @@ "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - - "item.page.abstract": "Abstract", "item.page.author": "Authors", @@ -2575,8 +2470,6 @@ "item.preview.oaire.fundingStream": "Funding Stream:", - - "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", @@ -2587,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", @@ -2628,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", @@ -2660,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}}?", @@ -2677,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", @@ -2738,8 +2624,6 @@ "itemtemplate.edit.metadata.save-button": "Save", - - "journal.listelement.badge": "Journal", "journal.page.description": "Description", @@ -2760,8 +2644,6 @@ "journal.search.title": "Journal Search", - - "journalissue.listelement.badge": "Journal Issue", "journalissue.page.description": "Description", @@ -2780,8 +2662,6 @@ "journalissue.page.titleprefix": "Journal Issue: ", - - "journalvolume.listelement.badge": "Journal Volume", "journalvolume.page.description": "Description", @@ -2794,7 +2674,6 @@ "journalvolume.page.volume": "Volume", - "iiifsearchable.listelement.badge": "Document Media", "iiifsearchable.page.titleprefix": "Document: ", @@ -2817,7 +2696,6 @@ "iiif.page.description": "Description: ", - "loading.bitstream": "Loading bitstream...", "loading.bitstreams": "Loading bitstreams...", @@ -2854,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?", @@ -2880,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", @@ -2906,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", @@ -2934,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", @@ -2950,8 +2812,6 @@ "menu.section.edit_item": "Item", - - "menu.section.export": "Export", "menu.section.export_collection": "Collection", @@ -2964,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", @@ -2997,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", @@ -3019,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", @@ -3067,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", @@ -3160,8 +3004,6 @@ "mydspace.view-btn": "View", - - "nav.browse.header": "All of DSpace", "nav.community-browse.header": "By Community", @@ -3186,7 +3028,6 @@ "nav.search.button": "Submit search", - "nav.statistics.header": "Statistics", "nav.stop-impersonating": "Stop impersonating EPerson", @@ -3199,7 +3040,6 @@ "none.listelement.badge": "Item", - "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.no-title": "Untitled", @@ -3218,8 +3058,6 @@ "orgunit.page.titleprefix": "Organizational Unit: ", - - "pagination.options.description": "Pagination options", "pagination.results-per-page": "Results Per Page", @@ -3230,8 +3068,6 @@ "pagination.sort-direction": "Sort Options", - - "person.listelement.badge": "Person", "person.listelement.no-title": "No name found", @@ -3264,8 +3100,6 @@ "person.search.title": "Person Search", - - "process.new.select-parameters": "Parameters", "process.new.cancel": "Cancel", @@ -3306,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", @@ -3354,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", @@ -3392,8 +3222,6 @@ "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", - - "profile.breadcrumbs": "Update Profile", "profile.card.identify": "Identify", @@ -3480,8 +3308,6 @@ "project-relationships.search.results.head": "Project Search Results", - - "publication.listelement.badge": "Publication", "publication.page.description": "Description", @@ -3504,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", @@ -3554,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.", @@ -3582,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", @@ -3592,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", @@ -3636,13 +3460,11 @@ "relationships.isFundingAgencyOf.OrgUnit": "Funder", - "repository.image.logo": "Repository logo", - "repository.title.prefix": "DSpace Angular :: ", - - "repository.title.prefixDSpace": "DSpace Angular ::", + "repository.title": "DSpace Repository", + "repository.title.prefix": "DSpace Repository :: ", "resource-policies.add.button": "Add", @@ -3758,8 +3580,6 @@ "resource-policies.table.headers.title.for.collection": "Policies for Collection", - - "search.description": "", "search.switch-configuration.title": "Show", @@ -3770,7 +3590,6 @@ "search.search-form.placeholder": "Search the repository ...", - "search.filters.applied.f.author": "Author", "search.filters.applied.f.dateIssued.max": "End date", @@ -3803,8 +3622,6 @@ "search.filters.applied.f.withdrawn": "Withdrawn", - - "search.filters.filter.author.head": "Author", "search.filters.filter.author.placeholder": "Author name", @@ -3951,8 +3768,6 @@ "search.filters.filter.supervisedBy.label": "Search Supervised By", - - "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalVolume": "Journal Volume", @@ -3971,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", @@ -4004,7 +3814,6 @@ "default-relationships.search.results.head": "Search Results", - "search.sidebar.close": "Back to results", "search.sidebar.filters.title": "Filters", @@ -4019,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", @@ -4053,7 +3858,6 @@ "sorting.lastModified.DESC": "Last modified Descending", - "statistics.title": "Statistics", "statistics.header": "Statistics for {{ scope }}", @@ -4078,8 +3882,6 @@ "statistics.table.no-name": "(object name could not be loaded)", - - "submission.edit.breadcrumbs": "Edit Submission", "submission.edit.title": "Edit Submission", @@ -4108,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", @@ -4316,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 }})", @@ -4331,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 }})", @@ -4370,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", @@ -4388,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", @@ -4419,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", @@ -4434,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", @@ -4555,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", @@ -4678,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", @@ -4731,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.", @@ -4751,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\".", @@ -4790,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...", @@ -4800,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.", @@ -4810,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.", @@ -4887,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", @@ -4904,12 +4696,8 @@ "thumbnail.person.placeholder": "No Profile Picture Available", - - "title": "DSpace", - - "vocabulary-treeview.header": "Hierarchical tree view", "vocabulary-treeview.load-more": "Load more", @@ -4944,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", @@ -4956,8 +4742,6 @@ "supervision.search.results.head": "Workflow and Workspace tasks", - - "workflow-item.edit.breadcrumbs": "Edit workflowitem", "workflow-item.edit.title": "Edit workflowitem", @@ -4978,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", @@ -5035,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", @@ -5303,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", @@ -5318,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", @@ -5355,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 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ä", diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index 9560fe46a5..73520c95ea 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 response headers to save into the cache + 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: { 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