Add a new Lock Table that will prevent two or more users to edit simultaneously a single DMP or Dataset (ref #240)

This commit is contained in:
George Kalampokis 2020-02-11 18:27:54 +02:00
parent ccea83b4d4
commit b62c0f7ff5
26 changed files with 834 additions and 26 deletions

View File

@ -0,0 +1,38 @@
package eu.eudat.data.dao.criteria;
import eu.eudat.data.entities.Lock;
import eu.eudat.data.entities.UserInfo;
import java.util.Date;
import java.util.UUID;
public class LockCriteria extends Criteria<Lock> {
private UUID target;
private UserInfo lockedBy;
private Date touchedAt;
public UUID getTarget() {
return target;
}
public void setTarget(UUID target) {
this.target = target;
}
public UserInfo getLockedBy() {
return lockedBy;
}
public void setLockedBy(UserInfo lockedBy) {
this.lockedBy = lockedBy;
}
public Date getTouchedAt() {
return touchedAt;
}
public void setTouchedAt(Date touchedAt) {
this.touchedAt = touchedAt;
}
}

View File

@ -0,0 +1,13 @@
package eu.eudat.data.dao.entities;
import eu.eudat.data.dao.DatabaseAccessLayer;
import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.data.entities.Lock;
import eu.eudat.queryable.QueryableList;
import java.util.UUID;
public interface LockDao extends DatabaseAccessLayer<Lock, UUID> {
QueryableList<Lock> getWithCriteria(LockCriteria criteria);
}

View File

@ -0,0 +1,65 @@
package eu.eudat.data.dao.entities;
import eu.eudat.data.dao.DatabaseAccess;
import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.data.dao.databaselayer.service.DatabaseService;
import eu.eudat.data.entities.Lock;
import eu.eudat.queryable.QueryableList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Service("LockDao")
public class LockDaoImpl extends DatabaseAccess<Lock> implements LockDao {
@Autowired
public LockDaoImpl(DatabaseService<Lock> databaseService) {
super(databaseService);
}
@Override
public QueryableList<Lock> getWithCriteria(LockCriteria criteria) {
QueryableList<Lock> query = this.getDatabaseService().getQueryable(Lock.class);
if (criteria.getTouchedAt() != null)
query.where((builder, root) -> builder.equal(root.get("touchedAt"), criteria.getTouchedAt()));
if (criteria.getLockedBy() != null)
query.where(((builder, root) -> builder.equal(root.get("lockedBy"), criteria.getLockedBy())));
if (criteria.getTarget() != null)
query.where(((builder, root) -> builder.equal(root.get("target"), criteria.getTarget())));
return query;
}
@Override
public Lock createOrUpdate(Lock item) {
return this.getDatabaseService().createOrUpdate(item, Lock.class);
}
@Async
@Override
public CompletableFuture<Lock> createOrUpdateAsync(Lock item) {
return CompletableFuture.supplyAsync(() -> this.createOrUpdate(item));
}
@Override
public Lock find(UUID id) {
return this.getDatabaseService().getQueryable(Lock.class).where(((builder, root) -> builder.equal(root.get("id"), id))).getSingle();
}
@Override
public Lock find(UUID id, String hint) {
throw new UnsupportedOperationException();
}
@Override
public void delete(Lock item) {
this.getDatabaseService().delete(item);
}
@Override
public QueryableList<Lock> asQueryable() {
return this.getDatabaseService().getQueryable(Lock.class);
}
}

View File

@ -0,0 +1,95 @@
package eu.eudat.data.entities;
import eu.eudat.data.converters.DateToUTCConverter;
import eu.eudat.data.entities.helpers.EntityBinder;
import eu.eudat.queryable.queryableentity.DataEntity;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "\"Lock\"")
public class Lock implements DataEntity<Lock, UUID> {
@Id
@GeneratedValue
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(name = "id", updatable = false, nullable = false, columnDefinition = "BINARY(16)")
private UUID id;
@Column(name = "\"Target\"", nullable = false)
private UUID target;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "\"LockedBy\"", nullable = false)
private UserInfo lockedBy;
@Column(name = "\"LockedAt\"")
@Convert(converter = DateToUTCConverter.class)
private Date lockedAt = new Date();
@Column(name = "\"TouchedAt\"")
@Convert(converter = DateToUTCConverter.class)
private Date touchedAt = null;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public UUID getTarget() {
return target;
}
public void setTarget(UUID target) {
this.target = target;
}
public UserInfo getLockedBy() {
return lockedBy;
}
public void setLockedBy(UserInfo lockedBy) {
this.lockedBy = lockedBy;
}
public Date getLockedAt() {
return lockedAt;
}
public void setLockedAt(Date lockedAt) {
this.lockedAt = lockedAt;
}
public Date getTouchedAt() {
return touchedAt;
}
public void setTouchedAt(Date touchedAt) {
this.touchedAt = touchedAt;
}
@Override
public void update(Lock entity) {
this.touchedAt = entity.touchedAt;
}
@Override
public UUID getKeys() {
return this.id;
}
@Override
public Lock buildFromTuple(List<Tuple> tuple, List<String> fields, String base) {
String currentBase = base.isEmpty() ? "" : base + ".";
if (fields.contains(currentBase + "id")) this.id = EntityBinder.fromTuple(tuple, currentBase + "id");
return this;
}
}

