diff --git a/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java b/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java index 91bccbfdc..585d2b207 100644 --- a/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java +++ b/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java @@ -308,4 +308,14 @@ public class ErrorThesaurusProperties { public void setImportDescriptionWithoutDmpDescriptionTemplate(ErrorDescription importDescriptionWithoutDmpDescriptionTemplate) { this.importDescriptionWithoutDmpDescriptionTemplate = importDescriptionWithoutDmpDescriptionTemplate; } + + private ErrorDescription duplicateDmpUser; + + public ErrorDescription getDuplicateDmpUser() { + return duplicateDmpUser; + } + + public void setDuplicateDmpUser(ErrorDescription duplicateDmpUser) { + this.duplicateDmpUser = duplicateDmpUser; + } } diff --git a/backend/core/src/main/java/org/opencdmp/service/dmp/DmpServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/dmp/DmpServiceImpl.java index 420f4648a..d2456cd1b 100644 --- a/backend/core/src/main/java/org/opencdmp/service/dmp/DmpServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/dmp/DmpServiceImpl.java @@ -768,7 +768,9 @@ public class DmpServiceImpl implements DmpService { this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.dmpAffiliation(dmpId)), Permission.AssignDmpUsers); if (!disableDelete && (model == null || model.stream().noneMatch(x-> x.getUser() != null && DmpUserRole.Owner.equals(x.getRole())))) throw new MyApplicationException("At least one owner required"); - + + this.checkDuplicateDmpUser(model); + DmpEntity dmpEntity = this.entityManager.find(DmpEntity.class, dmpId, true); if (dmpEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{dmpId, Dmp.class.getSimpleName()}, LocaleContextHolder.getLocale())); @@ -812,6 +814,20 @@ public class DmpServiceImpl implements DmpService { return this.builderFactory.builder(DmpUserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fieldSet, DmpUser._id, DmpUser._hash), persisted); } + private void checkDuplicateDmpUser(List model){ + for (DmpUserPersist user: model) { + List duplicateUser = null; + if (user.getUser() != null){ + duplicateUser = model.stream().filter(x -> x.getUser().equals(user.getUser()) && x.getRole().equals(user.getRole()) && Objects.equals(user.getSectionId(), x.getSectionId())).collect(Collectors.toList()); + } else { + duplicateUser = model.stream().filter(x -> x.getEmail().equals(user.getEmail()) && x.getRole().equals(user.getRole()) && Objects.equals(user.getSectionId(), x.getSectionId())).collect(Collectors.toList()); + } + if (duplicateUser.size() > 1) { + throw new MyValidationException(this.errors.getDuplicateDmpUser().getCode(), this.errors.getDuplicateDmpUser().getMessage()); + } + } + } + @Override public Dmp removeUser(DmpUserRemovePersist model, FieldSet fields) throws InvalidApplicationException, IOException { this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.dmpAffiliation(model.getDmpId())), Permission.AssignDmpUsers); diff --git a/backend/web/src/main/resources/config/errors.yml b/backend/web/src/main/resources/config/errors.yml index 8beb29beb..25d6f3a6e 100644 --- a/backend/web/src/main/resources/config/errors.yml +++ b/backend/web/src/main/resources/config/errors.yml @@ -97,4 +97,7 @@ error-thesaurus: message: missing contact info for this user import-description-without-dmp-description-template: code: 136 - message: Error creating description without dmp description template \ No newline at end of file + message: Error creating description without dmp description template + duplicate-dmp-user: + code: 137 + message: Duplicate Dmp User not allowed \ No newline at end of file diff --git a/dmp-frontend/src/app/core/common/enum/respone-error-code.ts b/dmp-frontend/src/app/core/common/enum/respone-error-code.ts index c9403a864..d9fd3f4c6 100644 --- a/dmp-frontend/src/app/core/common/enum/respone-error-code.ts +++ b/dmp-frontend/src/app/core/common/enum/respone-error-code.ts @@ -34,6 +34,7 @@ export enum ResponseErrorCode { DmpInactiveUser = 134, DmpMissingUserContactInfo = 135, ImportDescriptionWithoutDmpDescriptionTemplate = 136, + DuplicateDmpUser = 137, // Notification & Annotation Errors InvalidApiKey = 200, @@ -116,6 +117,8 @@ export class ResponseErrorCodeHelper { return language.instant("GENERAL.BACKEND-ERRORS.IMPORT-DESCRIPTION-WITHOUT-DMP-DESCRIPTION-TEMPLATE"); case ResponseErrorCode.InvalidApiKey: return language.instant("GENERAL.BACKEND-ERRORS.INVALID-API-KEY"); + case ResponseErrorCode.DuplicateDmpUser: + return language.instant("GENERAL.BACKEND-ERRORS.DUPLICATE-DMP-USER"); case ResponseErrorCode.StaleApiKey: return language.instant("GENERAL.BACKEND-ERRORS.STALE-API-KEY"); case ResponseErrorCode.SensitiveInfo: diff --git a/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.html b/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.html index cd2bd846e..3a86701b1 100644 --- a/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.html +++ b/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.html @@ -12,7 +12,7 @@
- + {{formGroup.get('users').getError('backendError').message}} {{'GENERAL.VALIDATION.REQUIRED' | translate}}
diff --git a/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.ts b/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.ts index be8e77237..5fed32cd6 100644 --- a/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.ts +++ b/dmp-frontend/src/app/ui/dmp/invitation/dialog/dmp-invitation-dialog.component.ts @@ -19,11 +19,14 @@ import { takeUntil } from 'rxjs/operators'; import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint'; import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { FormService } from '@common/forms/form-service'; +import { DmpEditorService } from '../../dmp-editor-blueprint/dmp-editor.service'; @Component({ selector: 'app-invitation-dialog-component', templateUrl: 'dmp-invitation-dialog.component.html', styleUrls: ['./dmp-invitation-dialog.component.scss'], + providers: [DmpEditorService] }) export class DmpInvitationDialogComponent extends BaseComponent implements OnInit { @@ -32,6 +35,7 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni formGroup: UntypedFormGroup; dmpUserRoleEnum = DmpUserRole; selectedBlueprint: DmpBlueprint; + inProgressSendButton = false; readonly separatorKeysCodes: number[] = [ENTER, COMMA]; constructor( @@ -43,8 +47,8 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni private uiNotificationService: UiNotificationService, private httpErrorHandlingService: HttpErrorHandlingService, private dmpService: DmpService, - private userService: UserService, - private filterService: FilterService, + private formService: FormService, + private dmpEditorService: DmpEditorService, @Inject(MAT_DIALOG_DATA) public data: any ) { super(); @@ -55,10 +59,15 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni ngOnInit() { this.formGroup = this.editorModel.buildForm(); + this.dmpEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); } send() { + this.formService.removeAllBackEndErrors(this.formGroup.get("users")); + this.formService.touchAllFormFields(this.formGroup.get("users")); + if (!this.formGroup.get("users").valid) { return; } + this.inProgressSendButton = true; const userFormData = this.formGroup.get("users").value as DmpUserPersist[]; this.dmpService.inviteUsers(this.dmpId, {users: userFormData}) @@ -85,6 +94,7 @@ export class DmpInvitationDialogComponent extends BaseComponent implements OnIni } onCallbackError(errorResponse: HttpErrorResponse) { + this.inProgressSendButton = false; let errorOverrides = new Map(); errorOverrides.set(-1, this.language.instant('DMP-USER-INVITATION-DIALOG.ERROR')); this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error); diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 95c6e559a..e63013ee4 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -69,7 +69,8 @@ "DESCRIPTION-TEMPLATE-INACTIVE-USER": "This description template contains users that are not exist", "DESCRIPTION-TEMPLATE-MISSING-USER-CONTACT-INFO": "This description template contains users that don't have contact info", "DMP-INACTIVE-USER": "This plan contains users that are not exist", - "DMP-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info" + "DMP-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", + "DUPLICATE-DMP-USER": "You can't invite authors with same role and plan section more than once" }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Warning!",