Add Email Notifications when one of the collaborators is editing either a DMP or a Dataset (ref #244)
This commit is contained in:
parent
f4cd087672
commit
25988ab272
|
@ -0,0 +1,26 @@
|
|||
package eu.eudat.data.dao.criteria;
|
||||
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
|
||||
public class NotificationCriteria {
|
||||
|
||||
private ActiveStatus isActive;
|
||||
private NotifyState notifyState;
|
||||
|
||||
public ActiveStatus getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(ActiveStatus isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public NotifyState getNotifyState() {
|
||||
return notifyState;
|
||||
}
|
||||
|
||||
public void setNotifyState(NotifyState notifyState) {
|
||||
this.notifyState = notifyState;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package eu.eudat.data.dao.entities;
|
||||
|
||||
import eu.eudat.data.dao.DatabaseAccessLayer;
|
||||
import eu.eudat.data.dao.criteria.NotificationCriteria;
|
||||
import eu.eudat.data.entities.Notification;
|
||||
import eu.eudat.queryable.QueryableList;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface NotificationDao extends DatabaseAccessLayer<Notification, UUID> {
|
||||
|
||||
QueryableList<Notification> getWithCriteria(NotificationCriteria criteria);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package eu.eudat.data.dao.entities;
|
||||
|
||||
import eu.eudat.data.dao.DatabaseAccess;
|
||||
import eu.eudat.data.dao.criteria.NotificationCriteria;
|
||||
import eu.eudat.data.dao.databaselayer.service.DatabaseService;
|
||||
import eu.eudat.data.entities.Lock;
|
||||
import eu.eudat.data.entities.Notification;
|
||||
import eu.eudat.queryable.QueryableList;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service("NotificationDao")
|
||||
public class NotificationDaoImpl extends DatabaseAccess<Notification> implements NotificationDao {
|
||||
@Autowired
|
||||
public NotificationDaoImpl(DatabaseService<Notification> databaseService) {
|
||||
super(databaseService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryableList<Notification> getWithCriteria(NotificationCriteria criteria) {
|
||||
QueryableList<Notification> query = this.getDatabaseService().getQueryable(Notification.class);
|
||||
if (criteria.getIsActive() != null)
|
||||
query.where((builder, root) -> builder.equal(root.get("isActive"), criteria.getIsActive()));
|
||||
if (criteria.getNotifyState() != null)
|
||||
query.where(((builder, root) -> builder.equal(root.get("notifyState"), criteria.getNotifyState())));
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification createOrUpdate(Notification item) {
|
||||
return this.getDatabaseService().createOrUpdate(item, Notification.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Notification> createOrUpdateAsync(Notification item) {
|
||||
return CompletableFuture.supplyAsync(() -> this.getDatabaseService().createOrUpdate(item, Notification.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification find(UUID id) {
|
||||
return this.getDatabaseService().getQueryable(Notification.class).where(((builder, root) -> builder.equal(root.get("id"), id))).getSingle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification find(UUID id, String hint) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Notification item) {
|
||||
this.getDatabaseService().delete(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryableList<Notification> asQueryable() {
|
||||
return this.getDatabaseService().getQueryable(Notification.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package eu.eudat.data.entities;
|
||||
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.ContactType;
|
||||
import eu.eudat.data.enumeration.notification.NotificationType;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
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 = "\"Notification\"")
|
||||
public class Notification implements DataEntity<Notification, UUID> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@GenericGenerator(name = "uuid2", strategy = "uuid2")
|
||||
@Column(name = "id", updatable = false, nullable = false, columnDefinition = "BINARY(16)")
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "\"UserId\"")
|
||||
private UserInfo userId;
|
||||
|
||||
@Enumerated
|
||||
@Column(name = "\"IsActive\"", nullable = false)
|
||||
private ActiveStatus isActive;
|
||||
|
||||
@Enumerated
|
||||
@Column(name = "\"Type\"", nullable = false)
|
||||
private NotificationType type;
|
||||
|
||||
@Enumerated
|
||||
@Column(name = "\"ContactTypeHint\"")
|
||||
private ContactType contactTypeHint;
|
||||
|
||||
@Column(name = "\"ContactHint\"")
|
||||
private String contactHint;
|
||||
|
||||
@Column(name = "\"Data\"")
|
||||
private String data;
|
||||
|
||||
@Enumerated
|
||||
@Column(name = "\"NotifyState\"")
|
||||
private NotifyState notifyState;
|
||||
|
||||
@Column(name = "\"NotifiedAt\"")
|
||||
private Date notifiedAt;
|
||||
|
||||
@Column(name = "\"RetryCount\"")
|
||||
private Integer retryCount;
|
||||
|
||||
@Column(name = "\"CreatedAt\"")
|
||||
private Date createdAt;
|
||||
|
||||
@Column(name = "\"UpdatedAt\"")
|
||||
private Date updatedAt;
|
||||
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UserInfo getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UserInfo userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public ActiveStatus getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(ActiveStatus isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public NotificationType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(NotificationType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ContactType getContactTypeHint() {
|
||||
return contactTypeHint;
|
||||
}
|
||||
|
||||
public void setContactTypeHint(ContactType contactTypeHint) {
|
||||
this.contactTypeHint = contactTypeHint;
|
||||
}
|
||||
|
||||
public String getContactHint() {
|
||||
return contactHint;
|
||||
}
|
||||
|
||||
public void setContactHint(String contactHint) {
|
||||
this.contactHint = contactHint;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public NotifyState getNotifyState() {
|
||||
return notifyState;
|
||||
}
|
||||
|
||||
public void setNotifyState(NotifyState notifyState) {
|
||||
this.notifyState = notifyState;
|
||||
}
|
||||
|
||||
public Date getNotifiedAt() {
|
||||
return notifiedAt;
|
||||
}
|
||||
|
||||
public void setNotifiedAt(Date notifiedAt) {
|
||||
this.notifiedAt = notifiedAt;
|
||||
}
|
||||
|
||||
public Integer getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(Integer retryCount) {
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Date getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Date updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Notification entity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getKeys() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification buildFromTuple(List<Tuple> tuple, List<String> fields, String base) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -72,6 +72,9 @@ public class UserInfo implements DataEntity<UserInfo, UUID> {
|
|||
@OneToMany(mappedBy = "lockedBy", fetch = FetchType.LAZY)
|
||||
private Set<Lock> locks = new HashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "userId", fetch = FetchType.LAZY)
|
||||
private Set<Notification> notifications = new HashSet<>();
|
||||
|
||||
public Set<DMP> getDmps() {
|
||||
return dmps;
|
||||
}
|
||||
|
@ -176,6 +179,14 @@ public class UserInfo implements DataEntity<UserInfo, UUID> {
|
|||
this.locks = locks;
|
||||
}
|
||||
|
||||
public Set<Notification> getNotifications() {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
public void setNotifications(Set<Notification> notifications) {
|
||||
this.notifications = notifications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(UserInfo entity) {
|
||||
this.name = entity.getName();
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package eu.eudat.data.enumeration.notification;
|
||||
|
||||
public enum ActiveStatus {
|
||||
ACTIVE(0),
|
||||
INACTIVE(1);
|
||||
|
||||
private int status;
|
||||
|
||||
ActiveStatus(int status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public ActiveStatus fromInteger(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return ACTIVE;
|
||||
case 1:
|
||||
return INACTIVE;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported Active Status");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package eu.eudat.data.enumeration.notification;
|
||||
|
||||
public enum ContactType {
|
||||
EMAIL(0);
|
||||
|
||||
private int type;
|
||||
|
||||
ContactType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public ContactType fromInteger(int type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return EMAIL;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported Contact Type");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.eudat.data.enumeration.notification;
|
||||
|
||||
public enum NotificationType {
|
||||
DMP_MODIFIED(0),
|
||||
DATASET_MODIFIED(1);
|
||||
|
||||
private int type;
|
||||
|
||||
NotificationType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public NotificationType fromInteger(int type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return DMP_MODIFIED;
|
||||
case 1:
|
||||
return DATASET_MODIFIED;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported Notification Type");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package eu.eudat.data.enumeration.notification;
|
||||
|
||||
public enum NotifyState {
|
||||
PENDING(0),
|
||||
PROCESSING(1),
|
||||
SENDING(2),
|
||||
SUCCEEDED(3),
|
||||
ERROR(4);
|
||||
|
||||
private int state;
|
||||
|
||||
NotifyState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public NotifyState fromInteger(int state) {
|
||||
switch (state) {
|
||||
case 0:
|
||||
return PENDING;
|
||||
case 1:
|
||||
return PROCESSING;
|
||||
case 2:
|
||||
return SENDING;
|
||||
case 3:
|
||||
return SUCCEEDED;
|
||||
case 4:
|
||||
return ERROR;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported Notify State");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import eu.eudat.logic.services.operations.authentication.AuthenticationService;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
@ -15,6 +16,7 @@ import java.util.List;
|
|||
|
||||
@EnableAsync
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class WebMVCConfiguration extends WebMvcConfigurerAdapter {
|
||||
|
||||
private ApiContext apiContext;
|
||||
|
|
|
@ -9,6 +9,10 @@ import eu.eudat.data.dao.entities.*;
|
|||
import eu.eudat.data.entities.Organisation;
|
||||
import eu.eudat.data.entities.Researcher;
|
||||
import eu.eudat.data.entities.*;
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.ContactType;
|
||||
import eu.eudat.data.enumeration.notification.NotificationType;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
import eu.eudat.data.query.items.item.dmp.DataManagementPlanCriteriaRequest;
|
||||
import eu.eudat.data.query.items.table.datasetprofile.DatasetProfileTableRequestItem;
|
||||
import eu.eudat.data.query.items.table.dmp.DataManagementPlanTableRequest;
|
||||
|
@ -474,7 +478,7 @@ public class DataManagementPlanManager {
|
|||
}
|
||||
|
||||
public DMP createOrUpdate(ApiContext apiContext, DataManagementPlanEditorModel dataManagementPlan, Principal principal) throws Exception {
|
||||
|
||||
boolean setNotification = false;
|
||||
if (dataManagementPlan.getId() != null) {
|
||||
DMP dmp1 = apiContext.getOperationsContext().getDatabaseRepository().getDmpDao().find(dataManagementPlan.getId());
|
||||
|
||||
|
@ -491,6 +495,8 @@ public class DataManagementPlanManager {
|
|||
}
|
||||
if (dataManagementPlan.getStatus() == (int) DMP.DMPStatus.FINALISED.getValue() && dmp1.getStatus().equals(DMP.DMPStatus.FINALISED.getValue()))
|
||||
throw new Exception("DMP is finalized, therefore cannot be edited.");
|
||||
|
||||
setNotification = true;
|
||||
}
|
||||
|
||||
DMP newDmp = dataManagementPlan.toDataModel();
|
||||
|
@ -568,9 +574,38 @@ public class DataManagementPlanManager {
|
|||
if (dataManagementPlan.getAssociatedUsers().size() == 0)
|
||||
assignUser(newDmp, user);
|
||||
|
||||
if (setNotification) {
|
||||
this.sendNotification(newDmp, user);
|
||||
}
|
||||
|
||||
return newDmp;
|
||||
}
|
||||
|
||||
private void sendNotification(DMP dmp, UserInfo user) {
|
||||
List<UserDMP> userDMPS = databaseRepository.getUserDmpDao().asQueryable().where(((builder, root) -> builder.equal(root.get("dmp").get("id"), dmp.getId()))).toList();
|
||||
for (UserDMP userDMP : userDMPS) {
|
||||
if (!userDMP.getUser().getId().equals(user.getId())) {
|
||||
Notification notification = new Notification();
|
||||
notification.setUserId(user);
|
||||
notification.setType(NotificationType.DMP_MODIFIED);
|
||||
notification.setNotifyState(NotifyState.PENDING);
|
||||
notification.setIsActive(ActiveStatus.ACTIVE);
|
||||
notification.setData("{" +
|
||||
"\"userId\": \"" + userDMP.getUser().getId() + "\"" +
|
||||
", \"id\": \"" + userDMP.getDmp().getId() + "\"" +
|
||||
", \"name\": \"" + userDMP.getDmp().getLabel() + "\"" +
|
||||
", \"path\": \"/plans/edit\"" +
|
||||
"}");
|
||||
notification.setCreatedAt(new Date());
|
||||
notification.setUpdatedAt(notification.getCreatedAt());
|
||||
notification.setContactTypeHint(ContactType.EMAIL);
|
||||
notification.setContactHint(userDMP.getUser().getEmail());
|
||||
databaseRepository.getNotificationDao().createOrUpdate(notification);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void assignUser(DMP dmp, UserInfo userInfo) {
|
||||
UserDMP userDMP = new UserDMP();
|
||||
|
|
|
@ -5,6 +5,10 @@ import eu.eudat.data.dao.entities.DataRepositoryDao;
|
|||
import eu.eudat.data.dao.entities.DatasetDao;
|
||||
import eu.eudat.data.dao.entities.RegistryDao;
|
||||
import eu.eudat.data.entities.*;
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.ContactType;
|
||||
import eu.eudat.data.enumeration.notification.NotificationType;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
import eu.eudat.data.query.items.table.dataset.DatasetPublicTableRequest;
|
||||
import eu.eudat.data.query.items.table.dataset.DatasetTableRequest;
|
||||
import eu.eudat.data.query.items.table.datasetprofile.DatasetProfileTableRequestItem;
|
||||
|
@ -400,6 +404,7 @@ public class DatasetManager {
|
|||
}
|
||||
|
||||
public eu.eudat.data.entities.Dataset createOrUpdate(DatasetWizardModel datasetWizardModel, Principal principal) throws Exception {
|
||||
Boolean sendNotification = false;
|
||||
DMP dmp = apiContext.getOperationsContext().getDatabaseRepository().getDmpDao().find(datasetWizardModel.getDmp().getId());
|
||||
if (datasetWizardModel.getId() != null) {
|
||||
Dataset tempDataset = apiContext.getOperationsContext().getDatabaseRepository().getDatasetDao().find(datasetWizardModel.getId());
|
||||
|
@ -407,6 +412,7 @@ public class DatasetManager {
|
|||
if (datasetWizardModel.getModified().getTime() != tempDataset.getModified().getTime()) {
|
||||
throw new Exception("Dataset has been modified already by another user.");
|
||||
}
|
||||
sendNotification = true;
|
||||
}
|
||||
}
|
||||
if (dmp.getStatus().equals(DMP.DMPStatus.FINALISED.getValue()) && datasetWizardModel.getId() != null)
|
||||
|
@ -427,9 +433,37 @@ public class DatasetManager {
|
|||
Dataset dataset1 = apiContext.getOperationsContext().getDatabaseRepository().getDatasetDao().createOrUpdate(dataset);
|
||||
datasetWizardModel.setId(dataset1.getId());
|
||||
updateTags(apiContext.getOperationsContext().getDatasetRepository(), datasetWizardModel);
|
||||
if (sendNotification) {
|
||||
this.sendNotification(dataset1, dataset1.getDmp(), userInfo);
|
||||
}
|
||||
return dataset1;
|
||||
}
|
||||
|
||||
private void sendNotification(Dataset dataset, DMP dmp, UserInfo user) {
|
||||
List<UserDMP> userDMPS = databaseRepository.getUserDmpDao().asQueryable().where(((builder, root) -> builder.equal(root.get("dmp").get("id"), dmp.getId()))).toList();
|
||||
for (UserDMP userDMP : userDMPS) {
|
||||
if (!userDMP.getUser().getId().equals(user.getId())) {
|
||||
Notification notification = new Notification();
|
||||
notification.setUserId(user);
|
||||
notification.setType(NotificationType.DATASET_MODIFIED);
|
||||
notification.setNotifyState(NotifyState.PENDING);
|
||||
notification.setIsActive(ActiveStatus.ACTIVE);
|
||||
notification.setData("{" +
|
||||
"\"userId\": \"" + userDMP.getUser().getId() + "\"" +
|
||||
", \"id\": \"" + dataset.getId() + "\"" +
|
||||
", \"name\": \"" + dataset.getLabel() + "\"" +
|
||||
", \"path\": \"/datasets/edit\"" +
|
||||
"}");
|
||||
notification.setCreatedAt(new Date());
|
||||
notification.setUpdatedAt(notification.getCreatedAt());
|
||||
notification.setContactTypeHint(ContactType.EMAIL);
|
||||
notification.setContactHint(userDMP.getUser().getEmail());
|
||||
databaseRepository.getNotificationDao().createOrUpdate(notification);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkDatasetValidation(Dataset dataset) throws Exception {
|
||||
List<String> datasetProfileValidators = new LinkedList<>();
|
||||
DatasetProfile profile = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().find(dataset.getProfile().getId());
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package eu.eudat.logic.managers;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import eu.eudat.data.entities.Notification;
|
||||
import eu.eudat.data.entities.UserInfo;
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.NotificationType;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
import eu.eudat.logic.services.ApiContext;
|
||||
import eu.eudat.logic.services.utilities.MailService;
|
||||
import eu.eudat.models.data.mail.SimpleMail;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.mail.MessagingException;
|
||||
import javax.transaction.Transactional;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Component
|
||||
public class NotificationManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotificationManager.class);
|
||||
|
||||
private ApiContext apiContext;
|
||||
private Environment environment;
|
||||
private MailService mailService;
|
||||
|
||||
@Autowired
|
||||
public NotificationManager(ApiContext apiContext, Environment environment, MailService mailService) {
|
||||
this.apiContext = apiContext;
|
||||
this.environment = environment;
|
||||
this.mailService = mailService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void sendNotification(Notification notification) throws Exception {
|
||||
if (notification.getNotifyState() == NotifyState.ERROR) {
|
||||
if (notification.getRetryCount() == null) {
|
||||
notification.setRetryCount(0);
|
||||
}
|
||||
notification.setRetryCount(notification.getRetryCount() + 1);
|
||||
if (notification.getRetryCount() >= this.environment.getProperty("notification.maxRetries", Integer.class)) {
|
||||
notification.setIsActive(ActiveStatus.INACTIVE);
|
||||
notification.setUpdatedAt(new Date());
|
||||
return;
|
||||
}
|
||||
}
|
||||
notification.setNotifyState(NotifyState.PROCESSING);
|
||||
notification.setNotifiedAt(new Date());
|
||||
notification.setUpdatedAt(new Date());
|
||||
try {
|
||||
Map<String, String> data = new ObjectMapper().readValue(notification.getData(), HashMap.class);
|
||||
UserInfo userInfo = this.apiContext.getOperationsContext().getDatabaseRepository().getUserInfoDao().find(UUID.fromString(data.get("userId")));
|
||||
String subjectTemplate = "";
|
||||
String contentTemplate = "";
|
||||
|
||||
switch (notification.getType()) {
|
||||
case DMP_MODIFIED:
|
||||
case DATASET_MODIFIED:
|
||||
subjectTemplate = this.environment.getProperty("mail.modified.notification.subject");
|
||||
contentTemplate = mailService.getMailTemplateContent("classpath:modifiedNotification.html");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
switch (notification.getContactTypeHint()) {
|
||||
case EMAIL:
|
||||
this.sendEmailNotification(notification, userInfo, data, subjectTemplate, contentTemplate);
|
||||
notification.setNotifyState(NotifyState.SUCCEEDED);
|
||||
notification.setUpdatedAt(new Date());
|
||||
break;
|
||||
}
|
||||
}catch (Exception e) {
|
||||
notification.setNotifyState(NotifyState.ERROR);
|
||||
notification.setUpdatedAt(new Date());
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEmailNotification(Notification notification, UserInfo userInfo, Map<String, String> data, String subjectTemplate, String contentTemplate) throws IOException {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
SimpleMail simpleMail = new SimpleMail();
|
||||
simpleMail.setFrom(this.environment.getProperty("mail.from"));
|
||||
simpleMail.setSubject(makeSubject(data, subjectTemplate));
|
||||
simpleMail.setTo(notification.getContactHint());
|
||||
simpleMail.setContent(makeContent(data, notification, userInfo, contentTemplate));
|
||||
try {
|
||||
mailService.sendSimpleMail(simpleMail);
|
||||
} catch (MessagingException e) {
|
||||
notification.setNotifyState(NotifyState.ERROR);
|
||||
notification.setUpdatedAt(new Date());
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String makeSubject(Map<String, String> data, String subjectTemplate) {
|
||||
return subjectTemplate.replace("{name}", data.get("name"));
|
||||
}
|
||||
|
||||
private String makeContent(Map<String, String> data, Notification notification, UserInfo userInfo, String template) {
|
||||
String content = template;
|
||||
content = content.replace("{recipient}", userInfo.getName());
|
||||
content = content.replace("{id}", data.get("id"));
|
||||
content = content.replace("{name}", data.get("name"));
|
||||
content = content.replace("{host}", this.environment.getProperty("dmp.domain"));
|
||||
content = content.replace("{reasonName}", notification.getUserId().getName());
|
||||
content = content.replace("{path}", data.get("path"));
|
||||
return content;
|
||||
}
|
||||
}
|
|
@ -54,5 +54,7 @@ public interface DatabaseRepository {
|
|||
|
||||
LockDao getLockDao();
|
||||
|
||||
NotificationDao getNotificationDao();
|
||||
|
||||
<T> void detachEntity(T entity);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ public class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||
private ProjectDao projectDao;
|
||||
private FunderDao funderDao;
|
||||
private LockDao lockDao;
|
||||
private NotificationDao notificationDao;
|
||||
|
||||
private EntityManager entityManager;
|
||||
|
||||
|
@ -284,6 +285,16 @@ public class DatabaseRepositoryImpl implements DatabaseRepository {
|
|||
return lockDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationDao getNotificationDao() {
|
||||
return notificationDao;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setNotificationDao(NotificationDao notificationDao) {
|
||||
this.notificationDao = notificationDao;
|
||||
}
|
||||
|
||||
public <T> void detachEntity(T entity) {
|
||||
this.entityManager.detach(entity);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package eu.eudat.logic.utilities.schedule.notification;
|
||||
|
||||
import eu.eudat.data.entities.Notification;
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
import eu.eudat.logic.managers.NotificationManager;
|
||||
import eu.eudat.logic.services.ApiContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class NotificationScheduleJob {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotificationScheduleJob.class);
|
||||
|
||||
private ApiContext apiContext;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Autowired
|
||||
public NotificationScheduleJob(ApiContext apiContext, NotificationManager notificationManager) {
|
||||
this.apiContext = apiContext;
|
||||
this.notificationManager = notificationManager;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Scheduled(fixedRateString = "${notification.rateInterval}")
|
||||
public void sendNotifications() {
|
||||
List<Notification> notifications = this.apiContext.getOperationsContext().getDatabaseRepository().getNotificationDao().asQueryable().where(((builder, root) ->
|
||||
builder.and(
|
||||
builder.or(
|
||||
builder.equal(root.get("notifyState"), NotifyState.PENDING), builder.equal(root.get("notifyState"), NotifyState.ERROR))
|
||||
, builder.equal(root.get("isActive"), ActiveStatus.ACTIVE)))).toList();
|
||||
if (!notifications.isEmpty()) {
|
||||
notifications.forEach(notification -> {
|
||||
try {
|
||||
this.notificationManager.sendNotification(notification);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package eu.eudat.models.data.notiication;
|
||||
|
||||
import eu.eudat.data.enumeration.notification.ActiveStatus;
|
||||
import eu.eudat.data.enumeration.notification.ContactType;
|
||||
import eu.eudat.data.enumeration.notification.NotificationType;
|
||||
import eu.eudat.data.enumeration.notification.NotifyState;
|
||||
import eu.eudat.models.DataModel;
|
||||
import eu.eudat.models.data.userinfo.UserInfo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Notification implements DataModel<eu.eudat.data.entities.Notification, Notification> {
|
||||
|
||||
private UUID id;
|
||||
private UserInfo userId;
|
||||
private ActiveStatus isActive;
|
||||
private NotificationType type;
|
||||
private ContactType contactTypeHint;
|
||||
private String contactHint;
|
||||
private String data;
|
||||
private NotifyState notifyState;
|
||||
private Date notifiedAt;
|
||||
private Integer retryCount;
|
||||
private Date createdAt;
|
||||
private Date updatedAt;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UserInfo getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UserInfo userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public ActiveStatus getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(ActiveStatus isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public NotificationType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(NotificationType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ContactType getContactTypeHint() {
|
||||
return contactTypeHint;
|
||||
}
|
||||
|
||||
public void setContactTypeHint(ContactType contactTypeHint) {
|
||||
this.contactTypeHint = contactTypeHint;
|
||||
}
|
||||
|
||||
public String getContactHint() {
|
||||
return contactHint;
|
||||
}
|
||||
|
||||
public void setContactHint(String contactHint) {
|
||||
this.contactHint = contactHint;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public NotifyState getNotifyState() {
|
||||
return notifyState;
|
||||
}
|
||||
|
||||
public void setNotifyState(NotifyState notifyState) {
|
||||
this.notifyState = notifyState;
|
||||
}
|
||||
|
||||
public Date getNotifiedAt() {
|
||||
return notifiedAt;
|
||||
}
|
||||
|
||||
public void setNotifiedAt(Date notifiedAt) {
|
||||
this.notifiedAt = notifiedAt;
|
||||
}
|
||||
|
||||
public Integer getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(Integer retryCount) {
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Date getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Date updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification fromDataModel(eu.eudat.data.entities.Notification entity) {
|
||||
this.id = entity.getId();
|
||||
this.contactHint = entity.getContactHint();
|
||||
this.contactTypeHint = entity.getContactTypeHint();
|
||||
this.createdAt = entity.getCreatedAt();
|
||||
this.data = entity.getData();
|
||||
this.isActive = entity.getIsActive();
|
||||
this.notifiedAt = entity.getNotifiedAt();
|
||||
this.notifyState = entity.getNotifyState();
|
||||
this.retryCount = entity.getRetryCount();
|
||||
this.type = entity.getType();
|
||||
this.updatedAt = entity.getUpdatedAt();
|
||||
this.userId = new UserInfo().fromDataModel(entity.getUserId());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public eu.eudat.data.entities.Notification toDataModel() throws Exception {
|
||||
eu.eudat.data.entities.Notification entity = new eu.eudat.data.entities.Notification();
|
||||
entity.setId(this.id);
|
||||
entity.setContactHint(this.contactHint);
|
||||
entity.setContactTypeHint(this.contactTypeHint);
|
||||
entity.setCreatedAt(this.createdAt);
|
||||
entity.setData(this.data);
|
||||
entity.setIsActive(this.isActive);
|
||||
entity.setNotifiedAt(this.notifiedAt);
|
||||
entity.setNotifyState(this.notifyState);
|
||||
entity.setRetryCount(this.retryCount);
|
||||
entity.setType(this.type);
|
||||
entity.setUpdatedAt(this.updatedAt);
|
||||
entity.setUserId(this.userId.toDataModel());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHint() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ eu.eudat.logic.proxy.allowed.host=https://eestore.paas2.uninett.no
|
|||
####################GENERIC MAIL CONFIGURATIONS#################
|
||||
mail.subject=Invitation to DMP Plan {dmpname}
|
||||
mail.from=TheApp@dev.cite.gr
|
||||
mail.modified.notification.subject=[OpenDMP] The {name} has been modified
|
||||
|
||||
####################SPRING MAIL CONFIGURATIONS#################
|
||||
spring.mail.default-encoding=UTF-8
|
||||
|
@ -76,3 +77,8 @@ http-logger.delay = 10
|
|||
#############GENERIC DATASOURCE CONFIGURATIONS#########
|
||||
database.driver-class-name=org.postgresql.Driver
|
||||
database.lock-fail-interval=120000
|
||||
|
||||
userguide.path=guide/
|
||||
|
||||
notification.rateInterval=30000
|
||||
notification.maxRetries=10
|
||||
|
|
|
@ -0,0 +1,304 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Simple Transactional Email</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%; }
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%; }
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top; }
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%; }
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px; }
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px; }
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%; }
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px; }
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%; }
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center; }
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px; }
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize; }
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px; }
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px; }
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline; }
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
width: auto; }
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center; }
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize; }
|
||||
.btn-primary table td {
|
||||
background-color: #3498db; }
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff; }
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0; }
|
||||
.first {
|
||||
margin-top: 0; }
|
||||
.align-center {
|
||||
text-align: center; }
|
||||
.align-right {
|
||||
text-align: right; }
|
||||
.align-left {
|
||||
text-align: left; }
|
||||
.clear {
|
||||
clear: both; }
|
||||
.mt0 {
|
||||
margin-top: 0; }
|
||||
.mb0 {
|
||||
margin-bottom: 0; }
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0; }
|
||||
.powered-by a {
|
||||
text-decoration: none; }
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0; }
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important; }
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important; }
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important; }
|
||||
table[class=body] .content {
|
||||
padding: 0 !important; }
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important; }
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important; }
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important; }
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important; }
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important; }}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%; }
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%; }
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important; }
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important; }
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
|
||||
<table class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p>Dear {recipient},</p>
|
||||
<p>{reasonName} just made changes to the {name}.</p>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{host}{path}/{id}" target="_blank">Click here to view it.</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
CREATE TABLE public."Notification" (
|
||||
id uuid NOT NULL,
|
||||
"UserId" uuid,
|
||||
"IsActive" integer NOT NULL,
|
||||
"Type" integer NOT NULL,
|
||||
"ContactTypeHint" integer,
|
||||
"ContactHint" character varying,
|
||||
"Data" character varying,
|
||||
"NotifyState" integer NOT NULL,
|
||||
"NotifiedAt" timestamp without time zone,
|
||||
"RetryCount" integer,
|
||||
"CreatedAt" timestamp without time zone,
|
||||
"UpdatedAt" timestamp without time zone
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public."Notification"
|
||||
ADD CONSTRAINT "Notification_pkey" PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public."Notification"
|
||||
ADD CONSTRAINT "NotificationUserReference" FOREIGN KEY ("UserId") REFERENCES public."UserInfo"(id);
|
Loading…
Reference in New Issue