View File

@ -69,6 +69,9 @@ public class UserInfo implements DataEntity<UserInfo, UUID> {
@OneToMany(mappedBy = "userInfo", fetch = FetchType.LAZY)
private Set<UserRole> userRoles = new HashSet<>();
@OneToMany(mappedBy = "lockedBy", fetch = FetchType.LAZY)
private Set<Lock> locks = new HashSet<>();
public Set<DMP> getDmps() {
return dmps;
}
@ -165,6 +168,14 @@ public class UserInfo implements DataEntity<UserInfo, UUID> {
this.userRoles = userRoles;
}
public Set<Lock> getLocks() {
return locks;
}
public void setLocks(Set<Lock> locks) {
this.locks = locks;
}
@Override
public void update(UserInfo entity) {
this.name = entity.getName();

View File

@ -0,0 +1,20 @@
package eu.eudat.data.query.items.item.lock;
import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.data.entities.Lock;
import eu.eudat.data.query.definition.Query;
import eu.eudat.queryable.QueryableList;
public class LockCriteriaRequest extends Query<LockCriteria, Lock> {
@Override
public QueryableList<Lock> applyCriteria() {
QueryableList<Lock> query = this.getQuery();
if (this.getCriteria().getTouchedAt() != null)
query.where((builder, root) -> builder.equal(root.get("touchedAt"), this.getCriteria().getTouchedAt()));
if (this.getCriteria().getLockedBy() != null)
query.where(((builder, root) -> builder.equal(root.get("lockedBy"), this.getCriteria().getLockedBy())));
if (this.getCriteria().getTarget() != null)
query.where(((builder, root) -> builder.equal(root.get("target"), this.getCriteria().getTarget())));
return query;
}
}

View File

@ -0,0 +1,29 @@
package eu.eudat.data.query.items.table.lock;
import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.data.entities.Lock;
import eu.eudat.data.query.PaginationService;
import eu.eudat.data.query.definition.Query;
import eu.eudat.data.query.definition.TableQuery;
import eu.eudat.queryable.QueryableList;
import java.util.UUID;
public class LockTableRequest extends TableQuery<LockCriteria, Lock, UUID> {
@Override
public QueryableList<Lock> applyCriteria() {
QueryableList<Lock> query = this.getQuery();
if (this.getCriteria().getTouchedAt() != null)
query.where((builder, root) -> builder.equal(root.get("touchedAt"), this.getCriteria().getTouchedAt()));
if (this.getCriteria().getLockedBy() != null)
query.where(((builder, root) -> builder.equal(root.get("lockedBy"), this.getCriteria().getLockedBy())));
if (this.getCriteria().getTarget() != null)
query.where(((builder, root) -> builder.equal(root.get("target"), this.getCriteria().getTarget())));
return query;
}
@Override
public QueryableList<Lock> applyPaging(QueryableList<Lock> items) {
return PaginationService.applyPaging(items, this);
}
}

View File

@ -0,0 +1,78 @@
package eu.eudat.query;
import eu.eudat.data.dao.DatabaseAccessLayer;
import eu.eudat.data.entities.Lock;
import eu.eudat.data.entities.UserInfo;
import eu.eudat.queryable.QueryableList;
import eu.eudat.queryable.types.FieldSelectionType;
import eu.eudat.queryable.types.SelectionField;
import javax.persistence.criteria.Subquery;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class LockQuery extends Query<Lock, UUID> {
private UUID id;
private UUID target;
private UserQuery userQuery;
private Date touchedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public UUID getTarget() {
return target;
}
public void setTarget(UUID target) {
this.target = target;
}
public UserQuery getUserQuery() {
return userQuery;
}
public void setUserQuery(UserQuery userQuery) {
this.userQuery = userQuery;
}
public Date getTouchedAt() {
return touchedAt;
}
public void setTouchedAt(Date touchedAt) {
this.touchedAt = touchedAt;
}
public LockQuery(DatabaseAccessLayer<Lock, UUID> databaseAccessLayer, List<String> selectionFields) {
super(databaseAccessLayer, selectionFields);
}
public LockQuery(DatabaseAccessLayer<Lock, UUID> databaseAccessLayer) {
super(databaseAccessLayer);
}
@Override
public QueryableList<Lock> getQuery() {
QueryableList<Lock> query = this.databaseAccessLayer.asQueryable();
if (this.id != null) {
query.where((builder, root) -> builder.equal(root.get("id"), this.id));
}
if (this.target != null) {
query.where(((builder, root) -> builder.equal(root.get("target"), this.target)));
}
if (this.userQuery != null) {
Subquery<UserInfo> userSubQuery = this.userQuery.getQuery().query(Arrays.asList(new SelectionField(FieldSelectionType.FIELD, "id")));
query.where((builder, root) -> root.get("lockedBy").get("id").in(userSubQuery));
}
return query;
}
}

View File

@ -0,0 +1,56 @@
package eu.eudat.controllers;
import com.sun.org.apache.xpath.internal.operations.Bool;
import eu.eudat.logic.managers.LockManager;
import eu.eudat.models.data.dmp.DataManagementPlan;
import eu.eudat.models.data.helpers.responses.ResponseItem;
import eu.eudat.models.data.lock.Lock;
import eu.eudat.models.data.security.Principal;
import eu.eudat.types.ApiMessageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@CrossOrigin
@RequestMapping(value = {"/api/lock/"})
public class LockController {
private LockManager lockManager;
@Autowired
public LockController(LockManager lockManager) {
this.lockManager = lockManager;
}
@Transactional
@RequestMapping(method = RequestMethod.GET, path = "target/status/{id}")
public @ResponseBody ResponseEntity<ResponseItem<Boolean>> getLocked(@PathVariable String id, Principal principal) throws Exception {
boolean locked = this.lockManager.isLocked(id, principal);
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<Boolean>().status(ApiMessageCode.SUCCESS_MESSAGE).message("locked").payload(locked));
}
@Transactional
@RequestMapping(method = RequestMethod.DELETE, path = "target/unlock/{id}")
public @ResponseBody ResponseEntity<ResponseItem<String>> unlock(@PathVariable String id, Principal principal) throws Exception {
this.lockManager.unlock(id, principal);
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<String>().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload("Lock Removed"));
}
@Transactional
@RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
public @ResponseBody ResponseEntity<ResponseItem<UUID>> createOrUpdate(@RequestBody Lock lock, Principal principal) throws Exception {
eu.eudat.data.entities.Lock result = this.lockManager.createOrUpdate(lock, principal);
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<UUID>().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(result.getId()));
}
@RequestMapping(method = RequestMethod.GET, path = "target/{id}")
public @ResponseBody ResponseEntity<ResponseItem<Lock>> getSingle(@PathVariable String id, Principal principal) throws Exception {
Lock lock = this.lockManager.getFromTarget(id, principal);
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<Lock>().status(ApiMessageCode.NO_MESSAGE).payload(lock));
}
}

