Skip to content

Commit 0f7965e

Browse files
committed
Merge remote-tracking branch 'github/main' into coar-notify-7-part-two
2 parents add1257 + f6f3962 commit 0f7965e

29 files changed

+968
-52
lines changed

config/config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ submission:
131131
# NOTE: after how many time (milliseconds) submission is saved automatically
132132
# eg. timer: 5 * (1000 * 60); // 5 minutes
133133
timer: 0
134+
# Always show the duplicate detection section if enabled, even if there are no potential duplicates detected
135+
# (a message will be displayed to indicate no matches were found)
136+
duplicateDetection:
137+
alwaysShowSection: false
134138
icons:
135139
metadata:
136140
# NOTE: example of configuration

src/app/core/core.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-s
199199
import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters';
200200
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
201201
import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
202+
import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service';
202203

203204
/**
204205
* When not in production, endpoint responses can be mocked for testing purposes
@@ -235,6 +236,7 @@ const PROVIDERS = [
235236
HALEndpointService,
236237
HostWindowService,
237238
ItemDataService,
239+
SubmissionDuplicateDataService,
238240
MetadataService,
239241
ObjectCacheService,
240242
PaginationComponentOptions,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Object model for the data returned by the REST API to present potential duplicates in a submission section
3+
*/
4+
import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model';
5+
6+
export interface WorkspaceitemSectionDuplicatesObject {
7+
potentialDuplicates?: Duplicate[]
8+
}

src/app/core/submission/models/workspaceitem-sections.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod
33
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
44
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
55
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model';
6-
import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model';
6+
import { WorkspaceitemSectionIdentifiersObject } from './workspaceitem-section-identifiers.model';
77
import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
8+
import { WorkspaceitemSectionDuplicatesObject } from './workspaceitem-section-duplicates.model';
89

