diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 786b5b3de30ec2f2664b090182bdac12780185ec..3e0c805be8f01f5e232cebd00c280e422d815ac1 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -12,6 +12,8 @@ import { NormalizedWorkspaceItem } from '../../submission/models/normalized-work import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model'; import { NormalizedGroup } from '../../eperson/models/normalized-group.model'; import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model'; +import { NormalizedClaimedTask } from '../../tasks/models/normalized-claimed-task-object.model'; +import { NormalizedPoolTask } from '../../tasks/models/normalized-pool-task-object.model'; import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model'; import { SubmissionFormsModel } from '../../config/models/config-submission-forms.model'; @@ -63,6 +65,12 @@ export class NormalizedObjectFactory { case ResourceType.Workflowitem: { return NormalizedWorkflowItem } + case ResourceType.ClaimedTask: { + return NormalizedClaimedTask + } + case ResourceType.PoolTask: { + return NormalizedPoolTask + } case ResourceType.BitstreamFormat: { return NormalizedBitstreamFormat } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 5640ac54d2474181fa98e322f5007bb06bd4ea76..8034a904136a56035524f7a6e5c75b31b879e92b 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -253,4 +253,28 @@ export class EpersonSuccessResponse extends RestResponse { } } +export class MessageResponse extends RestResponse { + public toCache = false; + + constructor( + public statusCode: number, + public statusText: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode, statusText); + } +} + +export class TaskResponse extends RestResponse { + public toCache = false; + + constructor( + public statusCode: number, + public statusText: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode, statusText); + } +} + /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 7106d28b3d3e914f6c608e66cc261f2c3eca6143..ce8e3397d22c646dcc473d7be712b1a34218fe21 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -77,6 +77,14 @@ import { MenuService } from '../shared/menu/menu.service'; import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service'; import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; +import { RoleService } from './roles/role.service'; +import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; +import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; +import { MessageService } from './message/message.service'; +import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; +import { PoolTaskDataService } from './tasks/pool-task-data.service'; +import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; +import { MessageResponseParsingService } from './message/message-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -128,6 +136,7 @@ const PROVIDERS = [ RegistryBitstreamformatsResponseParsingService, DebugResponseParsingService, SearchResponseParsingService, + MyDSpaceResponseParsingService, ServerResponseService, BrowseResponseParsingService, BrowseEntriesResponseParsingService, @@ -156,6 +165,13 @@ const PROVIDERS = [ DSOChangeAnalyzer, CSSVariableService, MenuService, + MyDSpaceGuard, + RoleService, + MessageResponseParsingService, + MessageService, + TaskResponseParsingService, + ClaimedTaskDataService, + PoolTaskDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -166,15 +182,20 @@ const PROVIDERS = [ { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; +const DIRECTIVES = [ +]; + @NgModule({ imports: [ ...IMPORTS ], declarations: [ - ...DECLARATIONS + ...DECLARATIONS, + ...DIRECTIVES ], exports: [ - ...EXPORTS + ...EXPORTS, + ...DIRECTIVES ], providers: [ ...PROVIDERS diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 6102f930b0833dec39fca3d20f49f4417f0d4489..57cd8c2cb9772721b5807ffa5396ac00e804d9e4 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -160,4 +160,10 @@ export abstract class BaseResponseParsingService { } return obj; } + + protected isSuccessStatus(statusCode: number) { + return (statusCode === 201 + || statusCode === 200 + || statusCode === 204) + } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 55642a181c3119ba9aaf20466ce936a63c28ecaf..d02e42ce79d2dfd70c2f39ffbc531af43fce26df 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,5 +1,8 @@ import { Injectable } from '@angular/core'; + +import { filter, map, take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -13,6 +16,10 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { FindAllOptions } from './request.models'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; @Injectable() export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> { @@ -34,4 +41,21 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio super(); } + /** + * Find whether there is a collection whom user has authorization to submit to + * + * @return boolean + * true if the user has at least one collection to submit to + */ + hasAuthorizedCollection(): Observable<boolean> { + const searchHref = 'findAuthorized'; + const options = new FindAllOptions(); + options.elementsPerPage = 1; + + return this.searchBy(searchHref, options).pipe( + filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending), + take(1), + map((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.totalElements > 0) + ); + } } diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a99661faed44d8eeee8295d1723e3f2f1d567617 --- /dev/null +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { RestResponse, SearchSuccessResponse } from '../cache/response.models'; +import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { hasValue } from '../../shared/empty.util'; +import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; +import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces'; + +@Injectable() +export class MyDSpaceResponseParsingService implements ResponseParsingService { + constructor(private dsoParser: DSOResponseParsingService) { + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + // fallback for unexpected empty response + const emptyPayload = { + _embedded : { + objects: [] + } + }; + const payload = data.payload._embedded.searchResult || emptyPayload; + const hitHighlights: MetadataMap[] = payload._embedded.objects + .map((object) => object.hitHighlights) + .map((hhObject) => { + const mdMap: MetadataMap = {}; + if (hhObject) { + for (const key of Object.keys(hhObject)) { + const value: MetadataValue = { value: hhObject[key].join('...'), language: null }; + mdMap[key] = [ value ]; + } + } + return mdMap; + }); + + const dsoSelfLinks = payload._embedded.objects + .filter((object) => hasValue(object._embedded)) + .map((object) => object._embedded.rObject) + .map((dso) => this.dsoParser.parse(request, { + payload: dso, + statusCode: data.statusCode, + statusText: data.statusText + })) + .map((obj) => obj.resourceSelfLinks) + .reduce((combined, thisElement) => [...combined, ...thisElement], []); + + const objects = payload._embedded.objects + .filter((object) => hasValue(object._embedded)) + .map((object, index) => Object.assign({}, object, { + rObject: dsoSelfLinks[index], + hitHighlights: hitHighlights[index] + })); + payload.objects = objects; + const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); + return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload)); + } +} diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index 8efdccd75d55a77cd15ef531976b87d1ce1dd3f8..e1c1b22569b752593030e4f28a45f25f6323ac7a 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -11,7 +11,7 @@ export class PaginatedList<T> { if (hasValue(this.pageInfo) && hasValue(this.pageInfo.elementsPerPage)) { return this.pageInfo.elementsPerPage; } - return this.page.length; + return this.getPageLength(); } set elementsPerPage(value: number) { @@ -22,7 +22,7 @@ export class PaginatedList<T> { if (hasValue(this.pageInfo) && hasValue(this.pageInfo.totalElements)) { return this.pageInfo.totalElements; } - return this.page.length; + return this.getPageLength(); } set totalElements(value: number) { @@ -89,4 +89,8 @@ export class PaginatedList<T> { set self(self: string) { this.pageInfo.self = self; } + + protected getPageLength() { + return (Array.isArray(this.page)) ? this.page.length : 0; + } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 1afd24962c43c9e87276ed81026386cf36c5473c..40680c9a377f95e3a0e29dea96e1a846ef6b2799 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -17,6 +17,8 @@ import { BrowseItemsResponseParsingService } from './browse-items-response-parsi import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service'; import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadatafieldParsingService } from './metadatafield-parsing.service'; +import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; +import { MessageResponseParsingService } from '../message/message-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -370,6 +372,46 @@ export class DeleteByIDRequest extends DeleteRequest { } } +export class MessagePostRequest extends PostRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor<ResponseParsingService> { + return MessageResponseParsingService; + } +} + +export class MessageGetRequest extends GetRequest { + constructor(uuid: string, href: string, public options?: HttpOptions) { + super(uuid, href, null, options); + } + + getResponseParser(): GenericConstructor<ResponseParsingService> { + return MessageResponseParsingService; + } +} + +export class TaskPostRequest extends PostRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor<ResponseParsingService> { + return TaskResponseParsingService; + } +} + +export class TaskDeleteRequest extends DeleteRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor<ResponseParsingService> { + return TaskResponseParsingService; + } +} + export class RequestError extends Error { statusCode: number; statusText: string; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 5c5f0880e0af12d36912ceddcd99ae2829e68649..da1857b1c0d5e5c7f8d4ebccde04aeb5ec8bc5df 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -4,7 +4,7 @@ import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { merge as observableMerge, Observable, of as observableOf, race as observableRace } from 'rxjs'; import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; @@ -123,9 +123,9 @@ export class RequestService { // TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void { const isGetRequest = request.method === RestRequestMethod.GET; - if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) { + if (!isGetRequest || !this.isCachedOrPending(request) || (forceBypassCache && !this.isPending(request))) { this.dispatchRequest(request); - if (isGetRequest && !forceBypassCache) { + if (isGetRequest) { this.trackRequestsOnTheirWayToTheStore(request); } } else { @@ -139,6 +139,29 @@ export class RequestService { } } + /** + * Convert request Payload to a URL-encoded string + * + * e.g. prepareBody({param: value, param1: value1}) + * returns: param=value¶m1=value1 + * + * @param body + * The request Payload to convert + * @return string + * URL-encoded string + */ + public prepareBody(body: any) { + let queryParams = ''; + if (isNotEmpty(body) && typeof body === 'object') { + Object.keys(body) + .forEach((param) => { + const paramValue = `${param}=${body[param]}`; + queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue); + }) + } + return encodeURI(queryParams); + } + /** * Remove all request cache providing (part of) the href * This also includes href-to-uuid index cache diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 0e6ac0fd05731ae38e78ae3c0f913c50af3d91fe..fe9a3a241ea22f201a2f94d1f87ae97c57ffd36d 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -15,7 +15,13 @@ export class SearchResponseParsingService implements ResponseParsingService { } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload._embedded.searchResult; + // fallback for unexpected empty response + const emptyPayload = { + _embedded : { + objects: [] + } + }; + const payload = data.payload._embedded.searchResult || emptyPayload; const hitHighlights: MetadataMap[] = payload._embedded.objects .map((object) => object.hitHighlights) .map((hhObject) => { @@ -31,7 +37,7 @@ export class SearchResponseParsingService implements ResponseParsingService { const dsoSelfLinks = payload._embedded.objects .filter((object) => hasValue(object._embedded)) - .map((object) => object._embedded.dspaceObject) + .map((object) => object._embedded.rObject) // we don't need embedded collections, bitstreamformats, etc for search results. // And parsing them all takes up a lot of time. Throw them away to improve performance // until objs until partial results are supported by the rest api @@ -47,7 +53,7 @@ export class SearchResponseParsingService implements ResponseParsingService { const objects = payload._embedded.objects .filter((object) => hasValue(object._embedded)) .map((object, index) => Object.assign({}, object, { - dspaceObject: dsoSelfLinks[index], + rObject: dsoSelfLinks[index], hitHighlights: hitHighlights[index], // we don't need embedded collections, bitstreamformats, etc for search results. // And parsing them all takes up a lot of time. Throw them away to improve performance diff --git a/src/app/core/message/message-data-response.ts b/src/app/core/message/message-data-response.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c7432b18c2e9a48443a1d53fabffa5cf932e7d9 --- /dev/null +++ b/src/app/core/message/message-data-response.ts @@ -0,0 +1,17 @@ +import { RemoteDataError } from '../data/remote-data-error'; + +/** + * A class to represent the data retrieved by after processing a message + */ +export class MessageDataResponse { + constructor( + private isSuccessful: boolean, + public error?: RemoteDataError, + public payload?: any + ) { + } + + get hasSucceeded(): boolean { + return this.isSuccessful; + } +} diff --git a/src/app/core/message/message-response-parsing.service.ts b/src/app/core/message/message-response-parsing.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a08ab481d4b18c77883590e82a15cb83c30c0dc --- /dev/null +++ b/src/app/core/message/message-response-parsing.service.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ResponseParsingService } from '../data/parsing.service'; +import { RestRequest } from '../data/request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { BaseResponseParsingService } from '../data/base-response-parsing.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NormalizedSubmissionObjectFactory } from '../submission/normalized-submission-object-factory'; +import { ErrorResponse, MessageResponse, RestResponse } from '../cache/response.models'; + +@Injectable() +export class MessageResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = NormalizedSubmissionObjectFactory; + protected toCache = false; + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService,) { + super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (this.isSuccessStatus(data.statusCode)) { + return new MessageResponse( data.statusCode, data.statusText); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from server'), + { statusCode: data.statusCode, statusText: data.statusText } + ) + ); + } + } + +} diff --git a/src/app/core/message/message.service.ts b/src/app/core/message/message.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbe761a443d573d40195533f97bbe5b9a8b48fd8 --- /dev/null +++ b/src/app/core/message/message.service.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; + +import { merge as observableMerge, Observable, of as observableOf } from 'rxjs'; +import { catchError, distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators'; + +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from '../data/request.service'; +import { isNotEmpty } from '../../shared/empty.util'; +import { MessageGetRequest, MessagePostRequest, PostRequest, RestRequest } from '../data/request.models'; +import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { MessageDataResponse } from './message-data-response'; +import { RemoteDataError } from '../data/remote-data-error'; +import { getResponseFromEntry } from '../shared/operators'; +import { ErrorResponse, MessageResponse, RestResponse } from '../cache/response.models'; +import { RestRequestMethod } from '../data/rest-request-method'; + +@Injectable() +export class MessageService { + protected linkPath = 'messages'; + + constructor(protected http: DSpaceRESTv2Service, + protected requestService: RequestService, + protected halService: HALEndpointService) { + } + + protected fetchRequest(requestId: string): Observable<MessageDataResponse> { + const responses = this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + const errorResponses = responses.pipe( + filter((response: RestResponse) => !response.isSuccessful), + mergeMap((response: ErrorResponse) => observableOf( + new MessageDataResponse( + response.isSuccessful, + new RemoteDataError(response.statusCode, response.statusText, response.errorMessage) + )) + )); + const successResponses = responses.pipe( + filter((response: RestResponse) => response.isSuccessful), + map((response: MessageResponse) => new MessageDataResponse(response.isSuccessful)), + distinctUntilChanged() + ); + return observableMerge(errorResponses, successResponses); + } + + protected getEndpointByMethod(endpoint: string, method: string): string { + return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; + } + + public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<MessageDataResponse> { + const requestId = this.requestService.generateRequestId(); + return this.halService.getEndpoint(this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + map((endpointURL: string) => this.getEndpointByMethod(endpointURL, method)), + distinctUntilChanged(), + map((endpointURL: string) => new MessagePostRequest(requestId, endpointURL, body, options)), + tap((request: PostRequest) => this.requestService.configure(request)), + flatMap((request: PostRequest) => this.fetchRequest(requestId)), + distinctUntilChanged()); + } + + public getRequest(method: string, options?: HttpOptions): Observable<any> { + const requestId = this.requestService.generateRequestId(); + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpointURL: string) => this.getEndpointByMethod(endpointURL, method)), + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new MessageGetRequest(requestId, endpointURL)), + tap((request: RestRequest) => this.requestService.configure(request, true)), + flatMap((request: RestRequest) => this.fetchRequest(requestId)), + distinctUntilChanged()); + } + + public createMessage(body: any, options?: HttpOptions): Observable<MessageDataResponse> { + return this.postToEndpoint('', this.requestService.prepareBody(body), this.makeHttpOptions()); + } + + public markAsRead(body: any, options?: HttpOptions): Observable<MessageDataResponse> { + return this.postToEndpoint('read', this.requestService.prepareBody(body), this.makeHttpOptions()); + } + + public markAsUnread(body: any, options?: HttpOptions): Observable<MessageDataResponse> { + return this.postToEndpoint('unread', this.requestService.prepareBody(body), this.makeHttpOptions()); + } + + protected makeHttpOptions() { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); + options.headers = headers; + return options; + } + + public getMessageContent(url: string): Observable<any> { + if (isNotEmpty(url)) { + const options: HttpOptions = Object.create({}); + options.observe = 'response'; + options.responseType = 'text'; + return this.http.request(RestRequestMethod.GET, url, null, options).pipe( + map((res) => ({ payload: res.payload })), + catchError((err) => observableOf({ payload: '' }))); + } else { + return observableOf({ payload: '' }); + } + } +} diff --git a/src/app/core/roles/role-types.ts b/src/app/core/roles/role-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..b39d1205a6d8536917e445ec361cdb86fa22ff89 --- /dev/null +++ b/src/app/core/roles/role-types.ts @@ -0,0 +1,5 @@ +export enum RoleType { + Submitter = 'submitter', + Controller = 'controller', + Admin = 'admin' +} diff --git a/src/app/core/roles/role.service.ts b/src/app/core/roles/role.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..49c358d3f5778546e7716e5b59661bd476a35454 --- /dev/null +++ b/src/app/core/roles/role.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { AppState } from '../../app.reducer'; + +import { Observable, of as observableOf } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { RoleType } from './role-types'; +import { CollectionDataService } from '../data/collection-data.service'; + +@Injectable() +export class RoleService { + + constructor( + private collectionService: CollectionDataService, + private store: Store<AppState>) { + + } + + isSubmitter(): Observable<boolean> { + return this.collectionService.hasAuthorizedCollection().pipe( + distinctUntilChanged() + ); + } + + isController(): Observable<boolean> { + // TODO find a way to check if user is a controller + return observableOf(true); + } + + isAdmin(): Observable<boolean> { + // TODO find a way to check if user is an admin + return observableOf(false); + } + + checkRole(role: RoleType): Observable<boolean> { + let check: Observable<boolean>; + switch (role) { + case RoleType.Submitter: + check = this.isSubmitter(); + break; + case RoleType.Controller: + check = this.isController(); + break; + case RoleType.Admin: + check = this.isAdmin(); + break; + } + + return check; + } +} diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index 484f1ea6e25c26c9ea58731d096747cd3d5bd875..8cbd255ae781f12e2b7c5fab736d8dc31d6e4e6c 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -20,4 +20,6 @@ export enum ResourceType { SubmissionForms = 'submissionforms', SubmissionSections = 'submissionsections', SubmissionSection = 'submissionsection', + ClaimedTask = 'claimedtask', + PoolTask = 'pooltask' } diff --git a/src/app/core/shared/view-mode.model.ts b/src/app/core/shared/view-mode.model.ts index b026d6843132dc1c72ebaa301d5ad87b0d1fbb5f..9c8d08609718d375c16f15d3a49fa8df849f0f67 100644 --- a/src/app/core/shared/view-mode.model.ts +++ b/src/app/core/shared/view-mode.model.ts @@ -4,5 +4,6 @@ export enum ViewMode { List = 'list', - Grid = 'grid' + Grid = 'grid', + Detail = 'detail' } diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f35c999ac5bc32be3061b1c2dbac4e312ea88569 --- /dev/null +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { RequestService } from '../data/request.service'; +import { NormalizedClaimedTask } from './models/normalized-claimed-task-object.model'; +import { ClaimedTask } from './models/claimed-task-object.model'; +import { TasksService } from './tasks.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; + +@Injectable() +export class ClaimedTaskDataService extends TasksService<NormalizedClaimedTask, ClaimedTask> { + protected linkPath = 'claimedtasks'; + protected forceBypassCache = true; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store<CoreState>, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { + super(); + } + + public approveTask(scopeId: string): Observable<any> { + const body = { + submit_approve: 'true' + }; + return this.postToEndpoint(this.linkPath, this.requestService.prepareBody(body), scopeId, this.makeHttpOptions()); + } + + public rejectTask(reason: string, scopeId: string): Observable<any> { + const body = { + submit_reject: 'true', + reason + }; + return this.postToEndpoint(this.linkPath, this.requestService.prepareBody(body), scopeId, this.makeHttpOptions()); + } + + public returnToPoolTask(scopeId: string): Observable<any> { + return this.deleteById(this.linkPath, scopeId, this.makeHttpOptions()); + } + +} diff --git a/src/app/core/tasks/models/claimed-task-object.model.ts b/src/app/core/tasks/models/claimed-task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0474a1aa8fe35ddd1689dcb8ad108d474dc5078 --- /dev/null +++ b/src/app/core/tasks/models/claimed-task-object.model.ts @@ -0,0 +1,5 @@ +import { TaskObject } from './task-object.model'; + +export class ClaimedTask extends TaskObject { + +} diff --git a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b8604ac42812b05f4fc2657d5f23a5cf67ad129 --- /dev/null +++ b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts @@ -0,0 +1,39 @@ +import { NormalizedTaskObject } from './normalized-task-object.model'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { autoserialize, inheritSerialization } from 'cerialize'; +import { ClaimedTask } from './claimed-task-object.model'; +import { ResourceType } from '../../shared/resource-type'; + +/** + * A model class for a NormalizedClaimedTaskObject. + */ +@mapsTo(ClaimedTask) +@inheritSerialization(NormalizedTaskObject) +export class NormalizedClaimedTask extends NormalizedTaskObject { + + /** + * The task identifier + */ + @autoserialize + id: string; + + /** + * The workflow step + */ + @autoserialize + step: string; + + /** + * The task action type + */ + @autoserialize + action: string; + + /** + * The workflowitem object whom this task is related + */ + @autoserialize + @relationship(ResourceType.Workflowitem, false) + workflowitem: string; + +} diff --git a/src/app/core/tasks/models/normalized-pool-task-object.model.ts b/src/app/core/tasks/models/normalized-pool-task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..15152b4f5a455114a25b9aac92b490ba472d65a5 --- /dev/null +++ b/src/app/core/tasks/models/normalized-pool-task-object.model.ts @@ -0,0 +1,38 @@ +import { NormalizedTaskObject } from './normalized-task-object.model'; +import { PoolTask } from './pool-task-object.model'; +import { autoserialize, inheritSerialization } from 'cerialize'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { ResourceType } from '../../shared/resource-type'; + +/** + * A model class for a NormalizedPoolTaskObject. + */ +@mapsTo(PoolTask) +@inheritSerialization(NormalizedTaskObject) +export class NormalizedPoolTask extends NormalizedTaskObject { + + /** + * The task identifier + */ + @autoserialize + id: string; + + /** + * The workflow step + */ + @autoserialize + step: string; + + /** + * The task action type + */ + @autoserialize + action: string; + + /** + * The workflowitem object whom this task is related + */ + @autoserialize + @relationship(ResourceType.Workflowitem, false) + workflowitem: string; +} diff --git a/src/app/core/tasks/models/normalized-task-object.model.ts b/src/app/core/tasks/models/normalized-task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a41ec983a6830513b2da68f8037e95a20652727 --- /dev/null +++ b/src/app/core/tasks/models/normalized-task-object.model.ts @@ -0,0 +1,38 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; +import { TaskObject } from './task-object.model'; + +/** + * An abstract model class for a DSpaceObject. + */ +@mapsTo(TaskObject) +@inheritSerialization(NormalizedDSpaceObject) +export abstract class NormalizedTaskObject extends NormalizedDSpaceObject { + + /** + * The task identifier + */ + @autoserialize + id: string; + + /** + * The workflow step + */ + @autoserialize + step: string; + + /** + * The task action type + */ + @autoserialize + action: string; + + /** + * The workflowitem object whom this task is related + */ + @autoserialize + @relationship(ResourceType.Workflowitem, false) + workflowitem: string; +} diff --git a/src/app/core/tasks/models/pool-task-object.model.ts b/src/app/core/tasks/models/pool-task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcaf4309a13110565f329ece3e3c35ef0f91c6bf --- /dev/null +++ b/src/app/core/tasks/models/pool-task-object.model.ts @@ -0,0 +1,5 @@ +import { TaskObject } from './task-object.model'; + +export class PoolTask extends TaskObject { + +} diff --git a/src/app/core/tasks/models/process-task-response.ts b/src/app/core/tasks/models/process-task-response.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca4bc9a0682dc6e20b7ec316cce154c300749885 --- /dev/null +++ b/src/app/core/tasks/models/process-task-response.ts @@ -0,0 +1,17 @@ +import { RemoteDataError } from '../../data/remote-data-error'; + +/** + * A class to represent the data retrieved by after processing a task + */ +export class ProcessTaskResponse { + constructor( + private isSuccessful: boolean, + public error?: RemoteDataError, + public payload?: any + ) { + } + + get hasSucceeded(): boolean { + return this.isSuccessful; + } +} diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..1475bdb14ad01258f2a4e353f40b317c0bed5f2b --- /dev/null +++ b/src/app/core/tasks/models/task-object.model.ts @@ -0,0 +1,30 @@ +import { Observable } from 'rxjs'; + +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { RemoteData } from '../../data/remote-data'; +import { Workflowitem } from '../../submission/models/workflowitem.model'; + +export class TaskObject extends DSpaceObject implements CacheableObject, ListableObject { + + /** + * The task identifier + */ + id: string; + + /** + * The workflow step + */ + step: string; + + /** + * The task action type + */ + action: string; + + /** + * The workflowitem object whom this task is related + */ + workflowitem: Observable<RemoteData<Workflowitem>> | Workflowitem; +} diff --git a/src/app/core/tasks/pool-task-data.service.ts b/src/app/core/tasks/pool-task-data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..df2c05fe6d2206499d336f82111e9f44d11ab136 --- /dev/null +++ b/src/app/core/tasks/pool-task-data.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { RequestService } from '../data/request.service'; +import { NormalizedPoolTask } from './models/normalized-pool-task-object.model'; +import { PoolTask } from './models/pool-task-object.model'; +import { TasksService } from './tasks.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; + +@Injectable() +export class PoolTaskDataService extends TasksService<NormalizedPoolTask, PoolTask> { + protected linkPath = 'pooltasks'; + protected forceBypassCache = true; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store<CoreState>, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { + super(); + } + + public claimTask(scopeId: string): Observable<any> { + return this.postToEndpoint(this.linkPath, {}, scopeId, this.makeHttpOptions()); + } +} diff --git a/src/app/core/tasks/task-response-parsing.service.ts b/src/app/core/tasks/task-response-parsing.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d12b1b22f8b97520ea619b5eafc1aee8b6493f43 --- /dev/null +++ b/src/app/core/tasks/task-response-parsing.service.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ResponseParsingService } from '../data/parsing.service'; +import { RestRequest } from '../data/request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; + +import { BaseResponseParsingService } from '../data/base-response-parsing.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; +import { ErrorResponse, RestResponse, TaskResponse } from '../cache/response.models'; + +@Injectable() +export class TaskResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = NormalizedObjectFactory; + protected toCache = false; + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService,) { + super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (this.isSuccessStatus(data.statusCode)) { + return new TaskResponse( data.statusCode, data.statusText); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from server'), + { statusCode: data.statusCode, statusText: data.statusText } + ) + ); + } + } + +} diff --git a/src/app/core/tasks/tasks.service.ts b/src/app/core/tasks/tasks.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b42d950507d5603189c684afcf899d5a17b7674 --- /dev/null +++ b/src/app/core/tasks/tasks.service.ts @@ -0,0 +1,82 @@ +import { HttpHeaders } from '@angular/common/http'; + +import { merge as observableMerge, Observable, of as observableOf } from 'rxjs'; +import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators'; + +import { DataService } from '../data/data.service'; +import { DeleteRequest, FindAllOptions, PostRequest, TaskDeleteRequest, TaskPostRequest } from '../data/request.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { ProcessTaskResponse } from './models/process-task-response'; +import { RemoteDataError } from '../data/remote-data-error'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { getResponseFromEntry } from '../shared/operators'; +import { ErrorResponse, MessageResponse, RestResponse } from '../cache/response.models'; +import { CacheableObject } from '../cache/object-cache.reducer'; + +export abstract class TasksService<TNormalized extends NormalizedObject, TDomain extends CacheableObject> extends DataService<TNormalized, TDomain> { + + public getBrowseEndpoint(options: FindAllOptions): Observable<string> { + return this.halService.getEndpoint(this.linkPath); + } + + protected fetchRequest(requestId: string): Observable<ProcessTaskResponse> { + const responses = this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + const errorResponses = responses.pipe( + filter((response: RestResponse) => !response.isSuccessful), + mergeMap((response: ErrorResponse) => observableOf( + new ProcessTaskResponse( + response.isSuccessful, + new RemoteDataError(response.statusCode, response.statusText, response.errorMessage) + )) + )); + const successResponses = responses.pipe( + filter((response: RestResponse) => response.isSuccessful), + map((response: MessageResponse) => new ProcessTaskResponse(response.isSuccessful)), + distinctUntilChanged() + ); + return observableMerge(errorResponses, successResponses); + } + + protected getEndpointByIDHref(endpoint, resourceID): string { + return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; + } + + protected getEndpointByMethod(endpoint: string, method: string): string { + return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; + } + + public postToEndpoint(linkPath: string, body: any, scopeId?: string, options?: HttpOptions): Observable<ProcessTaskResponse> { + const requestId = this.requestService.generateRequestId(); + return this.halService.getEndpoint(linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)), + distinctUntilChanged(), + map((endpointURL: string) => new TaskPostRequest(requestId, endpointURL, body, options)), + tap((request: PostRequest) => this.requestService.configure(request)), + flatMap((request: PostRequest) => this.fetchRequest(requestId)), + distinctUntilChanged()); + } + + public deleteById(linkName: string, scopeId: string, options?: HttpOptions): Observable<ProcessTaskResponse> { + const requestId = this.requestService.generateRequestId(); + return this.halService.getEndpoint(linkName || this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)), + map((endpointURL: string) => new TaskDeleteRequest(requestId, endpointURL, null, options)), + tap((request: DeleteRequest) => this.requestService.configure(request)), + flatMap((request: DeleteRequest) => this.fetchRequest(requestId)), + distinctUntilChanged()); + } + + protected makeHttpOptions() { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); + options.headers = headers; + return options; + } +}