View File

@ -0,0 +1,103 @@
package eu.eudat.logic.managers;
import eu.eudat.data.dao.criteria.LockCriteria;
import eu.eudat.logic.services.ApiContext;
import eu.eudat.models.data.lock.Lock;
import eu.eudat.models.data.security.Principal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.persistence.NoResultException;
import java.util.Date;
import java.util.UUID;
@Component
public class LockManager {
private ApiContext apiContext;
private Environment environment;
@Autowired
public LockManager(ApiContext apiContext, Environment environment) {
this.apiContext = apiContext;
this.environment = environment;
}
public eu.eudat.data.entities.Lock createOrUpdate(Lock lock, Principal principal) throws Exception {
if (lock.getId() != null) {
try {
eu.eudat.data.entities.Lock entity = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().find(lock.getId());
if (entity != null) {
if (!entity.getLockedBy().getId().equals(principal.getId())) {
throw new Exception("Is not locked by that user");
}
}
}catch(NoResultException e) {
return new eu.eudat.data.entities.Lock();
}
}
eu.eudat.data.entities.Lock newLock = lock.toDataModel();
newLock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().createOrUpdate(newLock);
return newLock;
}
public boolean isLocked(String targetId, Principal principal) throws Exception {
LockCriteria criteria = new LockCriteria();
criteria.setTarget(UUID.fromString(targetId));
Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
if (availableLocks > 0) {
eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
if (lock.getLockedBy().getId().equals(principal.getId())) {
lock.setTouchedAt(new Date());
this.createOrUpdate(new Lock().fromDataModel(lock), principal);
return false;
}
if (new Date().getTime() - lock.getTouchedAt().getTime() > environment.getProperty("database.lock-fail-interval", Integer.class)) {
this.forceUnlock(targetId);
return false;
}
return true;
}
return false;
}
private void forceUnlock(String targetId) throws Exception {
LockCriteria criteria = new LockCriteria();
criteria.setTarget(UUID.fromString(targetId));
Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
if (availableLocks > 0) {
eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock);
}
}
public void unlock(String targetId, Principal principal) throws Exception {
LockCriteria criteria = new LockCriteria();
criteria.setTarget(UUID.fromString(targetId));
Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
if (availableLocks > 0) {
eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
if (!lock.getLockedBy().getId().equals(principal.getId())) {
throw new Exception("Only the user who created that lock can delete it");
}
this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock);
}
}
public Lock getFromTarget(String targetId, Principal principal) throws Exception {
LockCriteria criteria = new LockCriteria();
criteria.setTarget(UUID.fromString(targetId));
Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count();
if (availableLocks > 0) {
eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle();
if (!lock.getLockedBy().getId().equals(principal.getId())) {
throw new Exception("Only the user who created that lock can access it");
}
return new Lock().fromDataModel(lock);
}
return null;
}
}