910
/**
1011
* An interface to represent submission's section object.
@@ -25,6 +26,7 @@ export type WorkspaceitemSectionDataType
2526
| WorkspaceitemSectionAccessesObject
2627
| WorkspaceitemSectionSherpaPoliciesObject
2728
| WorkspaceitemSectionIdentifiersObject
29+
| WorkspaceitemSectionDuplicatesObject
2830
| string;
2931

3032

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { SubmissionDuplicateDataService } from './submission-duplicate-data.service';
2+
import { FindListOptions } from '../data/find-list-options.model';
3+
import { RequestParam } from '../cache/models/request-param.model';
4+
5+
/**
6+
* Basic tests for the submission-duplicate-data.service.ts service
7+
*/
8+
describe('SubmissionDuplicateDataService', () => {
9+
const duplicateDataService = new SubmissionDuplicateDataService(null, null, null, null);
10+
11+
// Test the findDuplicates method to make sure that a call results in an expected
12+
// call to searchBy, using the 'findByItem' search method
13+
describe('findDuplicates', () => {
14+
beforeEach(() => {
15+
spyOn(duplicateDataService, 'searchBy');
16+
});
17+
18+
it('should call searchBy with the correct arguments', () => {
19+
// Set up expected search parameters and find options
20+
const searchParams = [];
21+
searchParams.push(new RequestParam('uuid', 'test'));
22+
let findListOptions = new FindListOptions();
23+
findListOptions.searchParams = searchParams;
24+
// Perform test search using uuid 'test' using the findDuplicates method
25+
const result = duplicateDataService.findDuplicates('test', new FindListOptions(), true, true);
26+
// Expect searchBy('findByItem'...) to have been used as SearchData impl with the expected options (uuid=test)
27+
expect(duplicateDataService.searchBy).toHaveBeenCalledWith('findByItem', findListOptions, true, true);
28+
});
29+
});
30+
});
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/* eslint-disable max-classes-per-file */
2+
import { Observable } from 'rxjs';
3+
import { Injectable } from '@angular/core';
4+
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
5+
import { ResponseParsingService } from '../data/parsing.service';
6+
import { RemoteData } from '../data/remote-data';
7+
import { GetRequest } from '../data/request.models';
8+
import { RequestService } from '../data/request.service';
9+
import { GenericConstructor } from '../shared/generic-constructor';
10+
import { HALEndpointService } from '../shared/hal-endpoint.service';
11+
import { SearchResponseParsingService } from '../data/search-response-parsing.service';
12+
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
13+
import { RestRequest } from '../data/rest-request.model';
14+
import { BaseDataService } from '../data/base/base-data.service';
15+
import { FindListOptions } from '../data/find-list-options.model';
16+
import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model';
17+
import { PaginatedList } from '../data/paginated-list.model';
18+
import { RequestParam } from '../cache/models/request-param.model';
19+
import { ObjectCacheService } from '../cache/object-cache.service';
20+
import { SearchData, SearchDataImpl } from '../data/base/search-data';
21+
import { DUPLICATE } from '../../shared/object-list/duplicate-data/duplicate.resource-type';
22+
import { dataService } from '../data/base/data-service.decorator';
23+
24+
25+
/**
26+
* Service that handles search requests for potential duplicate items.
27+
* This uses the /api/submission/duplicates endpoint to look for other archived or in-progress items (if user
28+
* has READ permission) that match the item (for the given uuid).
29+
* Matching is configured in the backend in dspace/config/modulesduplicate-detection.cfg
30+
* The returned results are small preview 'stubs' of items, and displayed in either a submission section
31+
* or the workflow pooled/claimed task page.
32+
*
33+
*/
34+
@Injectable()
35+
@dataService(DUPLICATE)
36+
export class SubmissionDuplicateDataService extends BaseDataService<Duplicate> implements SearchData<Duplicate> {
37+
38+
/**
39+
* The ResponseParsingService constructor name
40+
*/
41+
private parser: GenericConstructor<ResponseParsingService> = SearchResponseParsingService;
42+
43+
/**
44+
* The RestRequest constructor name
45+
*/
46+
private request: GenericConstructor<RestRequest> = GetRequest;
47+
48+
/**
49+
* SearchData interface to implement
50+
* @private
51+
*/
52+
private searchData: SearchData<Duplicate>;
53+
54+
/**
55+
* Subscription to unsubscribe from
56+
*/
57+
private sub;
58+
59+
constructor(
60+
protected requestService: RequestService,
61+
protected rdbService: RemoteDataBuildService,
62+
protected objectCache: ObjectCacheService,
63+
protected halService: HALEndpointService,
64+
) {
65+
super('duplicates', requestService, rdbService, objectCache, halService);
66+
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
67+
}
68+
69+
/**
70+
* Implement the searchBy method to return paginated lists of Duplicate resources
71+
*
72+
* @param searchMethod the search method name
73+
* @param options find list options
74+
* @param useCachedVersionIfAvailable whether to use cached version if available
75+
* @param reRequestOnStale whether to rerequest results on stale
76+
* @param linksToFollow links to follow in results
77+
*/
78+
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
79+
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
80+
}
81+
82+
/**
83+
* Helper method to get the duplicates endpoint
84+
* @protected
85+
*/
86+
protected getEndpoint(): Observable<string> {
87+
return this.halService.getEndpoint(this.linkPath);
88+
}
89+
90+
/**
91+
* Method to set service options
92+
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
93+
* @param {boolean} request The RestRequest constructor name
94+
*/
95+
setServiceOptions(parser: GenericConstructor<ResponseParsingService>, request: GenericConstructor<RestRequest>) {
96+
if (parser) {
97+
this.parser = parser;
98+
}
99+
if (request) {
100+
this.request = request;
101+
}
102+
}
103+
104+
/**
105+
* Find duplicates for a given item UUID. Locates and returns results from the /api/submission/duplicates/search/findByItem
106+
* SearchRestMethod, which is why this implements SearchData<Duplicate> and searchBy
107+
*
108+
* @param uuid the item UUID
109+
* @param options any find list options e.g. paging
110+
* @param useCachedVersionIfAvailable whether to use cached version if available
111+
* @param reRequestOnStale whether to rerequest results on stale
112+
* @param linksToFollow links to follow in results
113+
*/
114+
public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
115+
const searchParams = [new RequestParam('uuid', uuid)];
116+
let findListOptions = new FindListOptions();
117+
if (options) {
118+
findListOptions = Object.assign(new FindListOptions(), options);
119+
}
120+
if (findListOptions.searchParams) {
121+
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
122+
} else {
123+
findListOptions.searchParams = searchParams;
124+
}
125+
126+
// Return actual search/findByItem results
127+
return this.searchBy('findByItem', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
128+
129+
}
130+
131+
/**
132+
* Unsubscribe from the subscription
133+
*/
134+
ngOnDestroy(): void {
135+
if (this.sub !== undefined) {
136+
this.sub.unsubscribe();
137+
}
138+
}
139+
}

