Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions scripts/sync-i18n-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ function parseCliInput() {
.option('-o, --output-file <output>', 'where output of script ends up; mutually exclusive with -i')
.usage('([-d <output-dir>] [-s <source-file>]) || (-t <target-file> (-i | -o <output>) [-s <source-file>])')
.parse(process.argv);

const sourceFile = program.opts().sourceFile;

if (!program.targetFile) {
if (!program.targetFile) {
fs.readdirSync(projectRoot(LANGUAGE_FILES_LOCATION)).forEach(file => {
if (!sourceFile.toString().endsWith(file)) {
if (program.opts().sourceFile && !program.opts().sourceFile.toString().endsWith(file)) {
const targetFileLocation = projectRoot(LANGUAGE_FILES_LOCATION + "/" + file);
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + sourceFile);
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + program.opts().sourceFile);
if (program.outputDir) {
if (!fs.existsSync(program.outputDir)) {
fs.mkdirSync(program.outputDir);
Expand All @@ -69,7 +66,7 @@ function parseCliInput() {
console.log(program.outputHelp());
process.exit(1);
}
if (!checkIfFileExists(sourceFile)) {
if (!checkIfFileExists(program.opts().sourceFile)) {
console.error('Path of source file is not valid.');
console.log(program.outputHelp());
process.exit(1);
Expand Down
91 changes: 91 additions & 0 deletions src/app/core/data/item-data.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
import { Store } from '@ngrx/store';
import {
cold,
Expand All @@ -13,6 +14,7 @@ import { RestResponse } from '../cache/response.models';
import { CoreState } from '../core-state.model';
import { NotificationsService } from '../notification-system/notifications.service';
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { Item } from '../shared/item.model';
import { HALEndpointServiceStub } from '../testing/hal-endpoint-service.stub';
import { getMockRemoteDataBuildService } from '../testing/remote-data-build.service.mock';
import { getMockRequestService } from '../testing/request.service.mock';
Expand Down Expand Up @@ -209,4 +211,93 @@ describe('ItemDataService', () => {
});
});

describe('findByCustomUrl', () => {
let itemDataService: ItemDataService;
let searchData: any;
let findByHrefSpy: jasmine.Spy;
let getSearchByHrefSpy: jasmine.Spy;
const id = 'custom-id';
const fakeHrefObs = of('https://rest.api/core/items/search/findByCustomURL?q=custom-id');
const linksToFollow = [];
const projections = ['full', 'detailed'];

beforeEach(() => {
searchData = jasmine.createSpyObj('searchData', ['getSearchByHref']);
getSearchByHrefSpy = searchData.getSearchByHref.and.returnValue(fakeHrefObs);
itemDataService = new ItemDataService(
requestService,
rdbService,
objectCache,
halEndpointService,
notificationsService,
comparator,
browseService,
bundleService,
);

(itemDataService as any).searchData = searchData;
findByHrefSpy = spyOn(itemDataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
});

it('should call searchData.getSearchByHref with correct parameters', () => {
itemDataService.findByCustomUrl(id, true, true, linksToFollow, projections).subscribe();

expect(getSearchByHrefSpy).toHaveBeenCalledWith(
'findByCustomURL',
jasmine.objectContaining({
searchParams: jasmine.arrayContaining([
jasmine.objectContaining({ fieldName: 'q', fieldValue: id }),
jasmine.objectContaining({ fieldName: 'projection', fieldValue: 'full' }),
jasmine.objectContaining({ fieldName: 'projection', fieldValue: 'detailed' }),
]),
}),
...linksToFollow,
);
});

it('should call findByHref with the href observable returned from getSearchByHref', () => {
itemDataService.findByCustomUrl(id, true, false, linksToFollow, projections).subscribe();

expect(findByHrefSpy).toHaveBeenCalledWith(fakeHrefObs, true, false, ...linksToFollow);
});
});

describe('findById', () => {
let itemDataService: ItemDataService;

beforeEach(() => {
itemDataService = new ItemDataService(
requestService,
rdbService,
objectCache,
halEndpointService,
notificationsService,
comparator,
browseService,
bundleService,
);
spyOn(itemDataService, 'findByCustomUrl').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
spyOn(itemDataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
spyOn(itemDataService as any, 'getIDHrefObs').and.returnValue(of('uuid-href'));
});

it('should call findByHref when given a valid UUID', () => {
const validUuid = '4af28e99-6a9c-4036-a199-e1b587046d39';
itemDataService.findById(validUuid).subscribe();

expect((itemDataService as any).getIDHrefObs).toHaveBeenCalledWith(encodeURIComponent(validUuid));
expect(itemDataService.findByHref).toHaveBeenCalled();
expect(itemDataService.findByCustomUrl).not.toHaveBeenCalled();
});

it('should call findByCustomUrl when given a non-UUID id', () => {
const nonUuid = 'custom-url';
itemDataService.findById(nonUuid).subscribe();

expect(itemDataService.findByCustomUrl).toHaveBeenCalledWith(nonUuid, true, true, []);
expect(itemDataService.findByHref).not.toHaveBeenCalled();
});
});


});
56 changes: 56 additions & 0 deletions src/app/core/data/item-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
switchMap,
take,
} from 'rxjs/operators';
import { validate as uuidValidate } from 'uuid';

import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
Expand All @@ -34,6 +35,7 @@ import { NotificationsService } from '../notification-system/notifications.servi
import { Bundle } from '../shared/bundle.model';
import { Collection } from '../shared/collection.model';
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { FollowLinkConfig } from '../shared/follow-link-config.model';
import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
Expand All @@ -58,6 +60,7 @@ import {
PatchData,
PatchDataImpl,
} from './base/patch-data';
import { SearchDataImpl } from './base/search-data';
import { BundleDataService } from './bundle-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindListOptions } from './find-list-options.model';
Expand All @@ -83,6 +86,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
private createData: CreateData<Item>;
private patchData: PatchData<Item>;
private deleteData: DeleteData<Item>;
private searchData: SearchDataImpl<Item>;

protected constructor(
protected linkPath,
Expand All @@ -101,6 +105,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.patchData = new PatchDataImpl<Item>(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}

/**
Expand Down Expand Up @@ -425,6 +430,57 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
return this.createData.create(object, ...params);
}

/**
* Returns an observable of {@link RemoteData} of an object, based on its CustomURL or ID, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param id CustomUrl or UUID of object we want to retrieve
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @param projections List of {@link projections} used to pass as parameters
*/
public findByCustomUrl(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, linksToFollow: FollowLinkConfig<Item>[], projections: string[] = []): Observable<RemoteData<Item>> {
const searchHref = 'findByCustomURL';

const options = Object.assign({}, {
searchParams: [
new RequestParam('q', id),
],
});

projections.forEach((projection) => {
options.searchParams.push(new RequestParam('projection', projection));
});

const hrefObs = this.searchData.getSearchByHref(searchHref, options, ...linksToFollow);

return this.findByHref(hrefObs, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}

/**
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
* @param id ID of object we want to retrieve
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
public findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {

if (uuidValidate(id)) {
const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} else {
return this.findByCustomUrl(id, useCachedVersionIfAvailable, reRequestOnStale, linksToFollow);
}
}

}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/provide-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeEnvironmentProviders,
} from '@angular/core';
import { APP_CONFIG } from '@dspace/config/app-config.interface';
import { SubmissionCustomUrl } from '@dspace/core/submission/models/submission-custom-url.model';

import { AuthStatus } from './auth/models/auth-status.model';
import { ShortLivedToken } from './auth/models/short-lived-token.model';
Expand Down Expand Up @@ -228,4 +229,5 @@ export const models =
StatisticsEndpoint,
CorrectionType,
SupervisionOrder,
SubmissionCustomUrl,
];
9 changes: 8 additions & 1 deletion src/app/core/router/utils/dso-route.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ export function getCommunityPageRoute(communityId: string) {
*/
export function getItemPageRoute(item: Item) {
const type = item.firstMetadataValue('dspace.entity.type');
return getEntityPageRoute(type, item.uuid);
let url = item.uuid;

if (isNotEmpty(item.metadata) && item.hasMetadata('dspace.customurl')) {
url = item.firstMetadataValue('dspace.customurl');
}

return getEntityPageRoute(type, url);
}


export function getEntityPageRoute(entityType: string, itemId: string) {
if (isNotEmpty(entityType)) {
return new URLCombiner(`/${ENTITY_MODULE_PATH}`, encodeURIComponent(entityType.toLowerCase()), itemId).toString();
Expand Down
27 changes: 27 additions & 0 deletions src/app/core/submission/models/submission-custom-url.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
autoserialize,
inheritSerialization,
} from 'cerialize';

import { typedObject } from '../../cache/builders/build-decorators';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { SUBMISSION_CUSTOM_URL } from './submission-custom-url.resource-type';

@typedObject
@inheritSerialization(HALResource)
export class SubmissionCustomUrl extends HALResource {

static type = SUBMISSION_CUSTOM_URL;

/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;

@autoserialize
url: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';

/**
* The resource type for License
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SUBMISSION_CUSTOM_URL = new ResourceType('submissioncustomcurl');
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* An interface to represent the submission's custom url section data.
*/
export interface WorkspaceitemSectionCustomUrlObject {
'redirected-urls': string[];
'url': string;
}
1 change: 1 addition & 0 deletions src/app/core/submission/sections-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum SectionsType {
Upload = 'upload',
License = 'license',
CcLicense = 'cclicense',
CustomUrl = 'custom-url',
AccessesCondition = 'accessCondition',
SherpaPolicies = 'sherpaPolicy',
Identifiers = 'identifiers',
Expand Down
1 change: 1 addition & 0 deletions src/app/core/submission/submission-scope-type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum SubmissionScopeType {
WorkspaceItem = 'WORKSPACE',
WorkflowItem = 'WORKFLOW',
EditItem = 'EDIT'
}
68 changes: 68 additions & 0 deletions src/app/item-page/item-page.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,72 @@ describe('itemPageResolver', () => {
});

});

describe('when item has dspace.customurl metadata', () => {


const customUrl = 'my-custom-item';
let resolver: any;
let itemService: any;
let store: any;
let router: Router;
let authService: AuthServiceStub;

const uuid = '1234-65487-12354-1235';
let item: DSpaceObject;

beforeEach(() => {
router = TestBed.inject(Router);
item = Object.assign(new DSpaceObject(), {
uuid: uuid,
firstMetadataValue(_keyOrKeys: string | string[], _valueFilter?: MetadataValueFilter): string {
return _keyOrKeys === 'dspace.entity.type' ? 'person' : customUrl;
},
hasMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): boolean {
return true;
},
metadata: {
'dspace.customurl': customUrl,
},
});
itemService = {
findById: (_id: string) => createSuccessfulRemoteDataObject$(item),
};
store = jasmine.createSpyObj('store', {
dispatch: {},
});
authService = new AuthServiceStub();
resolver = itemPageResolver;
});

it('should navigate to the new custom URL if dspace.customurl is defined and different from route param', (done) => {
spyOn(router, 'navigateByUrl').and.callThrough();

const route = { params: { id: uuid } } as any;
const state = { url: `/entities/person/${uuid}` } as any;

resolver(route, state, router, itemService, store, authService)
.pipe(first())
.subscribe((rd: any) => {
const expectedUrl = `/entities/person/${customUrl}`;
expect(router.navigateByUrl).toHaveBeenCalledWith(expectedUrl);
done();
});
});

it('should not navigate if dspace.customurl matches the current route id', (done) => {
spyOn(router, 'navigateByUrl').and.callThrough();

const route = { params: { id: customUrl } } as any;
const state = { url: `/entities/person/${customUrl}` } as any;

resolver(route, state, router, itemService, store, authService)
.pipe(first())
.subscribe((rd: any) => {
expect(router.navigateByUrl).not.toHaveBeenCalled();
done();
});
});
});

});
Loading
Loading