View File

@ -52,5 +52,7 @@ public interface DatabaseRepository {
FunderDao getFunderDao();
LockDao getLockDao();
<T> void detachEntity(T entity);
}

View File

@ -35,6 +35,7 @@ public class DatabaseRepositoryImpl implements DatabaseRepository {
private LoginConfirmationEmailDao loginConfirmationEmailDao;
private ProjectDao projectDao;
private FunderDao funderDao;
private LockDao lockDao;
private EntityManager entityManager;
@ -273,6 +274,16 @@ public class DatabaseRepositoryImpl implements DatabaseRepository {
this.funderDao = funderDao;
}
@Autowired
public void setLockDao(LockDao lockDao) {
this.lockDao = lockDao;
}
@Override
public LockDao getLockDao() {
return lockDao;
}
public <T> void detachEntity(T entity) {
this.entityManager.detach(entity);
}

View File

@ -0,0 +1,81 @@
package eu.eudat.models.data.lock;
import eu.eudat.models.DataModel;
import eu.eudat.models.data.userinfo.UserInfo;
import java.util.Date;
import java.util.UUID;
public class Lock implements DataModel<eu.eudat.data.entities.Lock, Lock> {
private UUID id;
private UUID target;
private UserInfo lockedBy;
private Date lockedAt;
private Date touchedAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public UUID getTarget() {
return target;
}
public void setTarget(UUID target) {
this.target = target;
}
public UserInfo getLockedBy() {
return lockedBy;
}
public void setLockedBy(UserInfo lockedBy) {
this.lockedBy = lockedBy;
}
public Date getLockedAt() {
return lockedAt;
}
public void setLockedAt(Date lockedAt) {
this.lockedAt = lockedAt;
}
public Date getTouchedAt() {
return touchedAt;
}
public void setTouchedAt(Date touchedAt) {
this.touchedAt = touchedAt;
}
@Override
public Lock fromDataModel(eu.eudat.data.entities.Lock entity) {
this.id = entity.getId();
this.target = entity.getTarget();
this.lockedBy = new UserInfo().fromDataModel(entity.getLockedBy());
this.lockedAt = entity.getLockedAt();
this.touchedAt = entity.getTouchedAt();
return this;
}
@Override
public eu.eudat.data.entities.Lock toDataModel() throws Exception {
eu.eudat.data.entities.Lock entity = new eu.eudat.data.entities.Lock();
entity.setId(this.getId());
entity.setTarget(this.getTarget());
entity.setLockedAt(this.getLockedAt());
entity.setTouchedAt(this.getTouchedAt());
entity.setLockedBy(this.getLockedBy().toDataModel());
return entity;
}
@Override
public String getHint() {
return null;
}
}

View File

@ -107,8 +107,17 @@ public class UserInfo implements DataModel<eu.eudat.data.entities.UserInfo, User
@Override
public eu.eudat.data.entities.UserInfo toDataModel() {
// TODO Auto-generated method stub
return null;
eu.eudat.data.entities.UserInfo entity = new eu.eudat.data.entities.UserInfo();
entity.setId(this.getId());
entity.setEmail(this.getEmail());
entity.setName(this.getName());
entity.setAdditionalinfo(this.getAdditionalinfo());
entity.setAuthorization_level(this.getAuthorization_level());
entity.setCreated(this.getCreated());
entity.setLastloggedin(this.getLastloggedin());
entity.setUsertype(this.getUsertype());
entity.setVerified_email(this.getVerified_email());
return entity;
}
@Override

View File

@ -75,3 +75,4 @@ http-logger.delay = 10
##########################PERISTENCE##########################################
#############GENERIC DATASOURCE CONFIGURATIONS#########
database.driver-class-name=org.postgresql.Driver
database.lock-fail-interval=120000

View File

@ -0,0 +1,14 @@
CREATE TABLE public."Lock" (
id uuid NOT NULL,
"Target" uuid NOT NULL,
"LockedBy" uuid NOT NULL,
"LockedAt" timestamp NOT NULL,
"TouchedAt" timestamp
);
ALTER TABLE ONLY public."Lock"
ADD CONSTRAINT "Lock_pkey" PRIMARY KEY (id);
ALTER TABLE ONLY public."Lock"
ADD CONSTRAINT "LockUserReference" FOREIGN KEY ("LockedBy") REFERENCES public."UserInfo"(id);

View File

@ -37,6 +37,7 @@ import { FunderService } from './services/funder/funder.service';
import { ContactSupportService } from './services/contact-support/contact-support.service';
import { LanguageService } from './services/language/language.service';
import { AdminAuthGuard } from './admin-auth-guard.service';
import { LockService } from './services/lock/lock.service';
//
//
// This is shared module that provides all the services. Its imported only once on the AppModule.
@ -93,7 +94,8 @@ export class CoreServiceModule {
OrganisationService,
EmailConfirmationService,
ContactSupportService,
LanguageService
LanguageService,
LockService
],
};
}

