From 459da211bede61fb6f2170b96a80293885bdac6c Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 13 Apr 2022 13:03:37 -0400 Subject: [PATCH] Display the access status badges --- config/config.example.yml | 2 + src/app/core/core.module.ts | 4 +- src/app/core/data/item-data.service.ts | 22 +++ .../access-status-badge.component.html | 3 + .../access-status-badge.component.spec.ts | 160 ++++++++++++++++++ .../access-status-badge.component.ts | 45 +++++ .../access-status.model.ts | 23 +++ .../access-status.resource-type.ts | 9 + ...-search-result-list-element.component.html | 5 +- ...em-search-result-list-element.component.ts | 6 + src/app/shared/shared.module.ts | 2 + src/assets/i18n/en.json5 | 10 ++ src/assets/i18n/fr.json5 | 15 ++ src/config/default-app-config.ts | 5 +- src/config/ui-server-config.interface.ts | 2 + src/environments/environment.test.ts | 4 +- 16 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.html create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.model.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.resource-type.ts diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b1653..724d7f8a64 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -13,6 +13,8 @@ ui: rateLimiter: windowMs: 60000 # 1 minute max: 500 # limit each IP to 500 requests per windowMs + # Show the file access status in items lists + showAccessStatuses: false # The REST API server settings # NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e9e242dbc0..b90a3edd46 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,7 @@ import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; +import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -346,7 +347,8 @@ export const models = UsageReport, Root, SearchConfig, - SubmissionAccessesModel + SubmissionAccessesModel, + AccessStatusObject ]; @NgModule({ diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cb5d7a3d57..9a2dc5a8af 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -36,6 +36,7 @@ import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; @Injectable() @dataService(ITEM) @@ -291,6 +292,27 @@ export class ItemDataService extends DataService { ); } + /** + * Get the the access status + * @param itemId + */ + public getAccessStatus(itemId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint('accessStatus').pipe( + map((href) => href.replace('{?uuid}', `?uuid=${itemId}`)) + ); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new GetRequest(requestId, href); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + /** * Invalidate the cache of the item * @param itemUUID diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html new file mode 100644 index 0000000000..a7b3edc9b3 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -0,0 +1,3 @@ +
+ {{ status | translate }} +
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts new file mode 100644 index 0000000000..da0677a5de --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts @@ -0,0 +1,160 @@ +import { Item } from '../../../core/shared/item.model'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AccessStatusBadgeComponent } from './access-status-badge.component'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { By } from '@angular/platform-browser'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; + +describe('ItemAccessStatusBadgeComponent', () => { + let component: AccessStatusBadgeComponent; + let fixture: ComponentFixture; + + let unknownStatus: AccessStatusObject; + let metadataOnlyStatus: AccessStatusObject; + let openAccessStatus: AccessStatusObject; + let embargoStatus: AccessStatusObject; + let restrictedStatus: AccessStatusObject; + + let itemDataService: ItemDataService; + + let item: Item; + + function init() { + unknownStatus = Object.assign(new AccessStatusObject(), { + status: 'unknown' + }); + + metadataOnlyStatus = Object.assign(new AccessStatusObject(), { + status: 'metadata.only' + }); + + openAccessStatus = Object.assign(new AccessStatusObject(), { + status: 'open.access' + }); + + embargoStatus = Object.assign(new AccessStatusObject(), { + status: 'embargo' + }); + + restrictedStatus = Object.assign(new AccessStatusObject(), { + status: 'restricted' + }); + + itemDataService = jasmine.createSpyObj('itemDataService', { + getAccessStatus: createSuccessfulRemoteDataObject$(unknownStatus) + }); + + item = Object.assign(new Item(), { + uuid: 'item-uuid' + }); + } + + function initTestBed() { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AccessStatusBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ItemDataService, useValue: itemDataService} + ] + }).compileComponents(); + } + + function initFixtureAndComponent() { + fixture = TestBed.createComponent(AccessStatusBadgeComponent); + component = fixture.componentInstance; + component.uuid = item.uuid; + fixture.detectChanges(); + } + + function lookForAccessStatusBadge(status: string) { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge.nativeElement.textContent).toEqual(`access-status.${status.toLowerCase()}.listelement.badge`); + } + + describe('init', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should init the component', () => { + expect(component).toBeTruthy(); + }); + }); + + describe('When the getAccessStatus method returns unknown', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the unknown badge', () => { + lookForAccessStatusBadge('unknown'); + }); + }); + + describe('When the getAccessStatus method returns metadata.only', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the metadata only badge', () => { + lookForAccessStatusBadge('metadata.only'); + }); + }); + + describe('When the getAccessStatus method returns open.access', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the open access badge', () => { + lookForAccessStatusBadge('open.access'); + }); + }); + + describe('When the getAccessStatus method returns embargo', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the embargo badge', () => { + lookForAccessStatusBadge('embargo'); + }); + }); + + describe('When the getAccessStatus method returns restricted', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the restricted badge', () => { + lookForAccessStatusBadge('restricted'); + }); + }); +}); diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts new file mode 100644 index 0000000000..b181ae4104 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { getFirstSucceededRemoteDataPayload } from 'src/app/core/shared/operators'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; +import { hasValue } from '../../empty.util'; + +@Component({ + selector: 'ds-access-status-badge', + templateUrl: './access-status-badge.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class AccessStatusBadgeComponent { + + private _uuid: string; + private _accessStatus$: Observable; + + /** + * Initialize instance variables + * + * @param {ItemDataService} itemDataService + */ + constructor(private itemDataService: ItemDataService) { } + + ngOnInit(): void { + this._accessStatus$ = this.itemDataService + .getAccessStatus(this._uuid) + .pipe( + getFirstSucceededRemoteDataPayload(), + map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`) + ); + } + + @Input() set uuid(uuid: string) { + this._uuid = uuid; + } + + get accessStatus$(): Observable { + return this._accessStatus$; + } +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts new file mode 100644 index 0000000000..6cdcafdbd8 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -0,0 +1,23 @@ +import { autoserialize } from 'cerialize'; +import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { ResourceType } from 'src/app/core/shared/resource-type'; +import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; +import { ACCESS_STATUS } from './access-status.resource-type'; + +@typedObject +export class AccessStatusObject { + static type = ACCESS_STATUS; + + /** + * The type for this AccessStatusObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The access status value + */ + @autoserialize + status: string; +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts new file mode 100644 index 0000000000..ead2afc0b1 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from 'src/app/core/shared/resource-type'; + +/** + * The resource type for Access Status + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const ACCESS_STATUS = new ResourceType('accessStatus'); diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8898632eb5..8bea795cca 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,4 +1,7 @@ - +
+ + +