src/app/shared/mocks/submission.mock.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,10 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, {
11141114
isLoading: false,
11151115
isValid: false,
11161116
removePending: false
1117-
} as any
1117+
} as any,
1118+
'duplicates': {
1119+
potentialDuplicates: []
1120+
} as any,
11181121
},
11191122
isLoading: false,
11201123
savePending: false,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {autoserialize, deserialize} from 'cerialize';
2+
import { MetadataMap } from '../../../core/shared/metadata.models';
3+
import { HALLink} from '../../../core/shared/hal-link.model';
4+
import { CacheableObject } from '../../../core/cache/cacheable-object.model';
5+
import { DUPLICATE } from './duplicate.resource-type';
6+
import { ResourceType } from '../../../core/shared/resource-type';
7+
8+
/**
9+
* This implements the model of a duplicate preview stub, to be displayed to submitters or reviewers
10+
* if duplicate detection is enabled. The metadata map is configurable in the backend at duplicate-detection.cfg
11+
*/
12+
export class Duplicate implements CacheableObject {
13+
14+
static type = DUPLICATE;
15+
16+
/**
17+
* The item title
18+
*/
19+
@autoserialize
20+
title: string;
21+
/**
22+
* The item uuid
23+
*/
24+
@autoserialize
25+
uuid: string;
26+
/**
27+
* The workfow item ID, if any
28+
*/
29+
@autoserialize
30+
workflowItemId: number;
31+
/**
32+
* The workspace item ID, if any
33+
*/
34+
@autoserialize
35+
workspaceItemId: number;
36+
/**
37+
* The owning collection of the item
38+
*/
39+
@autoserialize
40+
owningCollection: string;
41+
/**
42+
* Metadata for the preview item (e.g. dc.title)
43+
*/
44+
@autoserialize
45+
metadata: MetadataMap;
46+
47+
@autoserialize
48+
type: ResourceType;
49+
50+
/**
51+
* The {@link HALLink}s for the URL that generated this item (in context of search results)
52+
*/
53+
@deserialize
54+
_links: {
55+
self: HALLink;
56+
};
57+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResourceType } from 'src/app/core/shared/resource-type';
2+
3+
/**
4+
* The resource type for Duplicate preview stubs
5+
*
6+
* Needs to be in a separate file to prevent circular
7+
* dependencies in webpack.
8+
*/
9+
export const DUPLICATE = new ResourceType('duplicate');

src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@
44
[showSubmitter]="showSubmitter"
55
[badgeContext]="badgeContext"
66
[workflowItem]="workflowitem$.value"></ds-themed-item-list-preview>
7-
7+
<!-- Display duplicate alert, if feature enabled and duplicates detected -->
8+
<ng-container *ngVar="(duplicates$|async)?.length as duplicateCount">
9+
<div [ngClass]="'row'" *ngIf="duplicateCount > 0">
10+
<div [ngClass]="'col-2'"></div>
11+
<div [ngClass]="'col-10'">
12+
<div class="d-flex alert alert-warning w-100">
13+
{{ duplicateCount }} {{ 'submission.workflow.tasks.duplicates' | translate }}
14+
</div>
15+
</div>
16+
</div>
17+
</ng-container>
818
<div class="row">
919
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
1020
<ds-claimed-task-actions [item]="item$.value"

0 commit comments

Comments
 (0)