View File

@ -0,0 +1,18 @@
import { Guid } from '@common/types/guid';
import { UserInfoListingModel } from '../user/user-info-listing';
export class LockModel {
id: Guid;
target: Guid;
lockedBy: UserInfoListingModel;
lockedAt: Date;
touchedAt: Date;
constructor(targetId: string, lockedBy: any) {
this.lockedAt = new Date();
this.touchedAt = new Date();
this.target = Guid.parse(targetId);
this.lockedBy = lockedBy;
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { BaseHttpService } from '../http/base-http.service';
import { environment } from 'environments/environment';
import { Observable } from 'rxjs';
import { LockModel } from '@app/core/model/lock/lock.model';
@Injectable()
export class LockService {
private actionUrl: string;
private headers = new HttpHeaders();
constructor(private http: BaseHttpService, private httpClient: HttpClient) {
this.actionUrl = environment.Server + 'lock/';
}
checkLockStatus(id: string): Observable<boolean> {
return this.http.get(`${this.actionUrl}target/status/${id}`, {headers: this.headers});
}
unlockTarget(id: string): Observable<any> {
return this.http.delete(`${this.actionUrl}target/unlock/${id}`, {headers: this.headers});
}
getSingle(id: string): Observable<LockModel> {
return this.http.get(`${this.actionUrl}target/${id}`, {headers: this.headers});
}
createOrUpdate(lock: LockModel): Observable<string> {
return this.http.post(`${this.actionUrl}`, lock, {headers: this.headers});
}
}

View File

@ -107,15 +107,16 @@
</mat-tab-group>
<div class="actions">
<mat-icon *ngIf="hasNotReversableStatus()" color="accent" class="align-self-center mr-1">info_outlined</mat-icon>
<mat-icon *ngIf="hasNotReversableStatus() || lockStatus" color="accent" class="align-self-center mr-1">info_outlined</mat-icon>
<div *ngIf="hasNotReversableStatus()" class="align-self-center mr-3">{{'DATASET-WIZARD.ACTIONS.INFO' | translate}}</div>
<div *ngIf="lockStatus" class="align-self-center mr-3">{{'DATASET-WIZARD.ACTIONS.LOCK' | translate}}</div>
<button mat-raised-button (click)="cancel()" type="button" class="cancelButton" color="primary">
{{'DMP-EDITOR.ACTIONS.CANCEL' | translate}}
</button>
<button *ngIf="datasetWizardModel.status == 0 || isNew" mat-raised-button class="saveButton" color="primary" (click)="save();" type="button">{{ 'DATASET-WIZARD.ACTIONS.SAVE' | translate }}</button>
<button *ngIf="datasetWizardModel.status == 0 || isNew" mat-raised-button class="finalizeButton" color="primary" (click)="saveFinalize();" type="button">{{ 'DATASET-WIZARD.ACTIONS.FINALIZE' | translate }}</button>
<button *ngIf="(datasetWizardModel.status == 0 || isNew) && !lockStatus" mat-raised-button class="saveButton" color="primary" (click)="save();" type="button">{{ 'DATASET-WIZARD.ACTIONS.SAVE' | translate }}</button>
<button *ngIf="(datasetWizardModel.status == 0 || isNew) && !lockStatus" mat-raised-button class="finalizeButton" color="primary" (click)="saveFinalize();" type="button">{{ 'DATASET-WIZARD.ACTIONS.FINALIZE' | translate }}</button>
<div class="fill-space"></div>
<button *ngIf="hasReversableStatus()" mat-raised-button class="reverseButton" color="primary" (click)="reverse()" type="button">{{ 'DATASET-WIZARD.ACTIONS.REVERSE' | translate }}</button>
<button *ngIf="hasReversableStatus() && !lockStatus" mat-raised-button class="reverseButton" color="primary" (click)="reverse()" type="button">{{ 'DATASET-WIZARD.ACTIONS.REVERSE' | translate }}</button>
</div>
</div>
</form>

View File

@ -31,8 +31,15 @@ import { ValidationErrorModel } from '@common/forms/validation/error-model/valid
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import * as FileSaver from 'file-saver';
import { Observable, of as observableOf } from 'rxjs';
import { Observable, of as observableOf, interval } from 'rxjs';
import { catchError, map, takeUntil } from 'rxjs/operators';
import { LockService } from '@app/core/services/lock/lock.service';
import { Location } from '@angular/common';
import { LockModel } from '@app/core/model/lock/lock.model';
import { Guid } from '@common/types/guid';
import { isNullOrUndefined } from 'util';
import { AuthService } from '@app/core/services/auth/auth.service';
import { environment } from 'environments/environment';
@Component({
selector: 'app-dataset-wizard-component',
@ -61,6 +68,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
profileUpdateId: string;
downloadDocumentId: string;
isLinear = false;
lock: LockModel;
lockStatus: Boolean;
constructor(
private datasetWizardService: DatasetWizardService,
@ -73,7 +82,10 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
public dialog: MatDialog,
public externalSourcesConfigurationService: ExternalSourcesConfigurationService,
private uiNotificationService: UiNotificationService,
private formService: FormService
private formService: FormService,
private lockService: LockService,
private location: Location,
private authService: AuthService
) {
super();
}
@ -112,6 +124,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
this.datasetWizardService.getSingle(this.itemId)
.pipe(takeUntil(this._destroyed))
.subscribe(data => {
this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data);
this.needsUpdate();
this.breadCrumbs = observableOf([
@ -129,14 +143,23 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
}]);
this.formGroup = this.datasetWizardModel.buildForm();
this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft;
if (this.datasetWizardModel.status === DatasetStatus.Finalized) {
if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) {
this.formGroup.disable();
this.viewOnly = true;
}
if (!lockStatus) {
this.lock = new LockModel(data.id, this.authService.current());
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.lock.id = Guid.parse(result);
interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
});
}
// if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP.
this.loadDatasetProfiles();
this.registerFormListeners();
// this.availableProfiles = this.datasetWizardModel.dmp.profiles;
})
},
error => {
this.uiNotificationService.snackBarNotification(this.language.instant('DATASET-WIZARD.MESSAGES.DATASET-NOT-FOUND'), SnackBarNotificationLevel.Error);
@ -184,6 +207,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
this.datasetWizardService.getSingle(this.itemId)
.pipe(takeUntil(this._destroyed))
.subscribe(data => {
this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data);
this.formGroup = this.datasetWizardModel.buildForm();
this.formGroup.get('id').setValue(null);
@ -216,13 +241,22 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
});
});
this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft;
if (this.datasetWizardModel.status === DatasetStatus.Finalized) {
if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) {
this.formGroup.disable();
this.viewOnly = true;
}
if (!lockStatus) {
this.lock = new LockModel(data.id, this.authService.current());
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.lock.id = Guid.parse(result);
interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
});
}
// if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP.
this.loadDatasetProfiles();
// this.availableProfiles = data.dmp.profiles;
})
});
} else if (this.publicId != null) { // For Finalized -> Public Datasets
this.isNew = false;
@ -235,19 +269,30 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
}))
.subscribe(data => {
if (data) {
this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data);
this.formGroup = this.datasetWizardModel.buildForm();
this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft;
if (this.datasetWizardModel.status === DatasetStatus.Finalized) {
if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) {
this.formGroup.disable();
this.viewOnly = true;
}
if (!lockStatus) {
this.lock = new LockModel(data.id, this.authService.current());
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.lock.id = Guid.parse(result);
interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
});
}
this.formGroup.get('dmp').setValue(this.datasetWizardModel.dmp);
this.loadDatasetProfiles();
const breadcrumbs = [];
breadcrumbs.push({ parentComponentName: null, label: this.language.instant('NAV-BAR.PUBLIC DATASETS'), url: '/explore' });
breadcrumbs.push({ parentComponentName: null, label: this.datasetWizardModel.label, url: '/datasets/publicEdit/' + this.datasetWizardModel.id });
this.breadCrumbs = observableOf(breadcrumbs);
})
}
});
this.publicMode = true;
@ -284,6 +329,7 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
// if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP.
this.loadDatasetProfiles();
});
} else {
this.datasetWizardModel = new DatasetWizardEditorModel();
this.formGroup = this.datasetWizardModel.buildForm();
@ -368,7 +414,21 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
}
public cancel(): void {
this.router.navigate(['/datasets']);
if (!isNullOrUndefined(this.lock)) {
this.lockService.unlockTarget(this.datasetWizardModel.id).pipe(takeUntil(this._destroyed)).subscribe(
complete => {
this.router.navigate(['/datasets']);
},
error => {
this.formGroup.get('status').setValue(DmpStatus.Draft);
this.onCallbackError(error);
}
)
} else {
this.router.navigate(['/datasets']);
}
}
getDatasetDisplay(item: any): string {
@ -679,7 +739,7 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
return false;
}
else {
return true
return true;
}
}
@ -706,4 +766,15 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr
onStepFound(linkToScroll: LinkToScroll) {
this.linkToScroll = linkToScroll;
}
private pumpLock() {
this.lock.touchedAt = new Date();
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe( async result => {
if (!isNullOrUndefined(result)) {
this.lock.id = Guid.parse(result);
} else {
this.location.back();
}
});
}
}

View File

@ -9,7 +9,7 @@
<h4 class="card-title">{{ 'DMP-EDITOR.TITLE.NEW' | translate }}</h4>
</div>
<div class="d-flex ml-auto p-2" *ngIf="!isNew">
<button *ngIf="!isFinalized" mat-icon-button [matMenuTriggerFor]="actionsMenu" class="ml-auto more-icon" (click)="$event.stopImmediatePropagation();">
<button *ngIf="!isFinalized && !lockStatus" mat-icon-button [matMenuTriggerFor]="actionsMenu" class="ml-auto more-icon" (click)="$event.stopImmediatePropagation();">
<mat-icon class="more-horiz">more_horiz</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
@ -65,21 +65,21 @@
<mat-icon class="mr-2">work_outline</mat-icon>
{{ 'DMP-LISTING.COLUMNS.GRANT' | translate }}
</ng-template>
<app-grant-tab [grantformGroup]="formGroup.get('grant')" [projectFormGroup]="formGroup.get('project')" [funderFormGroup]="formGroup.get('funder')" [isFinalized]="isFinalized" [isNew]="isNew" [isUserOwner]="isUserOwner"></app-grant-tab>
<app-grant-tab [grantformGroup]="formGroup.get('grant')" [projectFormGroup]="formGroup.get('project')" [funderFormGroup]="formGroup.get('funder')" [isFinalized]="isFinalized || lockStatus" [isNew]="isNew" [isUserOwner]="isUserOwner"></app-grant-tab>
</mat-tab>
<mat-tab *ngIf="!isNew">
<ng-template mat-tab-label>
<mat-icon class="mr-2">library_books</mat-icon>
{{ 'DMP-LISTING.COLUMNS.DATASETS' | translate }}
</ng-template>
<app-datasets-tab [dmp]="dmp" [isPublic]="isPublic" [isFinalized]="isFinalized"></app-datasets-tab>
<app-datasets-tab [dmp]="dmp" [isPublic]="isPublic" [isFinalized]="isFinalized || lockStatus"></app-datasets-tab>
</mat-tab>
<mat-tab *ngIf="!isNew">
<ng-template mat-tab-label>
<mat-icon class="mr-2">person</mat-icon>
{{ 'DMP-LISTING.COLUMNS.PEOPLE' | translate }}
</ng-template>
<app-people-tab [formGroup]="formGroup" [dmp]="dmp" [isPublic]="isPublic" [isFinalized]="isFinalized"></app-people-tab>
<app-people-tab [formGroup]="formGroup" [dmp]="dmp" [isPublic]="isPublic" [isFinalized]="isFinalized || lockStatus"></app-people-tab>
</mat-tab>
<mat-tab *ngIf="isNew" disabled>
<ng-template mat-tab-label></ng-template>
@ -101,12 +101,12 @@
{{'DMP-EDITOR.ACTIONS.CANCEL' | translate}}
</button>
</div>
<div *ngIf="isNew">
<div *ngIf="isNew && !lockStatus">
<button mat-raised-button color="primary" (click)="cancel()" type="button" class="text-uppercase mr-2">
{{'DMP-EDITOR.ACTIONS.CANCEL' | translate}}
</button>
</div>
<div *ngIf="formGroup.enabled">
<div *ngIf="formGroup.enabled && !lockStatus">
<button *ngIf="!isNew" mat-raised-button type="submit" class="text-uppercase dark-theme mr-2" color="primary">
{{'DMP-EDITOR.ACTIONS.SAVE-CHANGES' | translate}}
</button>
@ -114,7 +114,7 @@
{{'DMP-EDITOR.ACTIONS.SAVE' | translate}}
</button>
</div>
<div *ngIf="formGroup.enabled && !isNew">
<div *ngIf="formGroup.enabled && !isNew && !lockStatus">
<button type="button" mat-raised-button color="primary" class="text-uppercase mr-2" (click)="saveAndFinalize()">{{'DMP-EDITOR.ACTIONS.FINALISE' | translate}}
</button>
</div>

View File

@ -32,10 +32,16 @@ import { FormValidationErrorsDialogComponent } from '@common/forms/form-validati
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { TranslateService } from '@ngx-translate/core';
import * as FileSaver from 'file-saver';
import { Observable, of as observableOf } from 'rxjs';
import { Observable, of as observableOf, interval } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { Principal } from "@app/core/model/auth/Principal";
import { Role } from "@app/core/common/enum/role";
import { LockService } from '@app/core/services/lock/lock.service';
import { Location } from '@angular/common';
import { LockModel } from '@app/core/model/lock/lock.model';
import { Guid } from '@common/types/guid';
import { isNullOrUndefined } from 'util';
import { environment } from 'environments/environment';
@Component({
selector: 'app-dmp-editor-component',
@ -62,6 +68,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
selectedDmpProfileDefinition: DmpProfileDefinition;
DynamicDmpFieldResolverComponent: any;
isUserOwner: boolean = true;
lock: LockModel;
lockStatus: Boolean;
constructor(
private dmpProfileService: DmpProfileService,
@ -73,7 +81,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
private uiNotificationService: UiNotificationService,
private authentication: AuthService,
private authService: AuthService,
private formService: FormService
private formService: FormService,
private lockService: LockService
) {
super();
}
@ -104,6 +113,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
this.dmpService.getSingle(itemId).pipe(map(data => data as DmpModel))
.pipe(takeUntil(this._destroyed))
.subscribe(async data => {
this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
this.dmp = new DmpEditorModel();
this.dmp.grant = new GrantTabModel();
this.dmp.project = new ProjectFormModel();
@ -115,12 +126,22 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
this.isFinalized = true;
this.formGroup.disable();
}
//this.registerFormEventsForDmpProfile(this.dmp.definition);
if (!this.editMode || this.dmp.status === DmpStatus.Finalized) {
if (!this.editMode || this.dmp.status === DmpStatus.Finalized || lockStatus) {
this.isFinalized = true;
this.formGroup.disable();
}
if (this.isAuthenticated) {
if (!lockStatus) {
this.lock = new LockModel(data.id, this.getUserFromDMP());
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.lock.id = Guid.parse(result);
interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
});
}
// if (!this.isAuthenticated) {
const breadCrumbs = [];
breadCrumbs.push({
@ -139,6 +160,7 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
}
this.associatedUsers = data.associatedUsers;
this.people = data.users;
})
});
} else if (publicId != null) {
this.isNew = false;
@ -146,6 +168,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
this.dmpService.getSinglePublic(publicId).pipe(map(data => data as DmpModel))
.pipe(takeUntil(this._destroyed))
.subscribe(async data => {
this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => {
this.lockStatus = lockStatus;
this.dmp = new DmpEditorModel();
this.dmp.grant = new GrantTabModel();
this.dmp.project = new ProjectFormModel();
@ -153,7 +177,7 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
this.dmp.fromModel(data);
this.formGroup = this.dmp.buildForm();
//this.registerFormEventsForDmpProfile(this.dmp.definition);
if (!this.editMode || this.dmp.status === DmpStatus.Finalized) { this.formGroup.disable(); }
if (!this.editMode || this.dmp.status === DmpStatus.Finalized || lockStatus) { this.formGroup.disable(); }
// if (!this.isAuthenticated) {
const breadcrumbs = [];
breadcrumbs.push({ parentComponentName: null, label: this.language.instant('NAV-BAR.PUBLIC-DMPS').toUpperCase(), url: '/plans' });
@ -169,6 +193,15 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
// );
this.associatedUsers = data.associatedUsers;
// }
if (!lockStatus) {
this.lock = new LockModel(data.id, this.getUserFromDMP());
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => {
this.lock.id = Guid.parse(result);
interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock());
});
}
})
});
} else {
this.dmp = new DmpEditorModel();
@ -301,7 +334,17 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
public cancel(id: String): void {
if (id != null) {
this.router.navigate(['/plans/overview/' + id]);
this.lockService.unlockTarget(this.dmp.id).pipe(takeUntil(this._destroyed)).subscribe(
complete => {
this.router.navigate(['/plans/overview/' + id]);
},
error => {
this.formGroup.get('status').setValue(DmpStatus.Draft);
this.onCallbackError(error);
}
)
} else {
this.router.navigate(['/plans']);
}
@ -498,6 +541,17 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC
});
}
private pumpLock() {
this.lock.touchedAt = new Date();
this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe( async result => {
if (!isNullOrUndefined(result)) {
this.lock.id = Guid.parse(result);
} else {
this.location.back();
}
});
}
// advancedClicked() {
// const dialogRef = this.dialog.open(ExportMethodDialogComponent, {
// maxWidth: '500px',

View File

@ -477,6 +477,7 @@
"FINALIZE": "Finalize",
"REVERSE": "Undo Finalization",
"INFO": "Datasets of finalized DMPs can't revert to unfinalized",
"LOCK": "Dataset is Locked by another user",
"DOWNLOAD-PDF": "Download PDF",
"DOWNLOAD-XML": "Download XML",
"DOWNLOAD-DOCX": "Download DOCX",

View File

@ -477,6 +477,7 @@
"FINALIZE": "Finalize",
"REVERSE": "Undo Finalization",
"INFO": "Datasets of finalized DMPs can't revert to unfinalized",
"LOCK": "Dataset is Locked by another user",
"DOWNLOAD-PDF": "Download PDF",
"DOWNLOAD-XML": "Download XML",
"DOWNLOAD-DOCX": "Download DOCX",

View File

@ -43,4 +43,5 @@ export const environment = {
enabled: true,
logLevels: ["debug", "info", "warning", "error"]
},
lockInterval: 60000,
};